SPB Git

spb/chat-spboucher Public

Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai

TypeScript 78.8% CSS 15.1% JavaScript 4.9% Shell 1.2%

feat: chat.spboucher.ai — universal OpenRouter chat, v1 production release

Full implementation per CLAUDE.md: single-user argon2id auth, SQLite
persistence with message-tree branching, dynamic OpenRouter model catalog
(409 models), normalized SSE streaming with true upstream cancellation,
usage/cost tracking, Instrument Panel UI with Model Rail, installable PWA,
launchd ops for node m4m64a + ngrok at chat.spboucher.ai.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 h ago (Aug 12, 2026) parent ec5869d

Showing 71 changed files with +14,583 and −295

modified .gitignore +3 −0
@@ -39,3 +39,6 @@ yarn-error.log*
39 39 # typescript
40 40 *.tsbuildinfo
41 41 next-env.d.ts
42 +
43 +# app data (SQLite, uploads)
44 +/data
deleted AGENTS.md +0 −9
@@ -1,9 +0,0 @@
1 <!-- BEGIN:nextjs-agent-rules -->
2
3 # This is NOT the Next.js you know
4
5 This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
6
7 This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
8
9 <!-- END:nextjs-agent-rules -->
modified CLAUDE.md +500 −1
@@ -1 +1,500 @@
1 @AGENTS.md
1 +# CLAUDE.md — chat.spboucher.ai
2 +
3 +> Author: Simon-Pierre Boucher
4 +> Contact: contact@spboucher.ai
5 +> Project: chat.spboucher.ai
6 +> Deployment target: node **m4m64a** (Apple Silicon, macOS) exposed via **ngrok** at **https://chat.spboucher.ai**
7 +
8 +This file is the single source of truth for Claude Code when working on this repository. Read it fully before writing any code. When in doubt, prefer the decisions written here over general best practices.
9 +
10 +---
11 +
12 +## 0. What This Project Is
13 +
14 +chat.spboucher.ai is a **private, universal chat interface over the entire OpenRouter ecosystem** — a personal ChatGPT/Claude-class product, not an API demo. One user (Simon-Pierre Boucher) must be able to authenticate from any device (especially a smartphone), pick nearly any model OpenRouter exposes, stream responses smoothly, leave, and resume the same conversation later from another device with full model attribution on every answer.
15 +
16 +---
17 +
18 +## 1. Critical Architectural Decision: OpenRouter-First
19 +
20 +chat.spboucher.ai must use **OpenRouter as the primary and initially exclusive external LLM gateway**.
21 +
22 +**Do not** build separate integrations for Anthropic, OpenAI, Google, xAI, Mistral, DeepSeek, Groq, Together, Fireworks, or any other provider in the initial implementation. OpenRouter already solves that abstraction and gives access to hundreds of continuously updated models behind one API.
23 +
24 +```
25 +chat.spboucher.ai
26 +
27 +
28 + Internal Chat Engine
29 +
30 +
31 + OpenRouter API
32 +
33 + ├── Anthropic models
34 + ├── OpenAI models
35 + ├── Google models
36 + ├── xAI models
37 + ├── Meta models
38 + ├── Qwen models
39 + ├── DeepSeek models
40 + ├── Mistral models
41 + ├── Moonshot models
42 + └── hundreds of additional models
43 +```
44 +
45 +The UI must remain **model-centric**, never provider-integration-centric.
46 +
47 +For version one:
48 +
49 +- ONE gateway
50 +- ONE API key
51 +- ONE model catalog
52 +- ONE streaming implementation
53 +- HUNDREDS of models
54 +
55 +Exploit this. Do not overengineer provider support.
56 +
57 +---
58 +
59 +## 2. Single External Credential
60 +
61 +Use one server-side credential: `OPENROUTER_API_KEY`.
62 +
63 +The key must:
64 +
65 +- exist **only** server-side (loaded from `.env`, which is gitignored)
66 +- never be exposed to the browser
67 +- never appear in logs
68 +- never be committed to Git
69 +- never be embedded in static JavaScript
70 +- never be returned in API responses
71 +
72 +All LLM requests flow through the application backend. The browser **never** talks to OpenRouter directly.
73 +
74 +```
75 +Browser (phone or desktop)
76 + │ POST /api/chat
77 +
78 +chat.spboucher.ai backend (node m4m64a)
79 + │ authenticated request
80 +
81 +OpenRouter → selected model
82 +```
83 +
84 +---
85 +
86 +## 3. Deployment: node m4m64a + ngrok
87 +
88 +The app runs **locally on node m4m64a** and is exposed publicly through an **ngrok tunnel bound to the reserved domain chat.spboucher.ai**.
89 +
90 +### 3.1 Runtime layout
91 +
92 +- Backend + frontend served from a single Node.js process (Next.js recommended, or Node + Vite SSR) listening on `localhost:<PORT>` (default `3000`).
93 +- Database: **SQLite** (better-sqlite3 or Drizzle + SQLite) on m4m64a's local disk, e.g. `~/apps/chat.spboucher.ai/data/chat.db`. No external DB dependency — this is a single-node, single-primary-user product. Enable WAL mode.
94 +- File uploads stored under `~/apps/chat.spboucher.ai/data/uploads/` with content-hash filenames.
95 +
96 +### 3.2 ngrok configuration
97 +
98 +Use an ngrok config file (`~/.config/ngrok/ngrok.yml`) with a named tunnel, not ad-hoc CLI flags:
99 +
100 +```yaml
101 +version: 3
102 +agent:
103 + authtoken: <NGROK_AUTHTOKEN> # never commit
104 +tunnels:
105 + chat:
106 + proto: http
107 + addr: 3000
108 + domain: chat.spboucher.ai
109 +```
110 +
111 +Start with `ngrok start chat`.
112 +
113 +Requirements:
114 +
115 +- The app must trust `X-Forwarded-*` headers from ngrok (set `trust proxy`) so auth cookies, rate limiting, and absolute URLs work correctly.
116 +- Auth cookies: `Secure`, `HttpOnly`, `SameSite=Lax` — TLS terminates at ngrok, so the app must treat itself as HTTPS-fronted.
117 +- Streaming (SSE / ReadableStream) must be verified **through the tunnel on a real phone**: send periodic keep-alive comments (`: ping\n\n`) every ~15s so ngrok and mobile radios don't kill idle streams.
118 +- WebSockets work through ngrok, but prefer **SSE/fetch streaming** for chat — it survives mobile network transitions better.
119 +
120 +### 3.3 Process supervision on macOS
121 +
122 +Run both the app and ngrok as **launchd LaunchAgents** (preferred on macOS) or via `pm2` if simpler:
123 +
124 +- `ai.spboucher.chat.app.plist` → runs the Node server, `KeepAlive: true`, `RunAtLoad: true`, logs to `~/apps/chat.spboucher.ai/logs/app.log`.
125 +- `ai.spboucher.chat.ngrok.plist` → runs `ngrok start chat`, `KeepAlive: true`.
126 +
127 +Provide these plists (or a `pm2 ecosystem.config.js`) plus a `deploy.sh` script in `ops/` that: pulls latest, installs deps, builds, runs DB migrations, restarts the agents. Deployment must be a single command on m4m64a. Include a nightly SQLite backup script (`sqlite3 chat.db ".backup ..."`).
128 +
129 +### 3.4 Security posture for a public tunnel
130 +
131 +Because ngrok makes the app publicly reachable:
132 +
133 +- Every route except `/login` and static assets requires an authenticated session.
134 +- Strong auth from day one: single-user credential + optional TOTP, argon2id password hashing, session tokens in DB, aggressive login rate limiting by IP.
135 +- No unauthenticated API surface. `/api/*` returns 401 without a valid session, always.
136 +- Standard security headers: CSP, X-Content-Type-Options, Referrer-Policy, frame denial.
137 +
138 +---
139 +
140 +## 4. Do Not Hardcode the Model Catalog
141 +
142 +The model catalog must be **dynamically synchronized** from the OpenRouter models endpoint. Never maintain a manual list of hundreds of models.
143 +
144 +Periodically fetch and normalize: model id, display name, provider, context length, input/output modalities, supported parameters, pricing, architecture, description.
145 +
146 +```ts
147 +interface ModelDefinition {
148 + id: string; // exact OpenRouter model ID — opaque
149 + name: string;
150 + provider?: string;
151 + description?: string;
152 + contextLength?: number;
153 + pricing?: {
154 + prompt?: number;
155 + completion?: number;
156 + image?: number;
157 + request?: number;
158 + };
159 + capabilities: {
160 + text: boolean;
161 + vision: boolean;
162 + reasoning: boolean;
163 + tools: boolean;
164 + structuredOutput: boolean;
165 + };
166 + metadata: {
167 + architecture?: string;
168 + tokenizer?: string;
169 + raw?: unknown;
170 + };
171 +}
172 +```
173 +
174 +- Persist the normalized catalog in SQLite; the model picker loads instantly from cache.
175 +- Sync flow: `OpenRouter → server sync job → database cache → application`. Manual refresh from settings; scheduled refresh every few hours.
176 +- Adding a new OpenRouter model must **never require a deployment**.
177 +- Treat model IDs (`provider/model-name`) as **opaque strings**. Never infer behavior by string-parsing IDs. Store the exact ID in an `openrouter_model_id` column and send it back verbatim.
178 +
179 +### Removed models
180 +
181 +Catalogs change. Historical messages must remain intact: if a past generation used a model that no longer exists, still display its original stored metadata, marked `Unavailable`. Never corrupt old conversations.
182 +
183 +---
184 +
185 +## 5. Model Catalog UX
186 +
187 +With hundreds of models, a basic dropdown is forbidden. Build a **searchable model browser** supporting:
188 +
189 +- fuzzy search
190 +- provider filtering
191 +- favorites (persisted in DB, shown at top)
192 +- recently used models (tracked, shown near top)
193 +- pinned models
194 +- capability filters: reasoning, vision, tool use
195 +- context-window filtering, price filtering, free-model filtering
196 +- sorts: context window, price, alphabetical, recently added
197 +
198 +The picker must remain fast with 300+ entries — use list virtualization. On mobile, the picker opens as a **full-height bottom sheet** with a sticky search field, not a tiny dropdown.
199 +
200 +---
201 +
202 +## 6. Conversations, Model Switching, Branching
203 +
204 +- A conversation never belongs permanently to one model. The user can switch models for any new message.
205 +- Every assistant generation stores: `model_id`, `model_name`, `provider`, `generation_id`, `created_at`. The UI labels each assistant response with the model that produced it. This is critical for auditing and comparing models.
206 +- **Regenerate** on every assistant answer, with: same model, another model, or a favorite. Regeneration **creates a branch** — it never destroys the original answer.
207 +
208 +```
209 +Prompt
210 + ├── Claude answer
211 + ├── GPT answer
212 + └── Gemini answer
213 +```
214 +
215 +- Design the data model so a future **side-by-side comparison mode** (same prompt against N checked models, columns on desktop, swipeable panes on mobile) is possible. Don't fully implement comparison in v1, but the schema must not prevent it.
216 +
217 +---
218 +
219 +## 7. Streaming
220 +
221 +Use OpenRouter streaming for every model that supports it. The experience must be genuinely incremental — never wait for completion before showing output.
222 +
223 +```
224 +request → OpenRouter stream → server stream parser
225 + → normalized application events → browser ReadableStream
226 + → incremental rendering
227 +```
228 +
229 +### Internal streaming protocol
230 +
231 +The browser receives **normalized application events**, never raw OpenRouter wire format:
232 +
233 +```ts
234 +type ChatStreamEvent =
235 + | { type: "generation.start"; generationId: string; model: string }
236 + | { type: "content.delta"; text: string }
237 + | { type: "reasoning.delta"; text: string }
238 + | { type: "tool.start"; toolCallId: string; name: string }
239 + | { type: "tool.delta"; toolCallId: string; argumentsDelta: string }
240 + | { type: "usage"; promptTokens?: number; completionTokens?: number; totalTokens?: number; cost?: number }
241 + | { type: "generation.end" }
242 + | { type: "generation.error"; message: string; retryable: boolean };
243 +```
244 +
245 +This abstraction leaves the door open for local models, private endpoints, direct provider APIs, or other gateways later — without rewriting the frontend.
246 +
247 +---
248 +
249 +## 8. OpenRouter Client Module
250 +
251 +All OpenRouter interaction lives in one module. **No scattered `fetch()` calls anywhere else.**
252 +
253 +```
254 +src/lib/openrouter/
255 +├── client.ts
256 +├── models.ts
257 +├── stream.ts
258 +├── schemas.ts
259 +├── types.ts
260 +├── errors.ts
261 +├── pricing.ts
262 +├── capabilities.ts
263 +└── index.ts
264 +```
265 +
266 +Every created source file in this repository must begin with:
267 +
268 +```
269 +// Author: Simon-Pierre Boucher
270 +// Contact: contact@spboucher.ai
271 +// Project: chat.spboucher.ai
272 +```
273 +
274 +### Model routing
275 +
276 +OpenRouter can route across providers (provider preference, fallbacks, latency/price optimization, data-policy preferences). Do **not** expose these in the initial UI, but design the request layer so routing configuration can eventually be attached to an individual generation.
277 +
278 +---
279 +
280 +## 9. Errors, Retries, Cancellation, State
281 +
282 +### Error normalization
283 +
284 +Normalize before anything reaches the UI:
285 +
286 +`AUTHENTICATION_ERROR · RATE_LIMIT · MODEL_UNAVAILABLE · PROVIDER_UNAVAILABLE · CONTEXT_TOO_LARGE · INVALID_REQUEST · CONTENT_REJECTED · TIMEOUT · NETWORK_ERROR · UNKNOWN`
287 +
288 +```ts
289 +interface NormalizedAIError {
290 + code: AIErrorCode;
291 + message: string;
292 + retryable: boolean;
293 + modelId?: string;
294 + status?: number;
295 +}
296 +```
297 +
298 +Show useful errors without exposing sensitive internals.
299 +
300 +### Retries
301 +
302 +Conservative, bounded exponential backoff for transient cases only (network interruption, temporary upstream failure, selected 5xx). Never retry auth failures, invalid prompts, context-too-large, or non-retryable client errors. Never create infinite retry loops.
303 +
304 +### Cancellation
305 +
306 +"Stop" must abort the backend request **and** the upstream OpenRouter stream via `AbortController`. Never fake it by merely halting rendering while continuing to consume and pay for the generation.
307 +
308 +### Generation state machine
309 +
310 +Each generation is an explicit state machine: `queued → starting → streaming → completed | cancelled | failed`, with a persistent `generation_id`. No grab-bag `isLoading` booleans. This identity powers retry, branching, usage accounting, cancellation, debugging, and analytics.
311 +
312 +---
313 +
314 +## 10. Context Management
315 +
316 +- Use model metadata to know approximate context windows. Estimate conversation token usage **before** each request; never silently exceed the limit.
317 +- Display subtle context usage where useful: `42k / 200k`.
318 +- Database history and model context are **separate concepts**. A conversation may hold thousands of messages; never ship the whole thing to OpenRouter indefinitely.
319 +
320 +```
321 +Conversation
322 +
323 +
324 +Context Compiler
325 + │ ├── system instructions
326 + │ ├── memory
327 + │ ├── recent messages
328 + │ ├── selected historical messages
329 + │ ├── file context
330 + │ └── summaries
331 +
332 +OpenRouter request
333 +```
334 +
335 +The context compiler must be able to: include recent messages, preserve system instructions, retain pinned context, trim low-value older context, and eventually summarize older context.
336 +
337 +---
338 +
339 +## 11. Usage & Cost Tracking
340 +
341 +Capture per generation whenever possible: prompt tokens, completion tokens, reasoning tokens, cached tokens, total tokens, estimated cost, actual reported cost.
342 +
343 +```
344 +generation_usage
345 +----------------
346 +id
347 +generation_id
348 +model_id
349 +prompt_tokens
350 +completion_tokens
351 +reasoning_tokens
352 +cached_tokens
353 +total_tokens
354 +estimated_cost_usd
355 +reported_cost_usd
356 +created_at
357 +```
358 +
359 +### Display
360 +
361 +Each response may show subtle metadata — `Claude Opus · 2.8k tokens · $0.04` — with full details on tap/hover in an inspector. Never clutter the conversation.
362 +
363 +### Usage dashboard
364 +
365 +A settings/usage page with Today / 7 days / 30 days / All time: total requests, input/output/total tokens, total cost, average response cost, most used models, cost by model / provider / conversation. Architect for charts.
366 +
367 +---
368 +
369 +## 12. Mobile-First: The App Must Feel Native on a Smartphone
370 +
371 +This is a hard requirement, not a nice-to-have. The primary usage device is a phone.
372 +
373 +### 12.1 PWA
374 +
375 +- Full **installable PWA**: web app manifest (name, maskable icons, `display: standalone`, theme color per light/dark), service worker.
376 +- Service worker caches the app shell and model catalog for instant cold starts; conversation data stays network-first (single source of truth is the server DB on m4m64a).
377 +- App icon and splash must look deliberate, not default.
378 +
379 +### 12.2 Layout & interaction
380 +
381 +- **Mobile-first CSS**: design for ~390px width first, then progressively enhance to tablet and desktop (where the sidebar becomes persistent).
382 +- Respect safe areas: `viewport-fit=cover` + `env(safe-area-inset-*)` padding for notch and home indicator.
383 +- The composer must handle the mobile keyboard correctly: `dvh` units / VisualViewport API so the input is never hidden behind the keyboard and the message list stays anchored to the newest message.
384 +- Touch targets ≥ 44×44px. Message actions (copy, regenerate, branch, model info) live in a tap/long-press action row — never hover-only menus.
385 +- Conversation list: swipeable left drawer on mobile, persistent sidebar ≥ 1024px.
386 +- Model picker: bottom sheet on mobile (see §5).
387 +- Swipe gestures: swipe between sibling branches of a message; pull-to-refresh on the conversation list.
388 +- Streaming text auto-follows at the bottom; the instant the user scrolls up, auto-follow pauses with a "↓ jump to latest" pill.
389 +- 60fps scrolling on a phone with a 2,000-message conversation: virtualize the message list.
390 +- `prefers-reduced-motion` respected everywhere.
391 +
392 +### 12.3 Mobile network reality
393 +
394 +- SSE keep-alives (§3.2) so streams survive radio sleep.
395 +- If a stream drops mid-generation, the client reconnects and resyncs generation state from the server (the generation state machine makes this possible).
396 +- Optimistic send: the user's message appears instantly, marked pending until the server confirms.
397 +
398 +---
399 +
400 +## 13. Design Direction: Legendary, Not Templated
401 +
402 +The bar: someone seeing a screenshot should recognize this app instantly. It must not look like an AI-generated default, and explicitly **not** like a ChatGPT or Claude clone.
403 +
404 +### 13.1 What to avoid
405 +
406 +Three looks are forbidden because they read as generated defaults: (1) warm cream background + high-contrast serif + terracotta accent; (2) near-black + single acid-green/vermilion accent; (3) broadsheet hairline-rule newspaper layout. Also avoid stock "AI product" purple-gradient glassmorphism, rainbow provider branding, and bouncing-dots loaders.
407 +
408 +### 13.2 Direction — "Instrument Panel"
409 +
410 +The identity concept: this is a **personal instrument for operating hundreds of AI models** — closer to a beautifully machined control surface (studio console, cockpit, high-end synthesizer) than a messaging app. Precision, quiet confidence, dense information handled calmly.
411 +
412 +**Palette (dark-first, with a fully designed light theme):**
413 +
414 +| Token | Hex | Role |
415 +|---|---|---|
416 +| `--ink-950` | `#0E1116` | primary surface (deep blue-graphite, not pure black) |
417 +| `--ink-900` | `#151A22` | raised surfaces: composer, sheets, cards |
418 +| `--ink-700` | `#2A3240` | borders, dividers, inactive strokes |
419 +| `--fog-300` | `#AAB4C3` | secondary text, metadata |
420 +| `--fog-050` | `#EEF2F7` | primary text |
421 +| `--signal-500` | `#3FB6A8` | the single accent — calibrated teal, used **only** for live/active states: streaming indicator, active model, send button, focus rings |
422 +| `--amber-500` | `#E0A458` | cost/usage semantics only: token meters, price chips |
423 +
424 +One accent for "alive", one for "cost". Nothing else gets color. Providers are distinguished by small monochrome glyph badges, never by their brand colors.
425 +
426 +**Typography:**
427 +
428 +- Display/UI: **Söhne** (fallback **Inter**), tight tracking, weights 400/500/600 — labels, buttons, model names.
429 +- Chat body: **Inter** at a generous 16–17px / 1.6 line height on mobile — reading comfort is the product.
430 +- Data/mono: **Berkeley Mono** (fallback **JetBrains Mono**) for code blocks, token counts, model IDs, costs. The mono face is a first-class citizen of the identity: every model ID, cost, and context meter is set in it. That's what makes the app feel like an instrument.
431 +
432 +**Signature element — the Model Rail.** A slim, always-visible strip attached to the composer showing the current model as a machined "cartridge": mono model ID, provider glyph, live context meter (`42k / 200k` as a thin filling bar), and price-per-1M chip. Tapping it opens the model bottom sheet. During streaming, the rail's edge carries a subtle teal signal pulse. Switching models visibly swaps the cartridge with a short mechanical slide. This one element carries the entire identity; everything around it stays quiet and disciplined.
433 +
434 +**Motion:** restrained and purposeful. Streaming cursor is a soft teal caret. Bottom sheets use spring easing (~250ms). Branch switching slides horizontally. No ambient background animation. All motion honors `prefers-reduced-motion`.
435 +
436 +**Message rendering:** first-class Markdown, syntax-highlighted code blocks with copy button and language label, LaTeX (KaTeX), tables with horizontal scroll on mobile. Assistant messages are full-width text blocks with a slim attribution header (mono model name · timestamp · cost chip); user messages are compact right-aligned cards. No avatars.
437 +
438 +**Quality floor (never announce it, always meet it):** responsive down to 320px, visible keyboard focus states, WCAG AA contrast in both themes, reduced motion respected, light theme fully designed (`#F6F8FA` paper, same teal, ink text — not an inverted afterthought).
439 +
440 +### 13.3 Copy voice
441 +
442 +Interface copy is plain, active, specific: "Save changes," not "Submit." Errors state what happened and what to do next — never vague, never apologetic. Empty states invite action ("Pick a model and ask something"). One vocabulary throughout: a "model" is always a model, a "conversation" always a conversation.
443 +
444 +---
445 +
446 +## 14. Future Direct Providers
447 +
448 +OpenRouter is the only provider initially, but keep a narrow gateway abstraction:
449 +
450 +```ts
451 +interface ModelGateway {
452 + stream(request: GenerationRequest): AsyncIterable<ChatStreamEvent>;
453 + listModels(): Promise<ModelDefinition[]>;
454 +}
455 +```
456 +
457 +Initial implementation: `OpenRouterGateway`. Future possibilities (do **not** implement now): `LocalGateway`, `MLXGateway` (running directly on m4m64a's Apple Silicon), `PrivateClusterGateway`. The goal is only that the frontend never becomes inseparable from OpenRouter.
458 +
459 +---
460 +
461 +## 15. Product Philosophy
462 +
463 +**Simple externally, powerful internally.**
464 +
465 +The user sees: choose model → write message → receive answer.
466 +
467 +The internals handle: authentication, conversation persistence, context compilation, model metadata, streaming, error handling, usage, cost, branching, cancellation, search, security — without exposing complexity.
468 +
469 +---
470 +
471 +## 16. Development Priority
472 +
473 +Implement strictly in this order. Do not start secondary features before the fundamental chat loop is exceptionally stable.
474 +
475 +1. Authentication (single user, session cookies, rate-limited login)
476 +2. Database (SQLite schema + migrations)
477 +3. OpenRouter server client (`src/lib/openrouter/`)
478 +4. Dynamic model catalog (sync + cache)
479 +5. Conversation persistence
480 +6. Chat UI (mobile-first shell, per §12–13)
481 +7. Streaming (SSE through ngrok, verified on a real phone)
482 +8. Stop generation
483 +9. Markdown + code + LaTeX rendering
484 +10. Model search (browser/bottom sheet)
485 +11. Favorites + recent models
486 +12. Message regeneration
487 +13. Conversation branching
488 +14. Search history
489 +15. Usage/cost tracking + dashboard
490 +16. Settings
491 +17. File/image support (vision-capable models)
492 +18. Advanced context manager
493 +19. PWA polish (install, offline shell, icons/splash)
494 +20. Ops hardening (launchd agents, deploy.sh, log rotation, nightly DB backup)
495 +
496 +---
497 +
498 +## 17. Definition of Success
499 +
500 +The first production release is successful when Simon-Pierre Boucher can open **https://chat.spboucher.ai on his phone**, authenticate securely through the ngrok tunnel to m4m64a, install the app to his home screen, select almost any model exposed through OpenRouter from a fast, beautiful bottom-sheet picker, send a message, immediately receive a smooth streaming response even on cellular, leave the application, return later from another device, find the complete conversation, continue it with the same or a completely different model, and inspect which model generated every answer and what each one cost — inside an interface distinctive enough that no one could mistake it for anything else.
modified README.md +73 −20
@@ -1,36 +1,89 @@
1 This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1 +# chat.spboucher.ai
2 2
3 ## Getting Started
3 +![Status](https://img.shields.io/badge/status-live-3FB6A8)
4 +![Models](https://img.shields.io/badge/models-409%2B-3FB6A8)
5 +![Next.js](https://img.shields.io/badge/Next.js-16-0E1116)
6 +![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6)
7 +![SQLite](https://img.shields.io/badge/SQLite-WAL-E0A458)
8 +![Gateway](https://img.shields.io/badge/gateway-OpenRouter-8B5CF6)
9 +![PWA](https://img.shields.io/badge/PWA-installable-3FB6A8)
10 +![Node](https://img.shields.io/badge/deploy-m4m64a%20%C2%B7%20ngrok-151A22)
11 +![License](https://img.shields.io/badge/license-private-2A3240)
4 12
5 First, run the development server:
13 +> **Author:** Simon-Pierre Boucher
14 +> **Contact:** [contact@spboucher.ai](mailto:contact@spboucher.ai)
15 +> **Production:** [https://chat.spboucher.ai](https://chat.spboucher.ai)
16 +
17 +A private, universal chat interface over the entire **OpenRouter** ecosystem — one login, one server-side API key, 400+ models behind a single catalog, full conversation persistence with branching and per-message model attribution. Designed mobile-first as an installable PWA: a personal ChatGPT/Claude-class product, not an API demo.
18 +
19 +---
20 +
21 +## Highlights
22 +
23 +- **One gateway, hundreds of models** — the catalog is synced dynamically from OpenRouter into SQLite (never hand-maintained); removed models are marked `Unavailable`, historical conversations stay intact forever.
24 +- **Real streaming** — normalized SSE event protocol (`generation.start`, `content.delta`, `reasoning.delta`, `usage`, …) with 15 s keep-alive pings that survive ngrok and mobile radio sleep. Stop truly aborts the upstream OpenRouter stream.
25 +- **Branching & regeneration** — regenerating never destroys an answer; siblings live side by side with ‹ 1/2 › navigation, so the same prompt can be compared across models.
26 +- **Generation state machine**`queued → starting → streaming → completed | cancelled | failed`, persisted per generation; a dropped mobile connection resyncs from the server.
27 +- **Usage & cost tracking** — prompt/completion/reasoning/cached tokens and reported cost per generation, with a Today / 7d / 30d / All-time dashboard broken down by model.
28 +- **Instrument Panel design** — ink/graphite surfaces, one teal accent for "alive", one amber accent for "cost", mono type for model IDs and meters, and the signature **Model Rail** cartridge attached to the composer (provider glyph · mono model ID · live context meter · price-per-1M chip).
29 +- **Security posture for a public tunnel** — argon2id single-user auth, DB-backed sessions, aggressive login rate limiting, `Secure`/`HttpOnly`/`SameSite` cookies, CSP + security headers, zero unauthenticated API surface.
30 +
31 +## Architecture
32 +
33 +```
34 +Browser (phone or desktop, PWA)
35 + │ POST /api/chat (SSE)
36 +
37 +Next.js 16 backend — node m4m64a (Apple Silicon, macOS)
38 + │ src/lib/openrouter/ (single client module, normalized errors/streaming)
39 +
40 +OpenRouter API ──► Anthropic · OpenAI · Google · Meta · Qwen · DeepSeek · Mistral · …
41 +```
42 +
43 +- **Runtime:** single Node.js process (API + UI), port 3000, exposed via ngrok at `chat.spboucher.ai`
44 +- **Database:** SQLite (better-sqlite3, WAL) — conversations, message tree, generations, usage, model catalog cache, sessions
45 +- **Supervision:** three launchd agents (`app`, `ngrok`, nightly `backup` at 03:30 with 14-day retention)
46 +
47 +## Local development
6 48
7 49 ```bash
50 +cp .env.example .env # fill in OPENROUTER_API_KEY
51 +npm install
52 +npm run set-password <username> [password] # generates a strong one if omitted
8 53 npm run dev
9 # or
10 yarn dev
11 # or
12 pnpm dev
13 # or
14 bun dev
15 54 ```
16 55
17 Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
56 +## Deployment (m4m64a)
18 57
19 You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
58 +```bash
59 +rsync -az --delete --exclude node_modules --exclude .next --exclude data --exclude .env \
60 + ~/Desktop/chat-spboucher/ M4M64a:apps/chat.spboucher.ai/
61 +ssh M4M64a '~/apps/chat.spboucher.ai/ops/deploy.sh'
62 +```
20 63
21 This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
64 +| launchd agent | Role |
65 +|---|---|
66 +| `ai.spboucher.chat.app` | `npm run start` on port 3000, KeepAlive |
67 +| `ai.spboucher.chat.ngrok` | `ngrok http --url=chat.spboucher.ai 3000`, KeepAlive |
68 +| `ai.spboucher.chat.backup` | nightly SQLite backup 03:30, 14-day retention |
22 69
23 ## Learn More
70 +## API surface
24 71
25 To learn more about Next.js, take a look at the following resources:
72 +All routes require a session cookie except `POST /api/auth/login`.
26 73
27 - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
74 +| Endpoint | Purpose |
75 +|---|---|
76 +| `POST /api/chat` | send a message or regenerate (`{regenerateOf}`) — returns a normalized SSE stream |
77 +| `POST /api/generations/:id/cancel` | abort the upstream OpenRouter generation |
78 +| `GET /api/models` · `POST /api/models/sync` | cached catalog · manual refresh |
79 +| `POST /api/models/prefs` | favorites / pinned models |
80 +| `GET/PATCH/DELETE /api/conversations[/:id]` | conversation CRUD, branch leaf selection |
81 +| `GET /api/usage?period=today\|7d\|30d\|all` | usage & cost dashboard data |
29 82
30 You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
83 +## License
31 84
32 ## Deploy on Vercel
85 +Private software — © Simon-Pierre Boucher. All rights reserved.
33 86
34 The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
87 +---
35 88
36 Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
89 +**Simon-Pierre Boucher** · [contact@spboucher.ai](mailto:contact@spboucher.ai) · [chat.spboucher.ai](https://chat.spboucher.ai)
modified next.config.ts +7 −1
@@ -1,7 +1,13 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
1 5 import type { NextConfig } from "next";
2 6
3 7 const nextConfig: NextConfig = {
4 /* config options here */
8 + // Native modules stay external to the server bundle.
9 + serverExternalPackages: ["better-sqlite3", "@node-rs/argon2"],
10 + poweredByHeader: false,
5 11 };
6 12
7 13 export default nextConfig;
added ops/ai.spboucher.chat.app.plist +31 −0
@@ -0,0 +1,31 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 +<plist version="1.0">
4 +<dict>
5 + <key>Label</key>
6 + <string>ai.spboucher.chat.app</string>
7 + <key>WorkingDirectory</key>
8 + <string>/Users/simon-pierreboucher/apps/chat.spboucher.ai</string>
9 + <key>ProgramArguments</key>
10 + <array>
11 + <string>/bin/zsh</string>
12 + <string>-lc</string>
13 + <string>exec npm run start</string>
14 + </array>
15 + <key>EnvironmentVariables</key>
16 + <dict>
17 + <key>NODE_ENV</key>
18 + <string>production</string>
19 + <key>PORT</key>
20 + <string>3000</string>
21 + </dict>
22 + <key>RunAtLoad</key>
23 + <true/>
24 + <key>KeepAlive</key>
25 + <true/>
26 + <key>StandardOutPath</key>
27 + <string>/Users/simon-pierreboucher/apps/chat.spboucher.ai/logs/app.log</string>
28 + <key>StandardErrorPath</key>
29 + <string>/Users/simon-pierreboucher/apps/chat.spboucher.ai/logs/app.log</string>
30 +</dict>
31 +</plist>
added ops/ai.spboucher.chat.backup.plist +25 −0
@@ -0,0 +1,25 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 +<plist version="1.0">
4 +<dict>
5 + <key>Label</key>
6 + <string>ai.spboucher.chat.backup</string>
7 + <key>ProgramArguments</key>
8 + <array>
9 + <string>/bin/zsh</string>
10 + <string>-lc</string>
11 + <string>exec /Users/simon-pierreboucher/apps/chat.spboucher.ai/ops/backup.sh</string>
12 + </array>
13 + <key>StartCalendarInterval</key>
14 + <dict>
15 + <key>Hour</key>
16 + <integer>3</integer>
17 + <key>Minute</key>
18 + <integer>30</integer>
19 + </dict>
20 + <key>StandardOutPath</key>
21 + <string>/Users/simon-pierreboucher/apps/chat.spboucher.ai/logs/backup.log</string>
22 + <key>StandardErrorPath</key>
23 + <string>/Users/simon-pierreboucher/apps/chat.spboucher.ai/logs/backup.log</string>
24 +</dict>
25 +</plist>
added ops/ai.spboucher.chat.ngrok.plist +22 −0
@@ -0,0 +1,22 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 +<plist version="1.0">
4 +<dict>
5 + <key>Label</key>
6 + <string>ai.spboucher.chat.ngrok</string>
7 + <key>ProgramArguments</key>
8 + <array>
9 + <string>/bin/zsh</string>
10 + <string>-lc</string>
11 + <string>exec ngrok http --url=chat.spboucher.ai 3000 --log=stdout</string>
12 + </array>
13 + <key>RunAtLoad</key>
14 + <true/>
15 + <key>KeepAlive</key>
16 + <true/>
17 + <key>StandardOutPath</key>
18 + <string>/Users/simon-pierreboucher/apps/chat.spboucher.ai/logs/ngrok.log</string>
19 + <key>StandardErrorPath</key>
20 + <string>/Users/simon-pierreboucher/apps/chat.spboucher.ai/logs/ngrok.log</string>
21 +</dict>
22 +</plist>
added ops/backup.sh +24 −0
@@ -0,0 +1,24 @@
1 +#!/bin/zsh
2 +# Author: Simon-Pierre Boucher
3 +# Contact: contact@spboucher.ai
4 +# Project: chat.spboucher.ai
5 +#
6 +# Nightly SQLite backup with 14-day retention. Schedule via ops/ai.spboucher.chat.backup.plist.
7 +
8 +set -euo pipefail
9 +
10 +APP_DIR="$HOME/apps/chat.spboucher.ai"
11 +DB="$APP_DIR/data/chat.db"
12 +BACKUP_DIR="$APP_DIR/data/backups"
13 +STAMP=$(date +%Y%m%d-%H%M%S)
14 +
15 +mkdir -p "$BACKUP_DIR"
16 +[ -f "$DB" ] || { echo "no database at $DB"; exit 0; }
17 +
18 +sqlite3 "$DB" ".backup '$BACKUP_DIR/chat-$STAMP.db'"
19 +gzip "$BACKUP_DIR/chat-$STAMP.db"
20 +
21 +# retention: keep 14 days
22 +find "$BACKUP_DIR" -name 'chat-*.db.gz' -mtime +14 -delete
23 +
24 +echo "backup written: $BACKUP_DIR/chat-$STAMP.db.gz"
added ops/deploy.sh +51 −0
@@ -0,0 +1,51 @@
1 +#!/bin/zsh
2 +# Author: Simon-Pierre Boucher
3 +# Contact: contact@spboucher.ai
4 +# Project: chat.spboucher.ai
5 +#
6 +# Single-command deploy, run ON m4m64a:
7 +# ~/apps/chat.spboucher.ai/ops/deploy.sh
8 +# Pulls latest (if a git remote exists), installs deps, builds, restarts agents.
9 +
10 +set -euo pipefail
11 +
12 +APP_DIR="$HOME/apps/chat.spboucher.ai"
13 +cd "$APP_DIR"
14 +
15 +echo "==> chat.spboucher.ai deploy ($(date))"
16 +
17 +if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git remote get-url origin >/dev/null 2>&1; then
18 + echo "==> git pull"
19 + git pull --ff-only
20 +fi
21 +
22 +echo "==> npm ci"
23 +npm ci
24 +
25 +echo "==> build"
26 +npm run build
27 +
28 +mkdir -p "$APP_DIR/logs" "$APP_DIR/data"
29 +
30 +echo "==> restart launchd agents"
31 +for AGENT in ai.spboucher.chat.app ai.spboucher.chat.ngrok; do
32 + PLIST="$HOME/Library/LaunchAgents/$AGENT.plist"
33 + if [ ! -f "$PLIST" ]; then
34 + cp "$APP_DIR/ops/$AGENT.plist" "$PLIST"
35 + echo " installed $AGENT.plist"
36 + fi
37 + launchctl unload "$PLIST" 2>/dev/null || true
38 + launchctl load "$PLIST"
39 + echo " restarted $AGENT"
40 +done
41 +
42 +echo "==> waiting for health"
43 +for i in {1..30}; do
44 + if curl -sf -o /dev/null http://127.0.0.1:3000/login; then
45 + echo "==> app is up: http://127.0.0.1:3000 → https://chat.spboucher.ai"
46 + exit 0
47 + fi
48 + sleep 1
49 +done
50 +echo "!! app did not come up within 30s — check $APP_DIR/logs/app.log" >&2
51 +exit 1
added package-lock.json +8396 −0
@@ -0,0 +1,8396 @@
1 +{
2 + "name": "chat-spboucher",
3 + "version": "0.1.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "": {
8 + "name": "chat-spboucher",
9 + "version": "0.1.0",
10 + "dependencies": {
11 + "@node-rs/argon2": "^2.0.2",
12 + "better-sqlite3": "^13.0.3",
13 + "highlight.js": "^11.12.0",
14 + "katex": "^0.18.4",
15 + "next": "16.3.0",
16 + "react": "19.2.8",
17 + "react-dom": "19.2.8",
18 + "react-markdown": "^10.1.0",
19 + "rehype-highlight": "^7.0.2",
20 + "rehype-katex": "^7.0.1",
21 + "remark-gfm": "^4.0.1",
22 + "remark-math": "^6.0.0"
23 + },
24 + "devDependencies": {
25 + "@types/better-sqlite3": "^9.6.0",
26 + "@types/node": "^20",
27 + "@types/react": "^19",
28 + "@types/react-dom": "^19",
29 + "eslint": "^9",
30 + "eslint-config-next": "16.3.0",
31 + "typescript": "^5"
32 + }
33 + },
34 + "node_modules/@babel/code-frame": {
35 + "version": "7.29.7",
36 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
37 + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
38 + "dev": true,
39 + "license": "MIT",
40 + "dependencies": {
41 + "@babel/helper-validator-identifier": "^7.29.7",
42 + "js-tokens": "^4.0.0",
43 + "picocolors": "^1.1.1"
44 + },
45 + "engines": {
46 + "node": ">=6.9.0"
47 + }
48 + },
49 + "node_modules/@babel/compat-data": {
50 + "version": "7.29.7",
51 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
52 + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
53 + "dev": true,
54 + "license": "MIT",
55 + "engines": {
56 + "node": ">=6.9.0"
57 + }
58 + },
59 + "node_modules/@babel/core": {
60 + "version": "7.29.7",
61 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
62 + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
63 + "dev": true,
64 + "license": "MIT",
65 + "dependencies": {
66 + "@babel/code-frame": "^7.29.7",
67 + "@babel/generator": "^7.29.7",
68 + "@babel/helper-compilation-targets": "^7.29.7",
69 + "@babel/helper-module-transforms": "^7.29.7",
70 + "@babel/helpers": "^7.29.7",
71 + "@babel/parser": "^7.29.7",
72 + "@babel/template": "^7.29.7",
73 + "@babel/traverse": "^7.29.7",
74 + "@babel/types": "^7.29.7",
75 + "@jridgewell/remapping": "^2.3.5",
76 + "convert-source-map": "^2.0.0",
77 + "debug": "^4.1.0",
78 + "gensync": "^1.0.0-beta.2",
79 + "json5": "^2.2.3",
80 + "semver": "^6.3.1"
81 + },
82 + "engines": {
83 + "node": ">=6.9.0"
84 + },
85 + "funding": {
86 + "type": "opencollective",
87 + "url": "https://opencollective.com/babel"
88 + }
89 + },
90 + "node_modules/@babel/generator": {
91 + "version": "7.29.8",
92 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
93 + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
94 + "dev": true,
95 + "license": "MIT",
96 + "dependencies": {
97 + "@babel/parser": "^7.29.8",
98 + "@babel/types": "^7.29.8",
99 + "@jridgewell/gen-mapping": "^0.3.12",
100 + "@jridgewell/trace-mapping": "^0.3.28",
101 + "jsesc": "^3.0.2"
102 + },
103 + "engines": {
104 + "node": ">=6.9.0"
105 + }
106 + },
107 + "node_modules/@babel/helper-compilation-targets": {
108 + "version": "7.29.7",
109 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
110 + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
111 + "dev": true,
112 + "license": "MIT",
113 + "dependencies": {
114 + "@babel/compat-data": "^7.29.7",
115 + "@babel/helper-validator-option": "^7.29.7",
116 + "browserslist": "^4.24.0",
117 + "lru-cache": "^5.1.1",
118 + "semver": "^6.3.1"
119 + },
120 + "engines": {
121 + "node": ">=6.9.0"
122 + }
123 + },
124 + "node_modules/@babel/helper-globals": {
125 + "version": "7.29.7",
126 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
127 + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
128 + "dev": true,
129 + "license": "MIT",
130 + "engines": {
131 + "node": ">=6.9.0"
132 + }
133 + },
134 + "node_modules/@babel/helper-module-imports": {
135 + "version": "7.29.7",
136 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
137 + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
138 + "dev": true,
139 + "license": "MIT",
140 + "dependencies": {
141 + "@babel/traverse": "^7.29.7",
142 + "@babel/types": "^7.29.7"
143 + },
144 + "engines": {
145 + "node": ">=6.9.0"
146 + }
147 + },
148 + "node_modules/@babel/helper-module-transforms": {
149 + "version": "7.29.7",
150 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
151 + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
152 + "dev": true,
153 + "license": "MIT",
154 + "dependencies": {
155 + "@babel/helper-module-imports": "^7.29.7",
156 + "@babel/helper-validator-identifier": "^7.29.7",
157 + "@babel/traverse": "^7.29.7"
158 + },
159 + "engines": {
160 + "node": ">=6.9.0"
161 + },
162 + "peerDependencies": {
163 + "@babel/core": "^7.0.0"
164 + }
165 + },
166 + "node_modules/@babel/helper-string-parser": {
167 + "version": "7.29.7",
168 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
169 + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
170 + "dev": true,
171 + "license": "MIT",
172 + "engines": {
173 + "node": ">=6.9.0"
174 + }
175 + },
176 + "node_modules/@babel/helper-validator-identifier": {
177 + "version": "7.29.7",
178 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
179 + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
180 + "dev": true,
181 + "license": "MIT",
182 + "engines": {
183 + "node": ">=6.9.0"
184 + }
185 + },
186 + "node_modules/@babel/helper-validator-option": {
187 + "version": "7.29.7",
188 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
189 + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
190 + "dev": true,
191 + "license": "MIT",
192 + "engines": {
193 + "node": ">=6.9.0"
194 + }
195 + },
196 + "node_modules/@babel/helpers": {
197 + "version": "7.29.7",
198 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
199 + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
200 + "dev": true,
201 + "license": "MIT",
202 + "dependencies": {
203 + "@babel/template": "^7.29.7",
204 + "@babel/types": "^7.29.7"
205 + },
206 + "engines": {
207 + "node": ">=6.9.0"
208 + }
209 + },
210 + "node_modules/@babel/parser": {
211 + "version": "7.29.8",
212 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
213 + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
214 + "dev": true,
215 + "license": "MIT",
216 + "dependencies": {
217 + "@babel/types": "^7.29.8"
218 + },
219 + "bin": {
220 + "parser": "bin/babel-parser.js"
221 + },
222 + "engines": {
223 + "node": ">=6.0.0"
224 + }
225 + },
226 + "node_modules/@babel/template": {
227 + "version": "7.29.7",
228 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
229 + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
230 + "dev": true,
231 + "license": "MIT",
232 + "dependencies": {
233 + "@babel/code-frame": "^7.29.7",
234 + "@babel/parser": "^7.29.7",
235 + "@babel/types": "^7.29.7"
236 + },
237 + "engines": {
238 + "node": ">=6.9.0"
239 + }
240 + },
241 + "node_modules/@babel/traverse": {
242 + "version": "7.29.8",
243 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
244 + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
245 + "dev": true,
246 + "license": "MIT",
247 + "dependencies": {
248 + "@babel/code-frame": "^7.29.7",
249 + "@babel/generator": "^7.29.8",
250 + "@babel/helper-globals": "^7.29.7",
251 + "@babel/parser": "^7.29.8",
252 + "@babel/template": "^7.29.7",
253 + "@babel/types": "^7.29.8",
254 + "debug": "^4.3.1"
255 + },
256 + "engines": {
257 + "node": ">=6.9.0"
258 + }
259 + },
260 + "node_modules/@babel/types": {
261 + "version": "7.29.8",
262 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
263 + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
264 + "dev": true,
265 + "license": "MIT",
266 + "dependencies": {
267 + "@babel/helper-string-parser": "^7.29.7",
268 + "@babel/helper-validator-identifier": "^7.29.7"
269 + },
270 + "engines": {
271 + "node": ">=6.9.0"
272 + }
273 + },
274 + "node_modules/@emnapi/core": {
275 + "version": "1.10.0",
276 + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
277 + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
278 + "license": "MIT",
279 + "optional": true,
280 + "dependencies": {
281 + "@emnapi/wasi-threads": "1.2.1",
282 + "tslib": "^2.4.0"
283 + }
284 + },
285 + "node_modules/@emnapi/runtime": {
286 + "version": "1.11.3",
287 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
288 + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
289 + "license": "MIT",
290 + "optional": true,
291 + "dependencies": {
292 + "tslib": "^2.4.0"
293 + }
294 + },
295 + "node_modules/@emnapi/wasi-threads": {
296 + "version": "1.2.1",
297 + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
298 + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
299 + "license": "MIT",
300 + "optional": true,
301 + "dependencies": {
302 + "tslib": "^2.4.0"
303 + }
304 + },
305 + "node_modules/@eslint-community/eslint-utils": {
306 + "version": "4.10.1",
307 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
308 + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
309 + "dev": true,
310 + "license": "MIT",
311 + "dependencies": {
312 + "eslint-visitor-keys": "^3.4.3"
313 + },
314 + "engines": {
315 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
316 + },
317 + "funding": {
318 + "url": "https://opencollective.com/eslint"
319 + },
320 + "peerDependencies": {
321 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
322 + }
323 + },
324 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
325 + "version": "3.4.3",
326 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
327 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
328 + "dev": true,
329 + "license": "Apache-2.0",
330 + "engines": {
331 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
332 + },
333 + "funding": {
334 + "url": "https://opencollective.com/eslint"
335 + }
336 + },
337 + "node_modules/@eslint-community/regexpp": {
338 + "version": "4.12.2",
339 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
340 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
341 + "dev": true,
342 + "license": "MIT",
343 + "engines": {
344 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
345 + }
346 + },
347 + "node_modules/@eslint/config-array": {
348 + "version": "0.21.2",
349 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
350 + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
351 + "dev": true,
352 + "license": "Apache-2.0",
353 + "dependencies": {
354 + "@eslint/object-schema": "^2.1.7",
355 + "debug": "^4.3.1",
356 + "minimatch": "^3.1.5"
357 + },
358 + "engines": {
359 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
360 + }
361 + },
362 + "node_modules/@eslint/config-helpers": {
363 + "version": "0.4.2",
364 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
365 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
366 + "dev": true,
367 + "license": "Apache-2.0",
368 + "dependencies": {
369 + "@eslint/core": "^0.17.0"
370 + },
371 + "engines": {
372 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
373 + }
374 + },
375 + "node_modules/@eslint/core": {
376 + "version": "0.17.0",
377 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
378 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
379 + "dev": true,
380 + "license": "Apache-2.0",
381 + "dependencies": {
382 + "@types/json-schema": "^7.0.15"
383 + },
384 + "engines": {
385 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
386 + }
387 + },
388 + "node_modules/@eslint/eslintrc": {
389 + "version": "3.3.6",
390 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
391 + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
392 + "dev": true,
393 + "license": "MIT",
394 + "dependencies": {
395 + "ajv": "^6.14.0",
396 + "debug": "^4.3.2",
397 + "espree": "^10.0.1",
398 + "globals": "^14.0.0",
399 + "ignore": "^5.2.0",
400 + "import-fresh": "^3.2.1",
401 + "js-yaml": "^4.3.0",
402 + "minimatch": "^3.1.5",
403 + "strip-json-comments": "^3.1.1"
404 + },
405 + "engines": {
406 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
407 + },
408 + "funding": {
409 + "url": "https://opencollective.com/eslint"
410 + }
411 + },
412 + "node_modules/@eslint/js": {
413 + "version": "9.39.5",
414 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz",
415 + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==",
416 + "dev": true,
417 + "license": "MIT",
418 + "engines": {
419 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
420 + },
421 + "funding": {
422 + "url": "https://eslint.org/donate"
423 + }
424 + },
425 + "node_modules/@eslint/object-schema": {
426 + "version": "2.1.7",
427 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
428 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
429 + "dev": true,
430 + "license": "Apache-2.0",
431 + "engines": {
432 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
433 + }
434 + },
435 + "node_modules/@eslint/plugin-kit": {
436 + "version": "0.4.1",
437 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
438 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
439 + "dev": true,
440 + "license": "Apache-2.0",
441 + "dependencies": {
442 + "@eslint/core": "^0.17.0",
443 + "levn": "^0.4.1"
444 + },
445 + "engines": {
446 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
447 + }
448 + },
449 + "node_modules/@humanfs/core": {
450 + "version": "0.19.2",
451 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
452 + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
453 + "dev": true,
454 + "license": "Apache-2.0",
455 + "dependencies": {
456 + "@humanfs/types": "^0.15.0"
457 + },
458 + "engines": {
459 + "node": ">=18.18.0"
460 + }
461 + },
462 + "node_modules/@humanfs/node": {
463 + "version": "0.16.8",
464 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
465 + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
466 + "dev": true,
467 + "license": "Apache-2.0",
468 + "dependencies": {
469 + "@humanfs/core": "^0.19.2",
470 + "@humanfs/types": "^0.15.0",
471 + "@humanwhocodes/retry": "^0.4.0"
472 + },
473 + "engines": {
474 + "node": ">=18.18.0"
475 + }
476 + },
477 + "node_modules/@humanfs/types": {
478 + "version": "0.15.0",
479 + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
480 + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
481 + "dev": true,
482 + "license": "Apache-2.0",
483 + "engines": {
484 + "node": ">=18.18.0"
485 + }
486 + },
487 + "node_modules/@humanwhocodes/module-importer": {
488 + "version": "1.0.1",
489 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
490 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
491 + "dev": true,
492 + "license": "Apache-2.0",
493 + "engines": {
494 + "node": ">=12.22"
495 + },
496 + "funding": {
497 + "type": "github",
498 + "url": "https://github.com/sponsors/nzakas"
499 + }
500 + },
501 + "node_modules/@humanwhocodes/retry": {
502 + "version": "0.4.3",
503 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
504 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
505 + "dev": true,
506 + "license": "Apache-2.0",
507 + "engines": {
508 + "node": ">=18.18"
509 + },
510 + "funding": {
511 + "type": "github",
512 + "url": "https://github.com/sponsors/nzakas"
513 + }
514 + },
515 + "node_modules/@img/colour": {
516 + "version": "1.1.0",
517 + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
518 + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
519 + "license": "MIT",
520 + "optional": true,
521 + "engines": {
522 + "node": ">=18"
523 + }
524 + },
525 + "node_modules/@img/sharp-darwin-arm64": {
526 + "version": "0.35.3",
527 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
528 + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
529 + "cpu": [
530 + "arm64"
531 + ],
532 + "license": "Apache-2.0",
533 + "optional": true,
534 + "os": [
535 + "darwin"
536 + ],
537 + "engines": {
538 + "node": ">=20.9.0"
539 + },
540 + "funding": {
541 + "url": "https://opencollective.com/libvips"
542 + },
543 + "optionalDependencies": {
544 + "@img/sharp-libvips-darwin-arm64": "1.3.2"
545 + }
546 + },
547 + "node_modules/@img/sharp-darwin-x64": {
548 + "version": "0.35.3",
549 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
550 + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
551 + "cpu": [
552 + "x64"
553 + ],
554 + "license": "Apache-2.0",
555 + "optional": true,
556 + "os": [
557 + "darwin"
558 + ],
559 + "engines": {
560 + "node": ">=20.9.0"
561 + },
562 + "funding": {
563 + "url": "https://opencollective.com/libvips"
564 + },
565 + "optionalDependencies": {
566 + "@img/sharp-libvips-darwin-x64": "1.3.2"
567 + }
568 + },
569 + "node_modules/@img/sharp-freebsd-wasm32": {
570 + "version": "0.35.3",
571 + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
572 + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
573 + "license": "Apache-2.0",
574 + "optional": true,
575 + "os": [
576 + "freebsd"
577 + ],
578 + "dependencies": {
579 + "@img/sharp-wasm32": "0.35.3"
580 + },
581 + "engines": {
582 + "node": ">=20.9.0"
583 + },
584 + "funding": {
585 + "url": "https://opencollective.com/libvips"
586 + }
587 + },
588 + "node_modules/@img/sharp-libvips-darwin-arm64": {
589 + "version": "1.3.2",
590 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
591 + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
592 + "cpu": [
593 + "arm64"
594 + ],
595 + "license": "LGPL-3.0-or-later",
596 + "optional": true,
597 + "os": [
598 + "darwin"
599 + ],
600 + "funding": {
601 + "url": "https://opencollective.com/libvips"
602 + }
603 + },
604 + "node_modules/@img/sharp-libvips-darwin-x64": {
605 + "version": "1.3.2",
606 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
607 + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
608 + "cpu": [
609 + "x64"
610 + ],
611 + "license": "LGPL-3.0-or-later",
612 + "optional": true,
613 + "os": [
614 + "darwin"
615 + ],
616 + "funding": {
617 + "url": "https://opencollective.com/libvips"
618 + }
619 + },
620 + "node_modules/@img/sharp-libvips-linux-arm": {
621 + "version": "1.3.2",
622 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
623 + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
624 + "cpu": [
625 + "arm"
626 + ],
627 + "libc": [
628 + "glibc"
629 + ],
630 + "license": "LGPL-3.0-or-later",
631 + "optional": true,
632 + "os": [
633 + "linux"
634 + ],
635 + "funding": {
636 + "url": "https://opencollective.com/libvips"
637 + }
638 + },
639 + "node_modules/@img/sharp-libvips-linux-arm64": {
640 + "version": "1.3.2",
641 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
642 + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
643 + "cpu": [
644 + "arm64"
645 + ],
646 + "libc": [
647 + "glibc"
648 + ],
649 + "license": "LGPL-3.0-or-later",
650 + "optional": true,
651 + "os": [
652 + "linux"
653 + ],
654 + "funding": {
655 + "url": "https://opencollective.com/libvips"
656 + }
657 + },
658 + "node_modules/@img/sharp-libvips-linux-ppc64": {
659 + "version": "1.3.2",
660 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
661 + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
662 + "cpu": [
663 + "ppc64"
664 + ],
665 + "libc": [
666 + "glibc"
667 + ],
668 + "license": "LGPL-3.0-or-later",
669 + "optional": true,
670 + "os": [
671 + "linux"
672 + ],
673 + "funding": {
674 + "url": "https://opencollective.com/libvips"
675 + }
676 + },
677 + "node_modules/@img/sharp-libvips-linux-riscv64": {
678 + "version": "1.3.2",
679 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
680 + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
681 + "cpu": [
682 + "riscv64"
683 + ],
684 + "libc": [
685 + "glibc"
686 + ],
687 + "license": "LGPL-3.0-or-later",
688 + "optional": true,
689 + "os": [
690 + "linux"
691 + ],
692 + "funding": {
693 + "url": "https://opencollective.com/libvips"
694 + }
695 + },
696 + "node_modules/@img/sharp-libvips-linux-s390x": {
697 + "version": "1.3.2",
698 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
699 + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
700 + "cpu": [
701 + "s390x"
702 + ],
703 + "libc": [
704 + "glibc"
705 + ],
706 + "license": "LGPL-3.0-or-later",
707 + "optional": true,
708 + "os": [
709 + "linux"
710 + ],
711 + "funding": {
712 + "url": "https://opencollective.com/libvips"
713 + }
714 + },
715 + "node_modules/@img/sharp-libvips-linux-x64": {
716 + "version": "1.3.2",
717 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
718 + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
719 + "cpu": [
720 + "x64"
721 + ],
722 + "libc": [
723 + "glibc"
724 + ],
725 + "license": "LGPL-3.0-or-later",
726 + "optional": true,
727 + "os": [
728 + "linux"
729 + ],
730 + "funding": {
731 + "url": "https://opencollective.com/libvips"
732 + }
733 + },
734 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
735 + "version": "1.3.2",
736 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
737 + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
738 + "cpu": [
739 + "arm64"
740 + ],
741 + "libc": [
742 + "musl"
743 + ],
744 + "license": "LGPL-3.0-or-later",
745 + "optional": true,
746 + "os": [
747 + "linux"
748 + ],
749 + "funding": {
750 + "url": "https://opencollective.com/libvips"
751 + }
752 + },
753 + "node_modules/@img/sharp-libvips-linuxmusl-x64": {
754 + "version": "1.3.2",
755 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
756 + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
757 + "cpu": [
758 + "x64"
759 + ],
760 + "libc": [
761 + "musl"
762 + ],
763 + "license": "LGPL-3.0-or-later",
764 + "optional": true,
765 + "os": [
766 + "linux"
767 + ],
768 + "funding": {
769 + "url": "https://opencollective.com/libvips"
770 + }
771 + },
772 + "node_modules/@img/sharp-linux-arm": {
773 + "version": "0.35.3",
774 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
775 + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
776 + "cpu": [
777 + "arm"
778 + ],
779 + "libc": [
780 + "glibc"
781 + ],
782 + "license": "Apache-2.0",
783 + "optional": true,
784 + "os": [
785 + "linux"
786 + ],
787 + "engines": {
788 + "node": ">=20.9.0"
789 + },
790 + "funding": {
791 + "url": "https://opencollective.com/libvips"
792 + },
793 + "optionalDependencies": {
794 + "@img/sharp-libvips-linux-arm": "1.3.2"
795 + }
796 + },
797 + "node_modules/@img/sharp-linux-arm64": {
798 + "version": "0.35.3",
799 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
800 + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
801 + "cpu": [
802 + "arm64"
803 + ],
804 + "libc": [
805 + "glibc"
806 + ],
807 + "license": "Apache-2.0",
808 + "optional": true,
809 + "os": [
810 + "linux"
811 + ],
812 + "engines": {
813 + "node": ">=20.9.0"
814 + },
815 + "funding": {
816 + "url": "https://opencollective.com/libvips"
817 + },
818 + "optionalDependencies": {
819 + "@img/sharp-libvips-linux-arm64": "1.3.2"
820 + }
821 + },
822 + "node_modules/@img/sharp-linux-ppc64": {
823 + "version": "0.35.3",
824 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
825 + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
826 + "cpu": [
827 + "ppc64"
828 + ],
829 + "libc": [
830 + "glibc"
831 + ],
832 + "license": "Apache-2.0",
833 + "optional": true,
834 + "os": [
835 + "linux"
836 + ],
837 + "engines": {
838 + "node": ">=20.9.0"
839 + },
840 + "funding": {
841 + "url": "https://opencollective.com/libvips"
842 + },
843 + "optionalDependencies": {
844 + "@img/sharp-libvips-linux-ppc64": "1.3.2"
845 + }
846 + },
847 + "node_modules/@img/sharp-linux-riscv64": {
848 + "version": "0.35.3",
849 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
850 + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
851 + "cpu": [
852 + "riscv64"
853 + ],
854 + "libc": [
855 + "glibc"
856 + ],
857 + "license": "Apache-2.0",
858 + "optional": true,
859 + "os": [
860 + "linux"
861 + ],
862 + "engines": {
863 + "node": ">=20.9.0"
864 + },
865 + "funding": {
866 + "url": "https://opencollective.com/libvips"
867 + },
868 + "optionalDependencies": {
869 + "@img/sharp-libvips-linux-riscv64": "1.3.2"
870 + }
871 + },
872 + "node_modules/@img/sharp-linux-s390x": {
873 + "version": "0.35.3",
874 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
875 + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
876 + "cpu": [
877 + "s390x"
878 + ],
879 + "libc": [
880 + "glibc"
881 + ],
882 + "license": "Apache-2.0",
883 + "optional": true,
884 + "os": [
885 + "linux"
886 + ],
887 + "engines": {
888 + "node": ">=20.9.0"
889 + },
890 + "funding": {
891 + "url": "https://opencollective.com/libvips"
892 + },
893 + "optionalDependencies": {
894 + "@img/sharp-libvips-linux-s390x": "1.3.2"
895 + }
896 + },
897 + "node_modules/@img/sharp-linux-x64": {
898 + "version": "0.35.3",
899 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
900 + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
901 + "cpu": [
902 + "x64"
903 + ],
904 + "libc": [
905 + "glibc"
906 + ],
907 + "license": "Apache-2.0",
908 + "optional": true,
909 + "os": [
910 + "linux"
911 + ],
912 + "engines": {
913 + "node": ">=20.9.0"
914 + },
915 + "funding": {
916 + "url": "https://opencollective.com/libvips"
917 + },
918 + "optionalDependencies": {
919 + "@img/sharp-libvips-linux-x64": "1.3.2"
920 + }
921 + },
922 + "node_modules/@img/sharp-linuxmusl-arm64": {
923 + "version": "0.35.3",
924 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
925 + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
926 + "cpu": [
927 + "arm64"
928 + ],
929 + "libc": [
930 + "musl"
931 + ],
932 + "license": "Apache-2.0",
933 + "optional": true,
934 + "os": [
935 + "linux"
936 + ],
937 + "engines": {
938 + "node": ">=20.9.0"
939 + },
940 + "funding": {
941 + "url": "https://opencollective.com/libvips"
942 + },
943 + "optionalDependencies": {
944 + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
945 + }
946 + },
947 + "node_modules/@img/sharp-linuxmusl-x64": {
948 + "version": "0.35.3",
949 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
950 + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
951 + "cpu": [
952 + "x64"
953 + ],
954 + "libc": [
955 + "musl"
956 + ],
957 + "license": "Apache-2.0",
958 + "optional": true,
959 + "os": [
960 + "linux"
961 + ],
962 + "engines": {
963 + "node": ">=20.9.0"
964 + },
965 + "funding": {
966 + "url": "https://opencollective.com/libvips"
967 + },
968 + "optionalDependencies": {
969 + "@img/sharp-libvips-linuxmusl-x64": "1.3.2"
970 + }
971 + },
972 + "node_modules/@img/sharp-wasm32": {
973 + "version": "0.35.3",
974 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
975 + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
976 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
977 + "optional": true,
978 + "dependencies": {
979 + "@emnapi/runtime": "^1.11.1"
980 + },
981 + "engines": {
982 + "node": ">=20.9.0"
983 + },
984 + "funding": {
985 + "url": "https://opencollective.com/libvips"
986 + }
987 + },
988 + "node_modules/@img/sharp-webcontainers-wasm32": {
989 + "version": "0.35.3",
990 + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
991 + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
992 + "cpu": [
993 + "wasm32"
994 + ],
995 + "license": "Apache-2.0",
996 + "optional": true,
997 + "dependencies": {
998 + "@img/sharp-wasm32": "0.35.3"
999 + },
1000 + "engines": {
1001 + "node": ">=20.9.0"
1002 + },
1003 + "funding": {
1004 + "url": "https://opencollective.com/libvips"
1005 + }
1006 + },
1007 + "node_modules/@img/sharp-win32-arm64": {
1008 + "version": "0.35.3",
1009 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
1010 + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
1011 + "cpu": [
1012 + "arm64"
1013 + ],
1014 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1015 + "optional": true,
1016 + "os": [
1017 + "win32"
1018 + ],
1019 + "engines": {
1020 + "node": ">=20.9.0"
1021 + },
1022 + "funding": {
1023 + "url": "https://opencollective.com/libvips"
1024 + }
1025 + },
1026 + "node_modules/@img/sharp-win32-ia32": {
1027 + "version": "0.35.3",
1028 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
1029 + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
1030 + "cpu": [
1031 + "ia32"
1032 + ],
1033 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1034 + "optional": true,
1035 + "os": [
1036 + "win32"
1037 + ],
1038 + "engines": {
1039 + "node": "^20.9.0"
1040 + },
1041 + "funding": {
1042 + "url": "https://opencollective.com/libvips"
1043 + }
1044 + },
1045 + "node_modules/@img/sharp-win32-x64": {
1046 + "version": "0.35.3",
1047 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
1048 + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
1049 + "cpu": [
1050 + "x64"
1051 + ],
1052 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1053 + "optional": true,
1054 + "os": [
1055 + "win32"
1056 + ],
1057 + "engines": {
1058 + "node": ">=20.9.0"
1059 + },
1060 + "funding": {
1061 + "url": "https://opencollective.com/libvips"
1062 + }
1063 + },
1064 + "node_modules/@jridgewell/gen-mapping": {
1065 + "version": "0.3.13",
1066 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
1067 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
1068 + "dev": true,
1069 + "license": "MIT",
1070 + "dependencies": {
1071 + "@jridgewell/sourcemap-codec": "^1.5.0",
1072 + "@jridgewell/trace-mapping": "^0.3.24"
1073 + }
1074 + },
1075 + "node_modules/@jridgewell/remapping": {
1076 + "version": "2.3.5",
1077 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
1078 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
1079 + "dev": true,
1080 + "license": "MIT",
1081 + "dependencies": {
1082 + "@jridgewell/gen-mapping": "^0.3.5",
1083 + "@jridgewell/trace-mapping": "^0.3.24"
1084 + }
1085 + },
1086 + "node_modules/@jridgewell/resolve-uri": {
1087 + "version": "3.1.2",
1088 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
1089 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
1090 + "dev": true,
1091 + "license": "MIT",
1092 + "engines": {
1093 + "node": ">=6.0.0"
1094 + }
1095 + },
1096 + "node_modules/@jridgewell/sourcemap-codec": {
1097 + "version": "1.5.5",
1098 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
1099 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
1100 + "dev": true,
1101 + "license": "MIT"
1102 + },
1103 + "node_modules/@jridgewell/trace-mapping": {
1104 + "version": "0.3.31",
1105 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
1106 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
1107 + "dev": true,
1108 + "license": "MIT",
1109 + "dependencies": {
1110 + "@jridgewell/resolve-uri": "^3.1.0",
1111 + "@jridgewell/sourcemap-codec": "^1.4.14"
1112 + }
1113 + },
1114 + "node_modules/@napi-rs/wasm-runtime": {
1115 + "version": "1.2.3",
1116 + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
1117 + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
1118 + "dev": true,
1119 + "license": "MIT",
1120 + "optional": true,
1121 + "dependencies": {
1122 + "@tybys/wasm-util": "^0.10.3"
1123 + },
1124 + "engines": {
1125 + "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
1126 + },
1127 + "funding": {
1128 + "type": "github",
1129 + "url": "https://github.com/sponsors/Brooooooklyn"
1130 + },
1131 + "peerDependencies": {
1132 + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
1133 + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
1134 + }
1135 + },
1136 + "node_modules/@next/env": {
1137 + "version": "16.3.0",
1138 + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.0.tgz",
1139 + "integrity": "sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==",
1140 + "license": "MIT"
1141 + },
1142 + "node_modules/@next/eslint-plugin-next": {
1143 + "version": "16.3.0",
1144 + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.0.tgz",
1145 + "integrity": "sha512-OqgJ8PN0d04KcPhDX/PTY5tJUJZxlbrt7O7FBsm4XE0XW2JDrKnDXsc9uo9WUimJGPoo2j+JRGhyXApC//mvbw==",
1146 + "dev": true,
1147 + "license": "MIT",
1148 + "dependencies": {
1149 + "@eslint-community/eslint-utils": "4.9.1",
1150 + "fast-glob": "3.3.1"
1151 + }
1152 + },
1153 + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": {
1154 + "version": "4.9.1",
1155 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
1156 + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
1157 + "dev": true,
1158 + "license": "MIT",
1159 + "dependencies": {
1160 + "eslint-visitor-keys": "^3.4.3"
1161 + },
1162 + "engines": {
1163 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1164 + },
1165 + "funding": {
1166 + "url": "https://opencollective.com/eslint"
1167 + },
1168 + "peerDependencies": {
1169 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
1170 + }
1171 + },
1172 + "node_modules/@next/eslint-plugin-next/node_modules/eslint-visitor-keys": {
1173 + "version": "3.4.3",
1174 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
1175 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
1176 + "dev": true,
1177 + "license": "Apache-2.0",
1178 + "engines": {
1179 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1180 + },
1181 + "funding": {
1182 + "url": "https://opencollective.com/eslint"
1183 + }
1184 + },
1185 + "node_modules/@next/swc-darwin-arm64": {
1186 + "version": "16.3.0",
1187 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.0.tgz",
1188 + "integrity": "sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==",
1189 + "cpu": [
1190 + "arm64"
1191 + ],
1192 + "license": "MIT",
1193 + "optional": true,
1194 + "os": [
1195 + "darwin"
1196 + ],
1197 + "engines": {
1198 + "node": ">= 10"
1199 + }
1200 + },
1201 + "node_modules/@next/swc-darwin-x64": {
1202 + "version": "16.3.0",
1203 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.0.tgz",
1204 + "integrity": "sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==",
1205 + "cpu": [
1206 + "x64"
1207 + ],
1208 + "license": "MIT",
1209 + "optional": true,
1210 + "os": [
1211 + "darwin"
1212 + ],
1213 + "engines": {
1214 + "node": ">= 10"
1215 + }
1216 + },
1217 + "node_modules/@next/swc-linux-arm64-gnu": {
1218 + "version": "16.3.0",
1219 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.0.tgz",
1220 + "integrity": "sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==",
1221 + "cpu": [
1222 + "arm64"
1223 + ],
1224 + "libc": [
1225 + "glibc"
1226 + ],
1227 + "license": "MIT",
1228 + "optional": true,
1229 + "os": [
1230 + "linux"
1231 + ],
1232 + "engines": {
1233 + "node": ">= 10"
1234 + }
1235 + },
1236 + "node_modules/@next/swc-linux-arm64-musl": {
1237 + "version": "16.3.0",
1238 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.0.tgz",
1239 + "integrity": "sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==",
1240 + "cpu": [
1241 + "arm64"
1242 + ],
1243 + "libc": [
1244 + "musl"
1245 + ],
1246 + "license": "MIT",
1247 + "optional": true,
1248 + "os": [
1249 + "linux"
1250 + ],
1251 + "engines": {
1252 + "node": ">= 10"
1253 + }
1254 + },
1255 + "node_modules/@next/swc-linux-x64-gnu": {
1256 + "version": "16.3.0",
1257 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.0.tgz",
1258 + "integrity": "sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==",
1259 + "cpu": [
1260 + "x64"
1261 + ],
1262 + "libc": [
1263 + "glibc"
1264 + ],
1265 + "license": "MIT",
1266 + "optional": true,
1267 + "os": [
1268 + "linux"
1269 + ],
1270 + "engines": {
1271 + "node": ">= 10"
1272 + }
1273 + },
1274 + "node_modules/@next/swc-linux-x64-musl": {
1275 + "version": "16.3.0",
1276 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.0.tgz",
1277 + "integrity": "sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==",
1278 + "cpu": [
1279 + "x64"
1280 + ],
1281 + "libc": [
1282 + "musl"
1283 + ],
1284 + "license": "MIT",
1285 + "optional": true,
1286 + "os": [
1287 + "linux"
1288 + ],
1289 + "engines": {
1290 + "node": ">= 10"
1291 + }
1292 + },
1293 + "node_modules/@next/swc-win32-arm64-msvc": {
1294 + "version": "16.3.0",
1295 + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.0.tgz",
1296 + "integrity": "sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==",
1297 + "cpu": [
1298 + "arm64"
1299 + ],
1300 + "license": "MIT",
1301 + "optional": true,
1302 + "os": [
1303 + "win32"
1304 + ],
1305 + "engines": {
1306 + "node": ">= 10"
1307 + }
1308 + },
1309 + "node_modules/@next/swc-win32-x64-msvc": {
1310 + "version": "16.3.0",
1311 + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.0.tgz",
1312 + "integrity": "sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==",
1313 + "cpu": [
1314 + "x64"
1315 + ],
1316 + "license": "MIT",
1317 + "optional": true,
1318 + "os": [
1319 + "win32"
1320 + ],
1321 + "engines": {
1322 + "node": ">= 10"
1323 + }
1324 + },
1325 + "node_modules/@node-rs/argon2": {
1326 + "version": "2.0.2",
1327 + "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.0.2.tgz",
1328 + "integrity": "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==",
1329 + "license": "MIT",
1330 + "engines": {
1331 + "node": ">= 10"
1332 + },
1333 + "optionalDependencies": {
1334 + "@node-rs/argon2-android-arm-eabi": "2.0.2",
1335 + "@node-rs/argon2-android-arm64": "2.0.2",
1336 + "@node-rs/argon2-darwin-arm64": "2.0.2",
1337 + "@node-rs/argon2-darwin-x64": "2.0.2",
1338 + "@node-rs/argon2-freebsd-x64": "2.0.2",
1339 + "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2",
1340 + "@node-rs/argon2-linux-arm64-gnu": "2.0.2",
1341 + "@node-rs/argon2-linux-arm64-musl": "2.0.2",
1342 + "@node-rs/argon2-linux-x64-gnu": "2.0.2",
1343 + "@node-rs/argon2-linux-x64-musl": "2.0.2",
1344 + "@node-rs/argon2-wasm32-wasi": "2.0.2",
1345 + "@node-rs/argon2-win32-arm64-msvc": "2.0.2",
1346 + "@node-rs/argon2-win32-ia32-msvc": "2.0.2",
1347 + "@node-rs/argon2-win32-x64-msvc": "2.0.2"
1348 + }
1349 + },
1350 + "node_modules/@node-rs/argon2-android-arm-eabi": {
1351 + "version": "2.0.2",
1352 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz",
1353 + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==",
1354 + "cpu": [
1355 + "arm"
1356 + ],
1357 + "license": "MIT",
1358 + "optional": true,
1359 + "os": [
1360 + "android"
1361 + ],
1362 + "engines": {
1363 + "node": ">= 10"
1364 + }
1365 + },
1366 + "node_modules/@node-rs/argon2-android-arm64": {
1367 + "version": "2.0.2",
1368 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz",
1369 + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==",
1370 + "cpu": [
1371 + "arm64"
1372 + ],
1373 + "license": "MIT",
1374 + "optional": true,
1375 + "os": [
1376 + "android"
1377 + ],
1378 + "engines": {
1379 + "node": ">= 10"
1380 + }
1381 + },
1382 + "node_modules/@node-rs/argon2-darwin-arm64": {
1383 + "version": "2.0.2",
1384 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz",
1385 + "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==",
1386 + "cpu": [
1387 + "arm64"
1388 + ],
1389 + "license": "MIT",
1390 + "optional": true,
1391 + "os": [
1392 + "darwin"
1393 + ],
1394 + "engines": {
1395 + "node": ">= 10"
1396 + }
1397 + },
1398 + "node_modules/@node-rs/argon2-darwin-x64": {
1399 + "version": "2.0.2",
1400 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz",
1401 + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==",
1402 + "cpu": [
1403 + "x64"
1404 + ],
1405 + "license": "MIT",
1406 + "optional": true,
1407 + "os": [
1408 + "darwin"
1409 + ],
1410 + "engines": {
1411 + "node": ">= 10"
1412 + }
1413 + },
1414 + "node_modules/@node-rs/argon2-freebsd-x64": {
1415 + "version": "2.0.2",
1416 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz",
1417 + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==",
1418 + "cpu": [
1419 + "x64"
1420 + ],
1421 + "license": "MIT",
1422 + "optional": true,
1423 + "os": [
1424 + "freebsd"
1425 + ],
1426 + "engines": {
1427 + "node": ">= 10"
1428 + }
1429 + },
1430 + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": {
1431 + "version": "2.0.2",
1432 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz",
1433 + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==",
1434 + "cpu": [
1435 + "arm"
1436 + ],
1437 + "license": "MIT",
1438 + "optional": true,
1439 + "os": [
1440 + "linux"
1441 + ],
1442 + "engines": {
1443 + "node": ">= 10"
1444 + }
1445 + },
1446 + "node_modules/@node-rs/argon2-linux-arm64-gnu": {
1447 + "version": "2.0.2",
1448 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz",
1449 + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==",
1450 + "cpu": [
1451 + "arm64"
1452 + ],
1453 + "libc": [
1454 + "glibc"
1455 + ],
1456 + "license": "MIT",
1457 + "optional": true,
1458 + "os": [
1459 + "linux"
1460 + ],
1461 + "engines": {
1462 + "node": ">= 10"
1463 + }
1464 + },
1465 + "node_modules/@node-rs/argon2-linux-arm64-musl": {
1466 + "version": "2.0.2",
1467 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz",
1468 + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==",
1469 + "cpu": [
1470 + "arm64"
1471 + ],
1472 + "libc": [
1473 + "musl"
1474 + ],
1475 + "license": "MIT",
1476 + "optional": true,
1477 + "os": [
1478 + "linux"
1479 + ],
1480 + "engines": {
1481 + "node": ">= 10"
1482 + }
1483 + },
1484 + "node_modules/@node-rs/argon2-linux-x64-gnu": {
1485 + "version": "2.0.2",
1486 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz",
1487 + "integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==",
1488 + "cpu": [
1489 + "x64"
1490 + ],
1491 + "libc": [
1492 + "glibc"
1493 + ],
1494 + "license": "MIT",
1495 + "optional": true,
1496 + "os": [
1497 + "linux"
1498 + ],
1499 + "engines": {
1500 + "node": ">= 10"
1501 + }
1502 + },
1503 + "node_modules/@node-rs/argon2-linux-x64-musl": {
1504 + "version": "2.0.2",
1505 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz",
1506 + "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==",
1507 + "cpu": [
1508 + "x64"
1509 + ],
1510 + "libc": [
1511 + "musl"
1512 + ],
1513 + "license": "MIT",
1514 + "optional": true,
1515 + "os": [
1516 + "linux"
1517 + ],
1518 + "engines": {
1519 + "node": ">= 10"
1520 + }
1521 + },
1522 + "node_modules/@node-rs/argon2-wasm32-wasi": {
1523 + "version": "2.0.2",
1524 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz",
1525 + "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==",
1526 + "cpu": [
1527 + "wasm32"
1528 + ],
1529 + "license": "MIT",
1530 + "optional": true,
1531 + "dependencies": {
1532 + "@napi-rs/wasm-runtime": "^0.2.5"
1533 + },
1534 + "engines": {
1535 + "node": ">=14.0.0"
1536 + }
1537 + },
1538 + "node_modules/@node-rs/argon2-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
1539 + "version": "0.2.12",
1540 + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
1541 + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
1542 + "license": "MIT",
1543 + "optional": true,
1544 + "dependencies": {
1545 + "@emnapi/core": "^1.4.3",
1546 + "@emnapi/runtime": "^1.4.3",
1547 + "@tybys/wasm-util": "^0.10.0"
1548 + }
1549 + },
1550 + "node_modules/@node-rs/argon2-win32-arm64-msvc": {
1551 + "version": "2.0.2",
1552 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz",
1553 + "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==",
1554 + "cpu": [
1555 + "arm64"
1556 + ],
1557 + "license": "MIT",
1558 + "optional": true,
1559 + "os": [
1560 + "win32"
1561 + ],
1562 + "engines": {
1563 + "node": ">= 10"
1564 + }
1565 + },
1566 + "node_modules/@node-rs/argon2-win32-ia32-msvc": {
1567 + "version": "2.0.2",
1568 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz",
1569 + "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==",
1570 + "cpu": [
1571 + "ia32"
1572 + ],
1573 + "license": "MIT",
1574 + "optional": true,
1575 + "os": [
1576 + "win32"
1577 + ],
1578 + "engines": {
1579 + "node": ">= 10"
1580 + }
1581 + },
1582 + "node_modules/@node-rs/argon2-win32-x64-msvc": {
1583 + "version": "2.0.2",
1584 + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz",
1585 + "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==",
1586 + "cpu": [
1587 + "x64"
1588 + ],
1589 + "license": "MIT",
1590 + "optional": true,
1591 + "os": [
1592 + "win32"
1593 + ],
1594 + "engines": {
1595 + "node": ">= 10"
1596 + }
1597 + },
1598 + "node_modules/@nodelib/fs.scandir": {
1599 + "version": "2.1.5",
1600 + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
1601 + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
1602 + "dev": true,
1603 + "license": "MIT",
1604 + "dependencies": {
1605 + "@nodelib/fs.stat": "2.0.5",
1606 + "run-parallel": "^1.1.9"
1607 + },
1608 + "engines": {
1609 + "node": ">= 8"
1610 + }
1611 + },
1612 + "node_modules/@nodelib/fs.stat": {
1613 + "version": "2.0.5",
1614 + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
1615 + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
1616 + "dev": true,
1617 + "license": "MIT",
1618 + "engines": {
1619 + "node": ">= 8"
1620 + }
1621 + },
1622 + "node_modules/@nodelib/fs.walk": {
1623 + "version": "1.2.8",
1624 + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
1625 + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
1626 + "dev": true,
1627 + "license": "MIT",
1628 + "dependencies": {
1629 + "@nodelib/fs.scandir": "2.1.5",
1630 + "fastq": "^1.6.0"
1631 + },
1632 + "engines": {
1633 + "node": ">= 8"
1634 + }
1635 + },
1636 + "node_modules/@nolyfill/is-core-module": {
1637 + "version": "1.0.39",
1638 + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
1639 + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
1640 + "dev": true,
1641 + "license": "MIT",
1642 + "engines": {
1643 + "node": ">=12.4.0"
1644 + }
1645 + },
1646 + "node_modules/@rtsao/scc": {
1647 + "version": "1.1.0",
1648 + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
1649 + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
1650 + "dev": true,
1651 + "license": "MIT"
1652 + },
1653 + "node_modules/@swc/helpers": {
1654 + "version": "0.5.15",
1655 + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
1656 + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
1657 + "license": "Apache-2.0",
1658 + "dependencies": {
1659 + "tslib": "^2.8.0"
1660 + }
1661 + },
1662 + "node_modules/@tybys/wasm-util": {
1663 + "version": "0.10.3",
1664 + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
1665 + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
1666 + "license": "MIT",
1667 + "optional": true,
1668 + "dependencies": {
1669 + "tslib": "^2.4.0"
1670 + }
1671 + },
1672 + "node_modules/@types/better-sqlite3": {
1673 + "version": "9.6.0",
1674 + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
1675 + "integrity": "sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==",
1676 + "dev": true,
1677 + "license": "MIT",
1678 + "dependencies": {
1679 + "@types/node": "*"
1680 + }
1681 + },
1682 + "node_modules/@types/debug": {
1683 + "version": "4.1.13",
1684 + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
1685 + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
1686 + "license": "MIT",
1687 + "dependencies": {
1688 + "@types/ms": "*"
1689 + }
1690 + },
1691 + "node_modules/@types/estree": {
1692 + "version": "1.0.9",
1693 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1694 + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1695 + "license": "MIT"
1696 + },
1697 + "node_modules/@types/estree-jsx": {
1698 + "version": "1.0.5",
1699 + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
1700 + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
1701 + "license": "MIT",
1702 + "dependencies": {
1703 + "@types/estree": "*"
1704 + }
1705 + },
1706 + "node_modules/@types/hast": {
1707 + "version": "3.0.5",
1708 + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
1709 + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
1710 + "license": "MIT",
1711 + "dependencies": {
1712 + "@types/unist": "*"
1713 + }
1714 + },
1715 + "node_modules/@types/json-schema": {
1716 + "version": "7.0.15",
1717 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
1718 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
1719 + "dev": true,
1720 + "license": "MIT"
1721 + },
1722 + "node_modules/@types/json5": {
1723 + "version": "0.0.29",
1724 + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
1725 + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
1726 + "dev": true,
1727 + "license": "MIT"
1728 + },
1729 + "node_modules/@types/katex": {
1730 + "version": "0.16.8",
1731 + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz",
1732 + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==",
1733 + "license": "MIT"
1734 + },
1735 + "node_modules/@types/mdast": {
1736 + "version": "4.0.4",
1737 + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1738 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1739 + "license": "MIT",
1740 + "dependencies": {
1741 + "@types/unist": "*"
1742 + }
1743 + },
1744 + "node_modules/@types/ms": {
1745 + "version": "2.1.0",
1746 + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
1747 + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1748 + "license": "MIT"
1749 + },
1750 + "node_modules/@types/node": {
1751 + "version": "20.19.43",
1752 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
1753 + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
1754 + "dev": true,
1755 + "license": "MIT",
1756 + "dependencies": {
1757 + "undici-types": "~6.21.0"
1758 + }
1759 + },
1760 + "node_modules/@types/react": {
1761 + "version": "19.2.18",
1762 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
1763 + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
1764 + "license": "MIT",
1765 + "dependencies": {
1766 + "csstype": "^3.2.2"
1767 + }
1768 + },
1769 + "node_modules/@types/react-dom": {
1770 + "version": "19.2.4",
1771 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
1772 + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
1773 + "dev": true,
1774 + "license": "MIT",
1775 + "peerDependencies": {
1776 + "@types/react": "^19.2.0"
1777 + }
1778 + },
1779 + "node_modules/@types/unist": {
1780 + "version": "3.0.3",
1781 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1782 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1783 + "license": "MIT"
1784 + },
1785 + "node_modules/@typescript-eslint/eslint-plugin": {
1786 + "version": "8.67.0",
1787 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz",
1788 + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==",
1789 + "dev": true,
1790 + "license": "MIT",
1791 + "dependencies": {
1792 + "@eslint-community/regexpp": "^4.12.2",
1793 + "@typescript-eslint/scope-manager": "8.67.0",
1794 + "@typescript-eslint/type-utils": "8.67.0",
1795 + "@typescript-eslint/utils": "8.67.0",
1796 + "@typescript-eslint/visitor-keys": "8.67.0",
1797 + "ignore": "^7.0.5",
1798 + "natural-compare": "^1.4.0",
1799 + "ts-api-utils": "^2.5.0"
1800 + },
1801 + "engines": {
1802 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1803 + },
1804 + "funding": {
1805 + "type": "opencollective",
1806 + "url": "https://opencollective.com/typescript-eslint"
1807 + },
1808 + "peerDependencies": {
1809 + "@typescript-eslint/parser": "^8.67.0",
1810 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
1811 + "typescript": ">=4.8.4 <6.1.0"
1812 + }
1813 + },
1814 + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
1815 + "version": "7.0.6",
1816 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
1817 + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
1818 + "dev": true,
1819 + "license": "MIT",
1820 + "engines": {
1821 + "node": ">= 4"
1822 + }
1823 + },
1824 + "node_modules/@typescript-eslint/parser": {
1825 + "version": "8.67.0",
1826 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz",
1827 + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
1828 + "dev": true,
1829 + "license": "MIT",
1830 + "dependencies": {
1831 + "@typescript-eslint/scope-manager": "8.67.0",
1832 + "@typescript-eslint/types": "8.67.0",
1833 + "@typescript-eslint/typescript-estree": "8.67.0",
1834 + "@typescript-eslint/visitor-keys": "8.67.0",
1835 + "debug": "^4.4.3"
1836 + },
1837 + "engines": {
1838 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1839 + },
1840 + "funding": {
1841 + "type": "opencollective",
1842 + "url": "https://opencollective.com/typescript-eslint"
1843 + },
1844 + "peerDependencies": {
1845 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
1846 + "typescript": ">=4.8.4 <6.1.0"
1847 + }
1848 + },
1849 + "node_modules/@typescript-eslint/project-service": {
1850 + "version": "8.67.0",
1851 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz",
1852 + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==",
1853 + "dev": true,
1854 + "license": "MIT",
1855 + "dependencies": {
1856 + "@typescript-eslint/tsconfig-utils": "^8.67.0",
1857 + "@typescript-eslint/types": "^8.67.0",
1858 + "debug": "^4.4.3"
1859 + },
1860 + "engines": {
1861 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1862 + },
1863 + "funding": {
1864 + "type": "opencollective",
1865 + "url": "https://opencollective.com/typescript-eslint"
1866 + },
1867 + "peerDependencies": {
1868 + "typescript": ">=4.8.4 <6.1.0"
1869 + }
1870 + },
1871 + "node_modules/@typescript-eslint/scope-manager": {
1872 + "version": "8.67.0",
1873 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz",
1874 + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==",
1875 + "dev": true,
1876 + "license": "MIT",
1877 + "dependencies": {
1878 + "@typescript-eslint/types": "8.67.0",
1879 + "@typescript-eslint/visitor-keys": "8.67.0"
1880 + },
1881 + "engines": {
1882 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1883 + },
1884 + "funding": {
1885 + "type": "opencollective",
1886 + "url": "https://opencollective.com/typescript-eslint"
1887 + }
1888 + },
1889 + "node_modules/@typescript-eslint/tsconfig-utils": {
1890 + "version": "8.67.0",
1891 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz",
1892 + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==",
1893 + "dev": true,
1894 + "license": "MIT",
1895 + "engines": {
1896 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1897 + },
1898 + "funding": {
1899 + "type": "opencollective",
1900 + "url": "https://opencollective.com/typescript-eslint"
1901 + },
1902 + "peerDependencies": {
1903 + "typescript": ">=4.8.4 <6.1.0"
1904 + }
1905 + },
1906 + "node_modules/@typescript-eslint/type-utils": {
1907 + "version": "8.67.0",
1908 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz",
1909 + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==",
1910 + "dev": true,
1911 + "license": "MIT",
1912 + "dependencies": {
1913 + "@typescript-eslint/types": "8.67.0",
1914 + "@typescript-eslint/typescript-estree": "8.67.0",
1915 + "@typescript-eslint/utils": "8.67.0",
1916 + "debug": "^4.4.3",
1917 + "ts-api-utils": "^2.5.0"
1918 + },
1919 + "engines": {
1920 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1921 + },
1922 + "funding": {
1923 + "type": "opencollective",
1924 + "url": "https://opencollective.com/typescript-eslint"
1925 + },
1926 + "peerDependencies": {
1927 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
1928 + "typescript": ">=4.8.4 <6.1.0"
1929 + }
1930 + },
1931 + "node_modules/@typescript-eslint/types": {
1932 + "version": "8.67.0",
1933 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz",
1934 + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==",
1935 + "dev": true,
1936 + "license": "MIT",
1937 + "engines": {
1938 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1939 + },
1940 + "funding": {
1941 + "type": "opencollective",
1942 + "url": "https://opencollective.com/typescript-eslint"
1943 + }
1944 + },
1945 + "node_modules/@typescript-eslint/typescript-estree": {
1946 + "version": "8.67.0",
1947 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz",
1948 + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==",
1949 + "dev": true,
1950 + "license": "MIT",
1951 + "dependencies": {
1952 + "@typescript-eslint/project-service": "8.67.0",
1953 + "@typescript-eslint/tsconfig-utils": "8.67.0",
1954 + "@typescript-eslint/types": "8.67.0",
1955 + "@typescript-eslint/visitor-keys": "8.67.0",
1956 + "debug": "^4.4.3",
1957 + "minimatch": "^10.2.2",
1958 + "semver": "^7.7.3",
1959 + "tinyglobby": "^0.2.15",
1960 + "ts-api-utils": "^2.5.0"
1961 + },
1962 + "engines": {
1963 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1964 + },
1965 + "funding": {
1966 + "type": "opencollective",
1967 + "url": "https://opencollective.com/typescript-eslint"
1968 + },
1969 + "peerDependencies": {
1970 + "typescript": ">=4.8.4 <6.1.0"
1971 + }
1972 + },
1973 + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
1974 + "version": "4.0.4",
1975 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
1976 + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
1977 + "dev": true,
1978 + "license": "MIT",
1979 + "engines": {
1980 + "node": "18 || 20 || >=22"
1981 + }
1982 + },
1983 + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
1984 + "version": "5.0.9",
1985 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
1986 + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
1987 + "dev": true,
1988 + "license": "MIT",
1989 + "dependencies": {
1990 + "balanced-match": "^4.0.2"
1991 + },
1992 + "engines": {
1993 + "node": "20 || >=22"
1994 + }
1995 + },
1996 + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
1997 + "version": "10.2.6",
1998 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
1999 + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
2000 + "dev": true,
2001 + "license": "BlueOak-1.0.0",
2002 + "dependencies": {
2003 + "brace-expansion": "^5.0.8"
2004 + },
2005 + "engines": {
2006 + "node": "18 || 20 || >=22"
2007 + },
2008 + "funding": {
2009 + "url": "https://github.com/sponsors/isaacs"
2010 + }
2011 + },
2012 + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
2013 + "version": "7.8.5",
2014 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
2015 + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
2016 + "dev": true,
2017 + "license": "ISC",
2018 + "bin": {
2019 + "semver": "bin/semver.js"
2020 + },
2021 + "engines": {
2022 + "node": ">=10"
2023 + }
2024 + },
2025 + "node_modules/@typescript-eslint/utils": {
2026 + "version": "8.67.0",
2027 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz",
2028 + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==",
2029 + "dev": true,
2030 + "license": "MIT",
2031 + "dependencies": {
2032 + "@eslint-community/eslint-utils": "^4.9.1",
2033 + "@typescript-eslint/scope-manager": "8.67.0",
2034 + "@typescript-eslint/types": "8.67.0",
2035 + "@typescript-eslint/typescript-estree": "8.67.0"
2036 + },
2037 + "engines": {
2038 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2039 + },
2040 + "funding": {
2041 + "type": "opencollective",
2042 + "url": "https://opencollective.com/typescript-eslint"
2043 + },
2044 + "peerDependencies": {
2045 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
2046 + "typescript": ">=4.8.4 <6.1.0"
2047 + }
2048 + },
2049 + "node_modules/@typescript-eslint/visitor-keys": {
2050 + "version": "8.67.0",
2051 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz",
2052 + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==",
2053 + "dev": true,
2054 + "license": "MIT",
2055 + "dependencies": {
2056 + "@typescript-eslint/types": "8.67.0",
2057 + "eslint-visitor-keys": "^5.0.0"
2058 + },
2059 + "engines": {
2060 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2061 + },
2062 + "funding": {
2063 + "type": "opencollective",
2064 + "url": "https://opencollective.com/typescript-eslint"
2065 + }
2066 + },
2067 + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
2068 + "version": "5.0.1",
2069 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
2070 + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
2071 + "dev": true,
2072 + "license": "Apache-2.0",
2073 + "engines": {
2074 + "node": "^20.19.0 || ^22.13.0 || >=24"
2075 + },
2076 + "funding": {
2077 + "url": "https://opencollective.com/eslint"
2078 + }
2079 + },
2080 + "node_modules/@ungap/structured-clone": {
2081 + "version": "1.3.3",
2082 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
2083 + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
2084 + "license": "ISC"
2085 + },
2086 + "node_modules/@unrs/resolver-binding-android-arm-eabi": {
2087 + "version": "1.12.2",
2088 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz",
2089 + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==",
2090 + "cpu": [
2091 + "arm"
2092 + ],
2093 + "dev": true,
2094 + "license": "MIT",
2095 + "optional": true,
2096 + "os": [
2097 + "android"
2098 + ]
2099 + },
2100 + "node_modules/@unrs/resolver-binding-android-arm64": {
2101 + "version": "1.12.2",
2102 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz",
2103 + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==",
2104 + "cpu": [
2105 + "arm64"
2106 + ],
2107 + "dev": true,
2108 + "license": "MIT",
2109 + "optional": true,
2110 + "os": [
2111 + "android"
2112 + ]
2113 + },
2114 + "node_modules/@unrs/resolver-binding-darwin-arm64": {
2115 + "version": "1.12.2",
2116 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz",
2117 + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==",
2118 + "cpu": [
2119 + "arm64"
2120 + ],
2121 + "dev": true,
2122 + "license": "MIT",
2123 + "optional": true,
2124 + "os": [
2125 + "darwin"
2126 + ]
2127 + },
2128 + "node_modules/@unrs/resolver-binding-darwin-x64": {
2129 + "version": "1.12.2",
2130 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz",
2131 + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==",
2132 + "cpu": [
2133 + "x64"
2134 + ],
2135 + "dev": true,
2136 + "license": "MIT",
2137 + "optional": true,
2138 + "os": [
2139 + "darwin"
2140 + ]
2141 + },
2142 + "node_modules/@unrs/resolver-binding-freebsd-x64": {
2143 + "version": "1.12.2",
2144 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz",
2145 + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==",
2146 + "cpu": [
2147 + "x64"
2148 + ],
2149 + "dev": true,
2150 + "license": "MIT",
2151 + "optional": true,
2152 + "os": [
2153 + "freebsd"
2154 + ]
2155 + },
2156 + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
2157 + "version": "1.12.2",
2158 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz",
2159 + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==",
2160 + "cpu": [
2161 + "arm"
2162 + ],
2163 + "dev": true,
2164 + "license": "MIT",
2165 + "optional": true,
2166 + "os": [
2167 + "linux"
2168 + ]
2169 + },
2170 + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
2171 + "version": "1.12.2",
2172 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz",
2173 + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==",
2174 + "cpu": [
2175 + "arm"
2176 + ],
2177 + "dev": true,
2178 + "license": "MIT",
2179 + "optional": true,
2180 + "os": [
2181 + "linux"
2182 + ]
2183 + },
2184 + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
2185 + "version": "1.12.2",
2186 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz",
2187 + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==",
2188 + "cpu": [
2189 + "arm64"
2190 + ],
2191 + "dev": true,
2192 + "libc": [
2193 + "glibc"
2194 + ],
2195 + "license": "MIT",
2196 + "optional": true,
2197 + "os": [
2198 + "linux"
2199 + ]
2200 + },
2201 + "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
2202 + "version": "1.12.2",
2203 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz",
2204 + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==",
2205 + "cpu": [
2206 + "arm64"
2207 + ],
2208 + "dev": true,
2209 + "libc": [
2210 + "musl"
2211 + ],
2212 + "license": "MIT",
2213 + "optional": true,
2214 + "os": [
2215 + "linux"
2216 + ]
2217 + },
2218 + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": {
2219 + "version": "1.12.2",
2220 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz",
2221 + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==",
2222 + "cpu": [
2223 + "loong64"
2224 + ],
2225 + "dev": true,
2226 + "libc": [
2227 + "glibc"
2228 + ],
2229 + "license": "MIT",
2230 + "optional": true,
2231 + "os": [
2232 + "linux"
2233 + ]
2234 + },
2235 + "node_modules/@unrs/resolver-binding-linux-loong64-musl": {
2236 + "version": "1.12.2",
2237 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz",
2238 + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==",
2239 + "cpu": [
2240 + "loong64"
2241 + ],
2242 + "dev": true,
2243 + "libc": [
2244 + "musl"
2245 + ],
2246 + "license": "MIT",
2247 + "optional": true,
2248 + "os": [
2249 + "linux"
2250 + ]
2251 + },
2252 + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
2253 + "version": "1.12.2",
2254 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz",
2255 + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==",
2256 + "cpu": [
2257 + "ppc64"
2258 + ],
2259 + "dev": true,
2260 + "libc": [
2261 + "glibc"
2262 + ],
2263 + "license": "MIT",
2264 + "optional": true,
2265 + "os": [
2266 + "linux"
2267 + ]
2268 + },
2269 + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
2270 + "version": "1.12.2",
2271 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz",
2272 + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==",
2273 + "cpu": [
2274 + "riscv64"
2275 + ],
2276 + "dev": true,
2277 + "libc": [
2278 + "glibc"
2279 + ],
2280 + "license": "MIT",
2281 + "optional": true,
2282 + "os": [
2283 + "linux"
2284 + ]
2285 + },
2286 + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
2287 + "version": "1.12.2",
2288 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz",
2289 + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==",
2290 + "cpu": [
2291 + "riscv64"
2292 + ],
2293 + "dev": true,
2294 + "libc": [
2295 + "musl"
2296 + ],
2297 + "license": "MIT",
2298 + "optional": true,
2299 + "os": [
2300 + "linux"
2301 + ]
2302 + },
2303 + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
2304 + "version": "1.12.2",
2305 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz",
2306 + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==",
2307 + "cpu": [
2308 + "s390x"
2309 + ],
2310 + "dev": true,
2311 + "libc": [
2312 + "glibc"
2313 + ],
2314 + "license": "MIT",
2315 + "optional": true,
2316 + "os": [
2317 + "linux"
2318 + ]
2319 + },
2320 + "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
2321 + "version": "1.12.2",
2322 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz",
2323 + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==",
2324 + "cpu": [
2325 + "x64"
2326 + ],
2327 + "dev": true,
2328 + "libc": [
2329 + "glibc"
2330 + ],
2331 + "license": "MIT",
2332 + "optional": true,
2333 + "os": [
2334 + "linux"
2335 + ]
2336 + },
2337 + "node_modules/@unrs/resolver-binding-linux-x64-musl": {
2338 + "version": "1.12.2",
2339 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz",
2340 + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==",
2341 + "cpu": [
2342 + "x64"
2343 + ],
2344 + "dev": true,
2345 + "libc": [
2346 + "musl"
2347 + ],
2348 + "license": "MIT",
2349 + "optional": true,
2350 + "os": [
2351 + "linux"
2352 + ]
2353 + },
2354 + "node_modules/@unrs/resolver-binding-openharmony-arm64": {
2355 + "version": "1.12.2",
2356 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz",
2357 + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==",
2358 + "cpu": [
2359 + "arm64"
2360 + ],
2361 + "dev": true,
2362 + "license": "MIT",
2363 + "optional": true,
2364 + "os": [
2365 + "openharmony"
2366 + ]
2367 + },
2368 + "node_modules/@unrs/resolver-binding-wasm32-wasi": {
2369 + "version": "1.12.2",
2370 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz",
2371 + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==",
2372 + "cpu": [
2373 + "wasm32"
2374 + ],
2375 + "dev": true,
2376 + "license": "MIT",
2377 + "optional": true,
2378 + "dependencies": {
2379 + "@emnapi/core": "1.10.0",
2380 + "@emnapi/runtime": "1.10.0",
2381 + "@napi-rs/wasm-runtime": "^1.1.4"
2382 + },
2383 + "engines": {
2384 + "node": ">=14.0.0"
2385 + }
2386 + },
2387 + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": {
2388 + "version": "1.10.0",
2389 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
2390 + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
2391 + "dev": true,
2392 + "license": "MIT",
2393 + "optional": true,
2394 + "dependencies": {
2395 + "tslib": "^2.4.0"
2396 + }
2397 + },
2398 + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
2399 + "version": "1.12.2",
2400 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
2401 + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==",
2402 + "cpu": [
2403 + "arm64"
2404 + ],
2405 + "dev": true,
2406 + "license": "MIT",
2407 + "optional": true,
2408 + "os": [
2409 + "win32"
2410 + ]
2411 + },
2412 + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
2413 + "version": "1.12.2",
2414 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz",
2415 + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==",
2416 + "cpu": [
2417 + "ia32"
2418 + ],
2419 + "dev": true,
2420 + "license": "MIT",
2421 + "optional": true,
2422 + "os": [
2423 + "win32"
2424 + ]
2425 + },
2426 + "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
2427 + "version": "1.12.2",
2428 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz",
2429 + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==",
2430 + "cpu": [
2431 + "x64"
2432 + ],
2433 + "dev": true,
2434 + "license": "MIT",
2435 + "optional": true,
2436 + "os": [
2437 + "win32"
2438 + ]
2439 + },
2440 + "node_modules/acorn": {
2441 + "version": "8.18.0",
2442 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
2443 + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
2444 + "dev": true,
2445 + "license": "MIT",
2446 + "bin": {
2447 + "acorn": "bin/acorn"
2448 + },
2449 + "engines": {
2450 + "node": ">=0.4.0"
2451 + }
2452 + },
2453 + "node_modules/acorn-jsx": {
2454 + "version": "5.3.2",
2455 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
2456 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
2457 + "dev": true,
2458 + "license": "MIT",
2459 + "peerDependencies": {
2460 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
2461 + }
2462 + },
2463 + "node_modules/ajv": {
2464 + "version": "6.15.0",
2465 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
2466 + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
2467 + "dev": true,
2468 + "license": "MIT",
2469 + "dependencies": {
2470 + "fast-deep-equal": "^3.1.1",
2471 + "fast-json-stable-stringify": "^2.0.0",
2472 + "json-schema-traverse": "^0.4.1",
2473 + "uri-js": "^4.2.2"
2474 + },
2475 + "funding": {
2476 + "type": "github",
2477 + "url": "https://github.com/sponsors/epoberezkin"
2478 + }
2479 + },
2480 + "node_modules/ansi-styles": {
2481 + "version": "4.3.0",
2482 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
2483 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
2484 + "dev": true,
2485 + "license": "MIT",
2486 + "dependencies": {
2487 + "color-convert": "^2.0.1"
2488 + },
2489 + "engines": {
2490 + "node": ">=8"
2491 + },
2492 + "funding": {
2493 + "url": "https://github.com/chalk/ansi-styles?sponsor=1"
2494 + }
2495 + },
2496 + "node_modules/argparse": {
2497 + "version": "2.0.1",
2498 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
2499 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
2500 + "dev": true,
2501 + "license": "Python-2.0"
2502 + },
2503 + "node_modules/aria-query": {
2504 + "version": "5.3.2",
2505 + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
2506 + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
2507 + "dev": true,
2508 + "license": "Apache-2.0",
2509 + "engines": {
2510 + "node": ">= 0.4"
2511 + }
2512 + },
2513 + "node_modules/array-buffer-byte-length": {
2514 + "version": "1.0.2",
2515 + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
2516 + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
2517 + "dev": true,
2518 + "license": "MIT",
2519 + "dependencies": {
2520 + "call-bound": "^1.0.3",
2521 + "is-array-buffer": "^3.0.5"
2522 + },
2523 + "engines": {
2524 + "node": ">= 0.4"
2525 + },
2526 + "funding": {
2527 + "url": "https://github.com/sponsors/ljharb"
2528 + }
2529 + },
2530 + "node_modules/array-includes": {
2531 + "version": "3.1.9",
2532 + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
2533 + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
2534 + "dev": true,
2535 + "license": "MIT",
2536 + "dependencies": {
2537 + "call-bind": "^1.0.8",
2538 + "call-bound": "^1.0.4",
2539 + "define-properties": "^1.2.1",
2540 + "es-abstract": "^1.24.0",
2541 + "es-object-atoms": "^1.1.1",
2542 + "get-intrinsic": "^1.3.0",
2543 + "is-string": "^1.1.1",
2544 + "math-intrinsics": "^1.1.0"
2545 + },
2546 + "engines": {
2547 + "node": ">= 0.4"
2548 + },
2549 + "funding": {
2550 + "url": "https://github.com/sponsors/ljharb"
2551 + }
2552 + },
2553 + "node_modules/array.prototype.findlast": {
2554 + "version": "1.2.5",
2555 + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
2556 + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
2557 + "dev": true,
2558 + "license": "MIT",
2559 + "dependencies": {
2560 + "call-bind": "^1.0.7",
2561 + "define-properties": "^1.2.1",
2562 + "es-abstract": "^1.23.2",
2563 + "es-errors": "^1.3.0",
2564 + "es-object-atoms": "^1.0.0",
2565 + "es-shim-unscopables": "^1.0.2"
2566 + },
2567 + "engines": {
2568 + "node": ">= 0.4"
2569 + },
2570 + "funding": {
2571 + "url": "https://github.com/sponsors/ljharb"
2572 + }
2573 + },
2574 + "node_modules/array.prototype.findlastindex": {
2575 + "version": "1.2.6",
2576 + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
2577 + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
2578 + "dev": true,
2579 + "license": "MIT",
2580 + "dependencies": {
2581 + "call-bind": "^1.0.8",
2582 + "call-bound": "^1.0.4",
2583 + "define-properties": "^1.2.1",
2584 + "es-abstract": "^1.23.9",
2585 + "es-errors": "^1.3.0",
2586 + "es-object-atoms": "^1.1.1",
2587 + "es-shim-unscopables": "^1.1.0"
2588 + },
2589 + "engines": {
2590 + "node": ">= 0.4"
2591 + },
2592 + "funding": {
2593 + "url": "https://github.com/sponsors/ljharb"
2594 + }
2595 + },
2596 + "node_modules/array.prototype.flat": {
2597 + "version": "1.3.3",
2598 + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
2599 + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
2600 + "dev": true,
2601 + "license": "MIT",
2602 + "dependencies": {
2603 + "call-bind": "^1.0.8",
2604 + "define-properties": "^1.2.1",
2605 + "es-abstract": "^1.23.5",
2606 + "es-shim-unscopables": "^1.0.2"
2607 + },
2608 + "engines": {
2609 + "node": ">= 0.4"
2610 + },
2611 + "funding": {
2612 + "url": "https://github.com/sponsors/ljharb"
2613 + }
2614 + },
2615 + "node_modules/array.prototype.flatmap": {
2616 + "version": "1.3.3",
2617 + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
2618 + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
2619 + "dev": true,
2620 + "license": "MIT",
2621 + "dependencies": {
2622 + "call-bind": "^1.0.8",
2623 + "define-properties": "^1.2.1",
2624 + "es-abstract": "^1.23.5",
2625 + "es-shim-unscopables": "^1.0.2"
2626 + },
2627 + "engines": {
2628 + "node": ">= 0.4"
2629 + },
2630 + "funding": {
2631 + "url": "https://github.com/sponsors/ljharb"
2632 + }
2633 + },
2634 + "node_modules/array.prototype.tosorted": {
2635 + "version": "1.1.4",
2636 + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
2637 + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
2638 + "dev": true,
2639 + "license": "MIT",
2640 + "dependencies": {
2641 + "call-bind": "^1.0.7",
2642 + "define-properties": "^1.2.1",
2643 + "es-abstract": "^1.23.3",
2644 + "es-errors": "^1.3.0",
2645 + "es-shim-unscopables": "^1.0.2"
2646 + },
2647 + "engines": {
2648 + "node": ">= 0.4"
2649 + }
2650 + },
2651 + "node_modules/arraybuffer.prototype.slice": {
2652 + "version": "1.0.4",
2653 + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
2654 + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
2655 + "dev": true,
2656 + "license": "MIT",
2657 + "dependencies": {
2658 + "array-buffer-byte-length": "^1.0.1",
2659 + "call-bind": "^1.0.8",
2660 + "define-properties": "^1.2.1",
2661 + "es-abstract": "^1.23.5",
2662 + "es-errors": "^1.3.0",
2663 + "get-intrinsic": "^1.2.6",
2664 + "is-array-buffer": "^3.0.4"
2665 + },
2666 + "engines": {
2667 + "node": ">= 0.4"
2668 + },
2669 + "funding": {
2670 + "url": "https://github.com/sponsors/ljharb"
2671 + }
2672 + },
2673 + "node_modules/ast-types-flow": {
2674 + "version": "0.0.8",
2675 + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
2676 + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
2677 + "dev": true,
2678 + "license": "MIT"
2679 + },
2680 + "node_modules/async-function": {
2681 + "version": "1.0.0",
2682 + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
2683 + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
2684 + "dev": true,
2685 + "license": "MIT",
2686 + "engines": {
2687 + "node": ">= 0.4"
2688 + }
2689 + },
2690 + "node_modules/available-typed-arrays": {
2691 + "version": "1.0.7",
2692 + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
2693 + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
2694 + "dev": true,
2695 + "license": "MIT",
2696 + "dependencies": {
2697 + "possible-typed-array-names": "^1.0.0"
2698 + },
2699 + "engines": {
2700 + "node": ">= 0.4"
2701 + },
2702 + "funding": {
2703 + "url": "https://github.com/sponsors/ljharb"
2704 + }
2705 + },
2706 + "node_modules/axe-core": {
2707 + "version": "4.13.0",
2708 + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz",
2709 + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==",
2710 + "dev": true,
2711 + "license": "MPL-2.0",
2712 + "engines": {
2713 + "node": ">=4"
2714 + }
2715 + },
2716 + "node_modules/axobject-query": {
2717 + "version": "4.1.0",
2718 + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
2719 + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
2720 + "dev": true,
2721 + "license": "Apache-2.0",
2722 + "engines": {
2723 + "node": ">= 0.4"
2724 + }
2725 + },
2726 + "node_modules/bail": {
2727 + "version": "2.0.2",
2728 + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
2729 + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
2730 + "license": "MIT",
2731 + "funding": {
2732 + "type": "github",
2733 + "url": "https://github.com/sponsors/wooorm"
2734 + }
2735 + },
2736 + "node_modules/balanced-match": {
2737 + "version": "1.0.2",
2738 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
2739 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
2740 + "dev": true,
2741 + "license": "MIT"
2742 + },
2743 + "node_modules/baseline-browser-mapping": {
2744 + "version": "2.11.13",
2745 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz",
2746 + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==",
2747 + "license": "Apache-2.0",
2748 + "bin": {
2749 + "baseline-browser-mapping": "dist/cli.cjs"
2750 + },
2751 + "engines": {
2752 + "node": ">=6.0.0"
2753 + }
2754 + },
2755 + "node_modules/better-sqlite3": {
2756 + "version": "13.0.3",
2757 + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
2758 + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
2759 + "license": "MIT",
2760 + "dependencies": {
2761 + "node-addon-api": "^8.0.0"
2762 + },
2763 + "engines": {
2764 + "node": ">=22"
2765 + }
2766 + },
2767 + "node_modules/brace-expansion": {
2768 + "version": "1.1.18",
2769 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
2770 + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
2771 + "dev": true,
2772 + "license": "MIT",
2773 + "dependencies": {
2774 + "balanced-match": "^1.0.0",
2775 + "concat-map": "0.0.1"
2776 + }
2777 + },
2778 + "node_modules/braces": {
2779 + "version": "3.0.3",
2780 + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
2781 + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
2782 + "dev": true,
2783 + "license": "MIT",
2784 + "dependencies": {
2785 + "fill-range": "^7.1.1"
2786 + },
2787 + "engines": {
2788 + "node": ">=8"
2789 + }
2790 + },
2791 + "node_modules/browserslist": {
2792 + "version": "4.28.8",
2793 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
2794 + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
2795 + "dev": true,
2796 + "funding": [
2797 + {
2798 + "type": "opencollective",
2799 + "url": "https://opencollective.com/browserslist"
2800 + },
2801 + {
2802 + "type": "tidelift",
2803 + "url": "https://tidelift.com/funding/github/npm/browserslist"
2804 + },
2805 + {
2806 + "type": "github",
2807 + "url": "https://github.com/sponsors/ai"
2808 + }
2809 + ],
2810 + "license": "MIT",
2811 + "dependencies": {
2812 + "baseline-browser-mapping": "^2.11.12",
2813 + "caniuse-lite": "^1.0.30001809",
2814 + "electron-to-chromium": "^1.5.402",
2815 + "node-releases": "^2.0.53",
2816 + "update-browserslist-db": "^1.3.0"
2817 + },
2818 + "bin": {
2819 + "browserslist": "cli.js"
2820 + },
2821 + "engines": {
2822 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
2823 + }
2824 + },
2825 + "node_modules/call-bind": {
2826 + "version": "1.0.9",
2827 + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
2828 + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
2829 + "dev": true,
2830 + "license": "MIT",
2831 + "dependencies": {
2832 + "call-bind-apply-helpers": "^1.0.2",
2833 + "es-define-property": "^1.0.1",
2834 + "get-intrinsic": "^1.3.0",
2835 + "set-function-length": "^1.2.2"
2836 + },
2837 + "engines": {
2838 + "node": ">= 0.4"
2839 + },
2840 + "funding": {
2841 + "url": "https://github.com/sponsors/ljharb"
2842 + }
2843 + },
2844 + "node_modules/call-bind-apply-helpers": {
2845 + "version": "1.0.2",
2846 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
2847 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
2848 + "dev": true,
2849 + "license": "MIT",
2850 + "dependencies": {
2851 + "es-errors": "^1.3.0",
2852 + "function-bind": "^1.1.2"
2853 + },
2854 + "engines": {
2855 + "node": ">= 0.4"
2856 + }
2857 + },
2858 + "node_modules/call-bound": {
2859 + "version": "1.0.4",
2860 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
2861 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
2862 + "dev": true,
2863 + "license": "MIT",
2864 + "dependencies": {
2865 + "call-bind-apply-helpers": "^1.0.2",
2866 + "get-intrinsic": "^1.3.0"
2867 + },
2868 + "engines": {
2869 + "node": ">= 0.4"
2870 + },
2871 + "funding": {
2872 + "url": "https://github.com/sponsors/ljharb"
2873 + }
2874 + },
2875 + "node_modules/callsites": {
2876 + "version": "3.1.0",
2877 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
2878 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
2879 + "dev": true,
2880 + "license": "MIT",
2881 + "engines": {
2882 + "node": ">=6"
2883 + }
2884 + },
2885 + "node_modules/caniuse-lite": {
2886 + "version": "1.0.30001809",
2887 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
2888 + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
2889 + "funding": [
2890 + {
2891 + "type": "opencollective",
2892 + "url": "https://opencollective.com/browserslist"
2893 + },
2894 + {
2895 + "type": "tidelift",
2896 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
2897 + },
2898 + {
2899 + "type": "github",
2900 + "url": "https://github.com/sponsors/ai"
2901 + }
2902 + ],
2903 + "license": "CC-BY-4.0"
2904 + },
2905 + "node_modules/ccount": {
2906 + "version": "2.0.1",
2907 + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
2908 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
2909 + "license": "MIT",
2910 + "funding": {
2911 + "type": "github",
2912 + "url": "https://github.com/sponsors/wooorm"
2913 + }
2914 + },
2915 + "node_modules/chalk": {
2916 + "version": "4.1.2",
2917 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
2918 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
2919 + "dev": true,
2920 + "license": "MIT",
2921 + "dependencies": {
2922 + "ansi-styles": "^4.1.0",
2923 + "supports-color": "^7.1.0"
2924 + },
2925 + "engines": {
2926 + "node": ">=10"
2927 + },
2928 + "funding": {
2929 + "url": "https://github.com/chalk/chalk?sponsor=1"
2930 + }
2931 + },
2932 + "node_modules/character-entities": {
2933 + "version": "2.0.2",
2934 + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
2935 + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
2936 + "license": "MIT",
2937 + "funding": {
2938 + "type": "github",
2939 + "url": "https://github.com/sponsors/wooorm"
2940 + }
2941 + },
2942 + "node_modules/character-entities-html4": {
2943 + "version": "2.1.0",
2944 + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
2945 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
2946 + "license": "MIT",
2947 + "funding": {
2948 + "type": "github",
2949 + "url": "https://github.com/sponsors/wooorm"
2950 + }
2951 + },
2952 + "node_modules/character-entities-legacy": {
2953 + "version": "3.0.0",
2954 + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
2955 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
2956 + "license": "MIT",
2957 + "funding": {
2958 + "type": "github",
2959 + "url": "https://github.com/sponsors/wooorm"
2960 + }
2961 + },
2962 + "node_modules/character-reference-invalid": {
2963 + "version": "2.0.1",
2964 + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
2965 + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
2966 + "license": "MIT",
2967 + "funding": {
2968 + "type": "github",
2969 + "url": "https://github.com/sponsors/wooorm"
2970 + }
2971 + },
2972 + "node_modules/client-only": {
2973 + "version": "0.0.1",
2974 + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
2975 + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
2976 + "license": "MIT"
2977 + },
2978 + "node_modules/color-convert": {
2979 + "version": "2.0.1",
2980 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
2981 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
2982 + "dev": true,
2983 + "license": "MIT",
2984 + "dependencies": {
2985 + "color-name": "~1.1.4"
2986 + },
2987 + "engines": {
2988 + "node": ">=7.0.0"
2989 + }
2990 + },
2991 + "node_modules/color-name": {
2992 + "version": "1.1.4",
2993 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
2994 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
2995 + "dev": true,
2996 + "license": "MIT"
2997 + },
2998 + "node_modules/comma-separated-tokens": {
2999 + "version": "2.0.3",
3000 + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
3001 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
3002 + "license": "MIT",
3003 + "funding": {
3004 + "type": "github",
3005 + "url": "https://github.com/sponsors/wooorm"
3006 + }
3007 + },
3008 + "node_modules/commander": {
3009 + "version": "8.3.0",
3010 + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
3011 + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
3012 + "license": "MIT",
3013 + "engines": {
3014 + "node": ">= 12"
3015 + }
3016 + },
3017 + "node_modules/concat-map": {
3018 + "version": "0.0.1",
3019 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
3020 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
3021 + "dev": true,
3022 + "license": "MIT"
3023 + },
3024 + "node_modules/convert-source-map": {
3025 + "version": "2.0.0",
3026 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
3027 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
3028 + "dev": true,
3029 + "license": "MIT"
3030 + },
3031 + "node_modules/cross-spawn": {
3032 + "version": "7.0.6",
3033 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
3034 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
3035 + "dev": true,
3036 + "license": "MIT",
3037 + "dependencies": {
3038 + "path-key": "^3.1.0",
3039 + "shebang-command": "^2.0.0",
3040 + "which": "^2.0.1"
3041 + },
3042 + "engines": {
3043 + "node": ">= 8"
3044 + }
3045 + },
3046 + "node_modules/csstype": {
3047 + "version": "3.2.3",
3048 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
3049 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
3050 + "license": "MIT"
3051 + },
3052 + "node_modules/damerau-levenshtein": {
3053 + "version": "1.0.8",
3054 + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
3055 + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
3056 + "dev": true,
3057 + "license": "BSD-2-Clause"
3058 + },
3059 + "node_modules/data-view-buffer": {
3060 + "version": "1.0.2",
3061 + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
3062 + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
3063 + "dev": true,
3064 + "license": "MIT",
3065 + "dependencies": {
3066 + "call-bound": "^1.0.3",
3067 + "es-errors": "^1.3.0",
3068 + "is-data-view": "^1.0.2"
3069 + },
3070 + "engines": {
3071 + "node": ">= 0.4"
3072 + },
3073 + "funding": {
3074 + "url": "https://github.com/sponsors/ljharb"
3075 + }
3076 + },
3077 + "node_modules/data-view-byte-length": {
3078 + "version": "1.0.2",
3079 + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
3080 + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
3081 + "dev": true,
3082 + "license": "MIT",
3083 + "dependencies": {
3084 + "call-bound": "^1.0.3",
3085 + "es-errors": "^1.3.0",
3086 + "is-data-view": "^1.0.2"
3087 + },
3088 + "engines": {
3089 + "node": ">= 0.4"
3090 + },
3091 + "funding": {
3092 + "url": "https://github.com/sponsors/inspect-js"
3093 + }
3094 + },
3095 + "node_modules/data-view-byte-offset": {
3096 + "version": "1.0.1",
3097 + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
3098 + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
3099 + "dev": true,
3100 + "license": "MIT",
3101 + "dependencies": {
3102 + "call-bound": "^1.0.2",
3103 + "es-errors": "^1.3.0",
3104 + "is-data-view": "^1.0.1"
3105 + },
3106 + "engines": {
3107 + "node": ">= 0.4"
3108 + },
3109 + "funding": {
3110 + "url": "https://github.com/sponsors/ljharb"
3111 + }
3112 + },
3113 + "node_modules/debug": {
3114 + "version": "4.4.3",
3115 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
3116 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
3117 + "license": "MIT",
3118 + "dependencies": {
3119 + "ms": "^2.1.3"
3120 + },
3121 + "engines": {
3122 + "node": ">=6.0"
3123 + },
3124 + "peerDependenciesMeta": {
3125 + "supports-color": {
3126 + "optional": true
3127 + }
3128 + }
3129 + },
3130 + "node_modules/decode-named-character-reference": {
3131 + "version": "1.3.0",
3132 + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
3133 + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
3134 + "license": "MIT",
3135 + "dependencies": {
3136 + "character-entities": "^2.0.0"
3137 + },
3138 + "funding": {
3139 + "type": "github",
3140 + "url": "https://github.com/sponsors/wooorm"
3141 + }
3142 + },
3143 + "node_modules/deep-is": {
3144 + "version": "0.1.4",
3145 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
3146 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
3147 + "dev": true,
3148 + "license": "MIT"
3149 + },
3150 + "node_modules/define-data-property": {
3151 + "version": "1.1.4",
3152 + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
3153 + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
3154 + "dev": true,
3155 + "license": "MIT",
3156 + "dependencies": {
3157 + "es-define-property": "^1.0.0",
3158 + "es-errors": "^1.3.0",
3159 + "gopd": "^1.0.1"
3160 + },
3161 + "engines": {
3162 + "node": ">= 0.4"
3163 + },
3164 + "funding": {
3165 + "url": "https://github.com/sponsors/ljharb"
3166 + }
3167 + },
3168 + "node_modules/define-properties": {
3169 + "version": "1.2.1",
3170 + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
3171 + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
3172 + "dev": true,
3173 + "license": "MIT",
3174 + "dependencies": {
3175 + "define-data-property": "^1.0.1",
3176 + "has-property-descriptors": "^1.0.0",
3177 + "object-keys": "^1.1.1"
3178 + },
3179 + "engines": {
3180 + "node": ">= 0.4"
3181 + },
3182 + "funding": {
3183 + "url": "https://github.com/sponsors/ljharb"
3184 + }
3185 + },
3186 + "node_modules/dequal": {
3187 + "version": "2.0.3",
3188 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
3189 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
3190 + "license": "MIT",
3191 + "engines": {
3192 + "node": ">=6"
3193 + }
3194 + },
3195 + "node_modules/detect-libc": {
3196 + "version": "2.1.2",
3197 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
3198 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
3199 + "license": "Apache-2.0",
3200 + "optional": true,
3201 + "engines": {
3202 + "node": ">=8"
3203 + }
3204 + },
3205 + "node_modules/devlop": {
3206 + "version": "1.1.0",
3207 + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
3208 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
3209 + "license": "MIT",
3210 + "dependencies": {
3211 + "dequal": "^2.0.0"
3212 + },
3213 + "funding": {
3214 + "type": "github",
3215 + "url": "https://github.com/sponsors/wooorm"
3216 + }
3217 + },
3218 + "node_modules/doctrine": {
3219 + "version": "2.1.0",
3220 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
3221 + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
3222 + "dev": true,
3223 + "license": "Apache-2.0",
3224 + "dependencies": {
3225 + "esutils": "^2.0.2"
3226 + },
3227 + "engines": {
3228 + "node": ">=0.10.0"
3229 + }
3230 + },
3231 + "node_modules/dunder-proto": {
3232 + "version": "1.0.1",
3233 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
3234 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
3235 + "dev": true,
3236 + "license": "MIT",
3237 + "dependencies": {
3238 + "call-bind-apply-helpers": "^1.0.1",
3239 + "es-errors": "^1.3.0",
3240 + "gopd": "^1.2.0"
3241 + },
3242 + "engines": {
3243 + "node": ">= 0.4"
3244 + }
3245 + },
3246 + "node_modules/electron-to-chromium": {
3247 + "version": "1.5.405",
3248 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz",
3249 + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==",
3250 + "dev": true,
3251 + "license": "ISC"
3252 + },
3253 + "node_modules/emoji-regex": {
3254 + "version": "9.2.2",
3255 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
3256 + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
3257 + "dev": true,
3258 + "license": "MIT"
3259 + },
3260 + "node_modules/entities": {
3261 + "version": "6.0.1",
3262 + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
3263 + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
3264 + "license": "BSD-2-Clause",
3265 + "engines": {
3266 + "node": ">=0.12"
3267 + },
3268 + "funding": {
3269 + "url": "https://github.com/fb55/entities?sponsor=1"
3270 + }
3271 + },
3272 + "node_modules/es-abstract": {
3273 + "version": "1.24.2",
3274 + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
3275 + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
3276 + "dev": true,
3277 + "license": "MIT",
3278 + "dependencies": {
3279 + "array-buffer-byte-length": "^1.0.2",
3280 + "arraybuffer.prototype.slice": "^1.0.4",
3281 + "available-typed-arrays": "^1.0.7",
3282 + "call-bind": "^1.0.8",
3283 + "call-bound": "^1.0.4",
3284 + "data-view-buffer": "^1.0.2",
3285 + "data-view-byte-length": "^1.0.2",
3286 + "data-view-byte-offset": "^1.0.1",
3287 + "es-define-property": "^1.0.1",
3288 + "es-errors": "^1.3.0",
3289 + "es-object-atoms": "^1.1.1",
3290 + "es-set-tostringtag": "^2.1.0",
3291 + "es-to-primitive": "^1.3.0",
3292 + "function.prototype.name": "^1.1.8",
3293 + "get-intrinsic": "^1.3.0",
3294 + "get-proto": "^1.0.1",
3295 + "get-symbol-description": "^1.1.0",
3296 + "globalthis": "^1.0.4",
3297 + "gopd": "^1.2.0",
3298 + "has-property-descriptors": "^1.0.2",
3299 + "has-proto": "^1.2.0",
3300 + "has-symbols": "^1.1.0",
3301 + "hasown": "^2.0.2",
3302 + "internal-slot": "^1.1.0",
3303 + "is-array-buffer": "^3.0.5",
3304 + "is-callable": "^1.2.7",
3305 + "is-data-view": "^1.0.2",
3306 + "is-negative-zero": "^2.0.3",
3307 + "is-regex": "^1.2.1",
3308 + "is-set": "^2.0.3",
3309 + "is-shared-array-buffer": "^1.0.4",
3310 + "is-string": "^1.1.1",
3311 + "is-typed-array": "^1.1.15",
3312 + "is-weakref": "^1.1.1",
3313 + "math-intrinsics": "^1.1.0",
3314 + "object-inspect": "^1.13.4",
3315 + "object-keys": "^1.1.1",
3316 + "object.assign": "^4.1.7",
3317 + "own-keys": "^1.0.1",
3318 + "regexp.prototype.flags": "^1.5.4",
3319 + "safe-array-concat": "^1.1.3",
3320 + "safe-push-apply": "^1.0.0",
3321 + "safe-regex-test": "^1.1.0",
3322 + "set-proto": "^1.0.0",
3323 + "stop-iteration-iterator": "^1.1.0",
3324 + "string.prototype.trim": "^1.2.10",
3325 + "string.prototype.trimend": "^1.0.9",
3326 + "string.prototype.trimstart": "^1.0.8",
3327 + "typed-array-buffer": "^1.0.3",
3328 + "typed-array-byte-length": "^1.0.3",
3329 + "typed-array-byte-offset": "^1.0.4",
3330 + "typed-array-length": "^1.0.7",
3331 + "unbox-primitive": "^1.1.0",
3332 + "which-typed-array": "^1.1.19"
3333 + },
3334 + "engines": {
3335 + "node": ">= 0.4"
3336 + },
3337 + "funding": {
3338 + "url": "https://github.com/sponsors/ljharb"
3339 + }
3340 + },
3341 + "node_modules/es-abstract-get": {
3342 + "version": "1.0.0",
3343 + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz",
3344 + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==",
3345 + "dev": true,
3346 + "license": "MIT",
3347 + "dependencies": {
3348 + "es-errors": "^1.3.0",
3349 + "es-object-atoms": "^1.1.2",
3350 + "is-callable": "^1.2.7",
3351 + "object-inspect": "^1.13.4"
3352 + },
3353 + "engines": {
3354 + "node": ">= 0.4"
3355 + },
3356 + "funding": {
3357 + "url": "https://github.com/sponsors/ljharb"
3358 + }
3359 + },
3360 + "node_modules/es-define-property": {
3361 + "version": "1.0.1",
3362 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
3363 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
3364 + "dev": true,
3365 + "license": "MIT",
3366 + "engines": {
3367 + "node": ">= 0.4"
3368 + }
3369 + },
3370 + "node_modules/es-errors": {
3371 + "version": "1.3.0",
3372 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
3373 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
3374 + "dev": true,
3375 + "license": "MIT",
3376 + "engines": {
3377 + "node": ">= 0.4"
3378 + }
3379 + },
3380 + "node_modules/es-iterator-helpers": {
3381 + "version": "1.4.0",
3382 + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz",
3383 + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==",
3384 + "dev": true,
3385 + "license": "MIT",
3386 + "dependencies": {
3387 + "call-bind": "^1.0.9",
3388 + "call-bound": "^1.0.4",
3389 + "define-properties": "^1.2.1",
3390 + "es-abstract": "^1.24.2",
3391 + "es-errors": "^1.3.0",
3392 + "es-set-tostringtag": "^2.1.0",
3393 + "function-bind": "^1.1.2",
3394 + "get-intrinsic": "^1.3.0",
3395 + "globalthis": "^1.0.4",
3396 + "gopd": "^1.2.0",
3397 + "has-property-descriptors": "^1.0.2",
3398 + "has-proto": "^1.2.0",
3399 + "has-symbols": "^1.1.0",
3400 + "internal-slot": "^1.1.0",
3401 + "iterator.prototype": "^1.1.5",
3402 + "math-intrinsics": "^1.1.0"
3403 + },
3404 + "engines": {
3405 + "node": ">= 0.4"
3406 + }
3407 + },
3408 + "node_modules/es-object-atoms": {
3409 + "version": "1.1.2",
3410 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
3411 + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
3412 + "dev": true,
3413 + "license": "MIT",
3414 + "dependencies": {
3415 + "es-errors": "^1.3.0"
3416 + },
3417 + "engines": {
3418 + "node": ">= 0.4"
3419 + }
3420 + },
3421 + "node_modules/es-set-tostringtag": {
3422 + "version": "2.1.0",
3423 + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
3424 + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
3425 + "dev": true,
3426 + "license": "MIT",
3427 + "dependencies": {
3428 + "es-errors": "^1.3.0",
3429 + "get-intrinsic": "^1.2.6",
3430 + "has-tostringtag": "^1.0.2",
3431 + "hasown": "^2.0.2"
3432 + },
3433 + "engines": {
3434 + "node": ">= 0.4"
3435 + }
3436 + },
3437 + "node_modules/es-shim-unscopables": {
3438 + "version": "1.1.0",
3439 + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
3440 + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
3441 + "dev": true,
3442 + "license": "MIT",
3443 + "dependencies": {
3444 + "hasown": "^2.0.2"
3445 + },
3446 + "engines": {
3447 + "node": ">= 0.4"
3448 + }
3449 + },
3450 + "node_modules/es-to-primitive": {
3451 + "version": "1.3.4",
3452 + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz",
3453 + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==",
3454 + "dev": true,
3455 + "license": "MIT",
3456 + "dependencies": {
3457 + "es-abstract-get": "^1.0.0",
3458 + "es-define-property": "^1.0.1",
3459 + "es-errors": "^1.3.0",
3460 + "is-callable": "^1.2.7",
3461 + "is-date-object": "^1.1.0",
3462 + "is-symbol": "^1.1.1"
3463 + },
3464 + "engines": {
3465 + "node": ">= 0.4"
3466 + },
3467 + "funding": {
3468 + "url": "https://github.com/sponsors/ljharb"
3469 + }
3470 + },
3471 + "node_modules/escalade": {
3472 + "version": "3.2.0",
3473 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
3474 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
3475 + "dev": true,
3476 + "license": "MIT",
3477 + "engines": {
3478 + "node": ">=6"
3479 + }
3480 + },
3481 + "node_modules/escape-string-regexp": {
3482 + "version": "4.0.0",
3483 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
3484 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
3485 + "dev": true,
3486 + "license": "MIT",
3487 + "engines": {
3488 + "node": ">=10"
3489 + },
3490 + "funding": {
3491 + "url": "https://github.com/sponsors/sindresorhus"
3492 + }
3493 + },
3494 + "node_modules/eslint": {
3495 + "version": "9.39.5",
3496 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
3497 + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
3498 + "dev": true,
3499 + "license": "MIT",
3500 + "dependencies": {
3501 + "@eslint-community/eslint-utils": "^4.8.0",
3502 + "@eslint-community/regexpp": "^4.12.1",
3503 + "@eslint/config-array": "^0.21.2",
3504 + "@eslint/config-helpers": "^0.4.2",
3505 + "@eslint/core": "^0.17.0",
3506 + "@eslint/eslintrc": "^3.3.6",
3507 + "@eslint/js": "9.39.5",
3508 + "@eslint/plugin-kit": "^0.4.1",
3509 + "@humanfs/node": "^0.16.6",
3510 + "@humanwhocodes/module-importer": "^1.0.1",
3511 + "@humanwhocodes/retry": "^0.4.2",
3512 + "@types/estree": "^1.0.6",
3513 + "ajv": "^6.14.0",
3514 + "chalk": "^4.0.0",
3515 + "cross-spawn": "^7.0.6",
3516 + "debug": "^4.3.2",
3517 + "escape-string-regexp": "^4.0.0",
3518 + "eslint-scope": "^8.4.0",
3519 + "eslint-visitor-keys": "^4.2.1",
3520 + "espree": "^10.4.0",
3521 + "esquery": "^1.5.0",
3522 + "esutils": "^2.0.2",
3523 + "fast-deep-equal": "^3.1.3",
3524 + "file-entry-cache": "^8.0.0",
3525 + "find-up": "^5.0.0",
3526 + "glob-parent": "^6.0.2",
3527 + "ignore": "^5.2.0",
3528 + "imurmurhash": "^0.1.4",
3529 + "is-glob": "^4.0.0",
3530 + "json-stable-stringify-without-jsonify": "^1.0.1",
3531 + "lodash.merge": "^4.6.2",
3532 + "minimatch": "^3.1.5",
3533 + "natural-compare": "^1.4.0",
3534 + "optionator": "^0.9.3"
3535 + },
3536 + "bin": {
3537 + "eslint": "bin/eslint.js"
3538 + },
3539 + "engines": {
3540 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3541 + },
3542 + "funding": {
3543 + "url": "https://eslint.org/donate"
3544 + },
3545 + "peerDependencies": {
3546 + "jiti": "*"
3547 + },
3548 + "peerDependenciesMeta": {
3549 + "jiti": {
3550 + "optional": true
3551 + }
3552 + }
3553 + },
3554 + "node_modules/eslint-config-next": {
3555 + "version": "16.3.0",
3556 + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.0.tgz",
3557 + "integrity": "sha512-lPrf1kHsMJEZqO0uXkNB400c5MGrhrTk3BNX7P0ol4gt61+iUlQfjy9TyIOEA9eOXrf+5+mYbT/JsY8+zqUByQ==",
3558 + "dev": true,
3559 + "license": "MIT",
3560 + "dependencies": {
3561 + "@next/eslint-plugin-next": "16.3.0",
3562 + "eslint-import-resolver-node": "^0.3.6",
3563 + "eslint-import-resolver-typescript": "^3.5.2",
3564 + "eslint-plugin-import": "^2.32.0",
3565 + "eslint-plugin-jsx-a11y": "^6.10.0",
3566 + "eslint-plugin-react": "^7.37.0",
3567 + "eslint-plugin-react-hooks": "^7.0.0",
3568 + "globals": "16.4.0",
3569 + "typescript-eslint": "^8.46.0"
3570 + },
3571 + "peerDependencies": {
3572 + "eslint": ">=9.0.0",
3573 + "typescript": ">=3.3.1"
3574 + },
3575 + "peerDependenciesMeta": {
3576 + "typescript": {
3577 + "optional": true
3578 + }
3579 + }
3580 + },
3581 + "node_modules/eslint-config-next/node_modules/globals": {
3582 + "version": "16.4.0",
3583 + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
3584 + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
3585 + "dev": true,
3586 + "license": "MIT",
3587 + "engines": {
3588 + "node": ">=18"
3589 + },
3590 + "funding": {
3591 + "url": "https://github.com/sponsors/sindresorhus"
3592 + }
3593 + },
3594 + "node_modules/eslint-import-resolver-node": {
3595 + "version": "0.3.10",
3596 + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz",
3597 + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==",
3598 + "dev": true,
3599 + "license": "MIT",
3600 + "dependencies": {
3601 + "debug": "^3.2.7",
3602 + "is-core-module": "^2.16.1",
3603 + "resolve": "^2.0.0-next.6"
3604 + }
3605 + },
3606 + "node_modules/eslint-import-resolver-node/node_modules/debug": {
3607 + "version": "3.2.7",
3608 + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
3609 + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
3610 + "dev": true,
3611 + "license": "MIT",
3612 + "dependencies": {
3613 + "ms": "^2.1.1"
3614 + }
3615 + },
3616 + "node_modules/eslint-import-resolver-typescript": {
3617 + "version": "3.10.1",
3618 + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
3619 + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
3620 + "dev": true,
3621 + "license": "ISC",
3622 + "dependencies": {
3623 + "@nolyfill/is-core-module": "1.0.39",
3624 + "debug": "^4.4.0",
3625 + "get-tsconfig": "^4.10.0",
3626 + "is-bun-module": "^2.0.0",
3627 + "stable-hash": "^0.0.5",
3628 + "tinyglobby": "^0.2.13",
3629 + "unrs-resolver": "^1.6.2"
3630 + },
3631 + "engines": {
3632 + "node": "^14.18.0 || >=16.0.0"
3633 + },
3634 + "funding": {
3635 + "url": "https://opencollective.com/eslint-import-resolver-typescript"
3636 + },
3637 + "peerDependencies": {
3638 + "eslint": "*",
3639 + "eslint-plugin-import": "*",
3640 + "eslint-plugin-import-x": "*"
3641 + },
3642 + "peerDependenciesMeta": {
3643 + "eslint-plugin-import": {
3644 + "optional": true
3645 + },
3646 + "eslint-plugin-import-x": {
3647 + "optional": true
3648 + }
3649 + }
3650 + },
3651 + "node_modules/eslint-module-utils": {
3652 + "version": "2.14.0",
3653 + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz",
3654 + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==",
3655 + "dev": true,
3656 + "license": "MIT",
3657 + "dependencies": {
3658 + "debug": "^3.2.7"
3659 + },
3660 + "engines": {
3661 + "node": ">=4"
3662 + },
3663 + "peerDependenciesMeta": {
3664 + "eslint": {
3665 + "optional": true
3666 + }
3667 + }
3668 + },
3669 + "node_modules/eslint-module-utils/node_modules/debug": {
3670 + "version": "3.2.7",
3671 + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
3672 + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
3673 + "dev": true,
3674 + "license": "MIT",
3675 + "dependencies": {
3676 + "ms": "^2.1.1"
3677 + }
3678 + },
3679 + "node_modules/eslint-plugin-import": {
3680 + "version": "2.32.0",
3681 + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
3682 + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
3683 + "dev": true,
3684 + "license": "MIT",
3685 + "dependencies": {
3686 + "@rtsao/scc": "^1.1.0",
3687 + "array-includes": "^3.1.9",
3688 + "array.prototype.findlastindex": "^1.2.6",
3689 + "array.prototype.flat": "^1.3.3",
3690 + "array.prototype.flatmap": "^1.3.3",
3691 + "debug": "^3.2.7",
3692 + "doctrine": "^2.1.0",
3693 + "eslint-import-resolver-node": "^0.3.9",
3694 + "eslint-module-utils": "^2.12.1",
3695 + "hasown": "^2.0.2",
3696 + "is-core-module": "^2.16.1",
3697 + "is-glob": "^4.0.3",
3698 + "minimatch": "^3.1.2",
3699 + "object.fromentries": "^2.0.8",
3700 + "object.groupby": "^1.0.3",
3701 + "object.values": "^1.2.1",
3702 + "semver": "^6.3.1",
3703 + "string.prototype.trimend": "^1.0.9",
3704 + "tsconfig-paths": "^3.15.0"
3705 + },
3706 + "engines": {
3707 + "node": ">=4"
3708 + },
3709 + "peerDependencies": {
3710 + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
3711 + }
3712 + },
3713 + "node_modules/eslint-plugin-import/node_modules/debug": {
3714 + "version": "3.2.7",
3715 + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
3716 + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
3717 + "dev": true,
3718 + "license": "MIT",
3719 + "dependencies": {
3720 + "ms": "^2.1.1"
3721 + }
3722 + },
3723 + "node_modules/eslint-plugin-jsx-a11y": {
3724 + "version": "6.10.2",
3725 + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
3726 + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
3727 + "dev": true,
3728 + "license": "MIT",
3729 + "dependencies": {
3730 + "aria-query": "^5.3.2",
3731 + "array-includes": "^3.1.8",
3732 + "array.prototype.flatmap": "^1.3.2",
3733 + "ast-types-flow": "^0.0.8",
3734 + "axe-core": "^4.10.0",
3735 + "axobject-query": "^4.1.0",
3736 + "damerau-levenshtein": "^1.0.8",
3737 + "emoji-regex": "^9.2.2",
3738 + "hasown": "^2.0.2",
3739 + "jsx-ast-utils": "^3.3.5",
3740 + "language-tags": "^1.0.9",
3741 + "minimatch": "^3.1.2",
3742 + "object.fromentries": "^2.0.8",
3743 + "safe-regex-test": "^1.0.3",
3744 + "string.prototype.includes": "^2.0.1"
3745 + },
3746 + "engines": {
3747 + "node": ">=4.0"
3748 + },
3749 + "peerDependencies": {
3750 + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
3751 + }
3752 + },
3753 + "node_modules/eslint-plugin-react": {
3754 + "version": "7.37.5",
3755 + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
3756 + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
3757 + "dev": true,
3758 + "license": "MIT",
3759 + "dependencies": {
3760 + "array-includes": "^3.1.8",
3761 + "array.prototype.findlast": "^1.2.5",
3762 + "array.prototype.flatmap": "^1.3.3",
3763 + "array.prototype.tosorted": "^1.1.4",
3764 + "doctrine": "^2.1.0",
3765 + "es-iterator-helpers": "^1.2.1",
3766 + "estraverse": "^5.3.0",
3767 + "hasown": "^2.0.2",
3768 + "jsx-ast-utils": "^2.4.1 || ^3.0.0",
3769 + "minimatch": "^3.1.2",
3770 + "object.entries": "^1.1.9",
3771 + "object.fromentries": "^2.0.8",
3772 + "object.values": "^1.2.1",
3773 + "prop-types": "^15.8.1",
3774 + "resolve": "^2.0.0-next.5",
3775 + "semver": "^6.3.1",
3776 + "string.prototype.matchall": "^4.0.12",
3777 + "string.prototype.repeat": "^1.0.0"
3778 + },
3779 + "engines": {
3780 + "node": ">=4"
3781 + },
3782 + "peerDependencies": {
3783 + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
3784 + }
3785 + },
3786 + "node_modules/eslint-plugin-react-hooks": {
3787 + "version": "7.1.1",
3788 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
3789 + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
3790 + "dev": true,
3791 + "license": "MIT",
3792 + "dependencies": {
3793 + "@babel/core": "^7.24.4",
3794 + "@babel/parser": "^7.24.4",
3795 + "hermes-parser": "^0.25.1",
3796 + "zod": "^3.25.0 || ^4.0.0",
3797 + "zod-validation-error": "^3.5.0 || ^4.0.0"
3798 + },
3799 + "engines": {
3800 + "node": ">=18"
3801 + },
3802 + "peerDependencies": {
3803 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
3804 + }
3805 + },
3806 + "node_modules/eslint-scope": {
3807 + "version": "8.4.0",
3808 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
3809 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
3810 + "dev": true,
3811 + "license": "BSD-2-Clause",
3812 + "dependencies": {
3813 + "esrecurse": "^4.3.0",
3814 + "estraverse": "^5.2.0"
3815 + },
3816 + "engines": {
3817 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3818 + },
3819 + "funding": {
3820 + "url": "https://opencollective.com/eslint"
3821 + }
3822 + },
3823 + "node_modules/eslint-visitor-keys": {
3824 + "version": "4.2.1",
3825 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
3826 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
3827 + "dev": true,
3828 + "license": "Apache-2.0",
3829 + "engines": {
3830 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3831 + },
3832 + "funding": {
3833 + "url": "https://opencollective.com/eslint"
3834 + }
3835 + },
3836 + "node_modules/espree": {
3837 + "version": "10.4.0",
3838 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
3839 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
3840 + "dev": true,
3841 + "license": "BSD-2-Clause",
3842 + "dependencies": {
3843 + "acorn": "^8.15.0",
3844 + "acorn-jsx": "^5.3.2",
3845 + "eslint-visitor-keys": "^4.2.1"
3846 + },
3847 + "engines": {
3848 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3849 + },
3850 + "funding": {
3851 + "url": "https://opencollective.com/eslint"
3852 + }
3853 + },
3854 + "node_modules/esquery": {
3855 + "version": "1.7.0",
3856 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
3857 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
3858 + "dev": true,
3859 + "license": "BSD-3-Clause",
3860 + "dependencies": {
3861 + "estraverse": "^5.1.0"
3862 + },
3863 + "engines": {
3864 + "node": ">=0.10"
3865 + }
3866 + },
3867 + "node_modules/esrecurse": {
3868 + "version": "4.3.0",
3869 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
3870 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
3871 + "dev": true,
3872 + "license": "BSD-2-Clause",
3873 + "dependencies": {
3874 + "estraverse": "^5.2.0"
3875 + },
3876 + "engines": {
3877 + "node": ">=4.0"
3878 + }
3879 + },
3880 + "node_modules/estraverse": {
3881 + "version": "5.3.0",
3882 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
3883 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
3884 + "dev": true,
3885 + "license": "BSD-2-Clause",
3886 + "engines": {
3887 + "node": ">=4.0"
3888 + }
3889 + },
3890 + "node_modules/estree-util-is-identifier-name": {
3891 + "version": "3.0.0",
3892 + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
3893 + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
3894 + "license": "MIT",
3895 + "funding": {
3896 + "type": "opencollective",
3897 + "url": "https://opencollective.com/unified"
3898 + }
3899 + },
3900 + "node_modules/esutils": {
3901 + "version": "2.0.3",
3902 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
3903 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
3904 + "dev": true,
3905 + "license": "BSD-2-Clause",
3906 + "engines": {
3907 + "node": ">=0.10.0"
3908 + }
3909 + },
3910 + "node_modules/extend": {
3911 + "version": "3.0.2",
3912 + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
3913 + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
3914 + "license": "MIT"
3915 + },
3916 + "node_modules/fast-deep-equal": {
3917 + "version": "3.1.3",
3918 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
3919 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
3920 + "dev": true,
3921 + "license": "MIT"
3922 + },
3923 + "node_modules/fast-glob": {
3924 + "version": "3.3.1",
3925 + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
3926 + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
3927 + "dev": true,
3928 + "license": "MIT",
3929 + "dependencies": {
3930 + "@nodelib/fs.stat": "^2.0.2",
3931 + "@nodelib/fs.walk": "^1.2.3",
3932 + "glob-parent": "^5.1.2",
3933 + "merge2": "^1.3.0",
3934 + "micromatch": "^4.0.4"
3935 + },
3936 + "engines": {
3937 + "node": ">=8.6.0"
3938 + }
3939 + },
3940 + "node_modules/fast-glob/node_modules/glob-parent": {
3941 + "version": "5.1.2",
3942 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
3943 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
3944 + "dev": true,
3945 + "license": "ISC",
3946 + "dependencies": {
3947 + "is-glob": "^4.0.1"
3948 + },
3949 + "engines": {
3950 + "node": ">= 6"
3951 + }
3952 + },
3953 + "node_modules/fast-json-stable-stringify": {
3954 + "version": "2.1.0",
3955 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
3956 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
3957 + "dev": true,
3958 + "license": "MIT"
3959 + },
3960 + "node_modules/fast-levenshtein": {
3961 + "version": "2.0.6",
3962 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
3963 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
3964 + "dev": true,
3965 + "license": "MIT"
3966 + },
3967 + "node_modules/fastq": {
3968 + "version": "1.20.1",
3969 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
3970 + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
3971 + "dev": true,
3972 + "license": "ISC",
3973 + "dependencies": {
3974 + "reusify": "^1.0.4"
3975 + }
3976 + },
3977 + "node_modules/file-entry-cache": {
3978 + "version": "8.0.0",
3979 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
3980 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
3981 + "dev": true,
3982 + "license": "MIT",
3983 + "dependencies": {
3984 + "flat-cache": "^4.0.0"
3985 + },
3986 + "engines": {
3987 + "node": ">=16.0.0"
3988 + }
3989 + },
3990 + "node_modules/fill-range": {
3991 + "version": "7.1.1",
3992 + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
3993 + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
3994 + "dev": true,
3995 + "license": "MIT",
3996 + "dependencies": {
3997 + "to-regex-range": "^5.0.1"
3998 + },
3999 + "engines": {
4000 + "node": ">=8"
4001 + }
4002 + },
4003 + "node_modules/find-up": {
4004 + "version": "5.0.0",
4005 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
4006 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
4007 + "dev": true,
4008 + "license": "MIT",
4009 + "dependencies": {
4010 + "locate-path": "^6.0.0",
4011 + "path-exists": "^4.0.0"
4012 + },
4013 + "engines": {
4014 + "node": ">=10"
4015 + },
4016 + "funding": {
4017 + "url": "https://github.com/sponsors/sindresorhus"
4018 + }
4019 + },
4020 + "node_modules/flat-cache": {
4021 + "version": "4.0.1",
4022 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
4023 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
4024 + "dev": true,
4025 + "license": "MIT",
4026 + "dependencies": {
4027 + "flatted": "^3.2.9",
4028 + "keyv": "^4.5.4"
4029 + },
4030 + "engines": {
4031 + "node": ">=16"
4032 + }
4033 + },
4034 + "node_modules/flatted": {
4035 + "version": "3.4.4",
4036 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
4037 + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
4038 + "dev": true,
4039 + "license": "ISC"
4040 + },
4041 + "node_modules/for-each": {
4042 + "version": "0.3.5",
4043 + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
4044 + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
4045 + "dev": true,
4046 + "license": "MIT",
4047 + "dependencies": {
4048 + "is-callable": "^1.2.7"
4049 + },
4050 + "engines": {
4051 + "node": ">= 0.4"
4052 + },
4053 + "funding": {
4054 + "url": "https://github.com/sponsors/ljharb"
4055 + }
4056 + },
4057 + "node_modules/function-bind": {
4058 + "version": "1.1.2",
4059 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
4060 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
4061 + "dev": true,
4062 + "license": "MIT",
4063 + "funding": {
4064 + "url": "https://github.com/sponsors/ljharb"
4065 + }
4066 + },
4067 + "node_modules/function.prototype.name": {
4068 + "version": "1.2.0",
4069 + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
4070 + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
4071 + "dev": true,
4072 + "license": "MIT",
4073 + "dependencies": {
4074 + "call-bind": "^1.0.9",
4075 + "call-bound": "^1.0.4",
4076 + "es-define-property": "^1.0.1",
4077 + "es-errors": "^1.3.0",
4078 + "functions-have-names": "^1.2.3",
4079 + "has-property-descriptors": "^1.0.2",
4080 + "hasown": "^2.0.4",
4081 + "is-callable": "^1.2.7",
4082 + "is-document.all": "^1.0.0"
4083 + },
4084 + "engines": {
4085 + "node": ">= 0.4"
4086 + },
4087 + "funding": {
4088 + "url": "https://github.com/sponsors/ljharb"
4089 + }
4090 + },
4091 + "node_modules/functions-have-names": {
4092 + "version": "1.2.3",
4093 + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
4094 + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
4095 + "dev": true,
4096 + "license": "MIT",
4097 + "funding": {
4098 + "url": "https://github.com/sponsors/ljharb"
4099 + }
4100 + },
4101 + "node_modules/generator-function": {
4102 + "version": "2.0.1",
4103 + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
4104 + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
4105 + "dev": true,
4106 + "license": "MIT",
4107 + "engines": {
4108 + "node": ">= 0.4"
4109 + }
4110 + },
4111 + "node_modules/gensync": {
4112 + "version": "1.0.0-beta.2",
4113 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
4114 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
4115 + "dev": true,
4116 + "license": "MIT",
4117 + "engines": {
4118 + "node": ">=6.9.0"
4119 + }
4120 + },
4121 + "node_modules/get-intrinsic": {
4122 + "version": "1.3.0",
4123 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
4124 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
4125 + "dev": true,
4126 + "license": "MIT",
4127 + "dependencies": {
4128 + "call-bind-apply-helpers": "^1.0.2",
4129 + "es-define-property": "^1.0.1",
4130 + "es-errors": "^1.3.0",
4131 + "es-object-atoms": "^1.1.1",
4132 + "function-bind": "^1.1.2",
4133 + "get-proto": "^1.0.1",
4134 + "gopd": "^1.2.0",
4135 + "has-symbols": "^1.1.0",
4136 + "hasown": "^2.0.2",
4137 + "math-intrinsics": "^1.1.0"
4138 + },
4139 + "engines": {
4140 + "node": ">= 0.4"
4141 + },
4142 + "funding": {
4143 + "url": "https://github.com/sponsors/ljharb"
4144 + }
4145 + },
4146 + "node_modules/get-proto": {
4147 + "version": "1.0.1",
4148 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
4149 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
4150 + "dev": true,
4151 + "license": "MIT",
4152 + "dependencies": {
4153 + "dunder-proto": "^1.0.1",
4154 + "es-object-atoms": "^1.0.0"
4155 + },
4156 + "engines": {
4157 + "node": ">= 0.4"
4158 + }
4159 + },
4160 + "node_modules/get-symbol-description": {
4161 + "version": "1.1.0",
4162 + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
4163 + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
4164 + "dev": true,
4165 + "license": "MIT",
4166 + "dependencies": {
4167 + "call-bound": "^1.0.3",
4168 + "es-errors": "^1.3.0",
4169 + "get-intrinsic": "^1.2.6"
4170 + },
4171 + "engines": {
4172 + "node": ">= 0.4"
4173 + },
4174 + "funding": {
4175 + "url": "https://github.com/sponsors/ljharb"
4176 + }
4177 + },
4178 + "node_modules/get-tsconfig": {
4179 + "version": "4.14.2",
4180 + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.2.tgz",
4181 + "integrity": "sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==",
4182 + "dev": true,
4183 + "license": "MIT",
4184 + "dependencies": {
4185 + "resolve-pkg-maps": "^1.0.0"
4186 + },
4187 + "funding": {
4188 + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
4189 + }
4190 + },
4191 + "node_modules/glob-parent": {
4192 + "version": "6.0.2",
4193 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
4194 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
4195 + "dev": true,
4196 + "license": "ISC",
4197 + "dependencies": {
4198 + "is-glob": "^4.0.3"
4199 + },
4200 + "engines": {
4201 + "node": ">=10.13.0"
4202 + }
4203 + },
4204 + "node_modules/globals": {
4205 + "version": "14.0.0",
4206 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
4207 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
4208 + "dev": true,
4209 + "license": "MIT",
4210 + "engines": {
4211 + "node": ">=18"
4212 + },
4213 + "funding": {
4214 + "url": "https://github.com/sponsors/sindresorhus"
4215 + }
4216 + },
4217 + "node_modules/globalthis": {
4218 + "version": "1.0.4",
4219 + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
4220 + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
4221 + "dev": true,
4222 + "license": "MIT",
4223 + "dependencies": {
4224 + "define-properties": "^1.2.1",
4225 + "gopd": "^1.0.1"
4226 + },
4227 + "engines": {
4228 + "node": ">= 0.4"
4229 + },
4230 + "funding": {
4231 + "url": "https://github.com/sponsors/ljharb"
4232 + }
4233 + },
4234 + "node_modules/gopd": {
4235 + "version": "1.2.0",
4236 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
4237 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
4238 + "dev": true,
4239 + "license": "MIT",
4240 + "engines": {
4241 + "node": ">= 0.4"
4242 + },
4243 + "funding": {
4244 + "url": "https://github.com/sponsors/ljharb"
4245 + }
4246 + },
4247 + "node_modules/has-bigints": {
4248 + "version": "1.1.0",
4249 + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
4250 + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
4251 + "dev": true,
4252 + "license": "MIT",
4253 + "engines": {
4254 + "node": ">= 0.4"
4255 + },
4256 + "funding": {
4257 + "url": "https://github.com/sponsors/ljharb"
4258 + }
4259 + },
4260 + "node_modules/has-flag": {
4261 + "version": "4.0.0",
4262 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
4263 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
4264 + "dev": true,
4265 + "license": "MIT",
4266 + "engines": {
4267 + "node": ">=8"
4268 + }
4269 + },
4270 + "node_modules/has-property-descriptors": {
4271 + "version": "1.0.2",
4272 + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
4273 + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
4274 + "dev": true,
4275 + "license": "MIT",
4276 + "dependencies": {
4277 + "es-define-property": "^1.0.0"
4278 + },
4279 + "funding": {
4280 + "url": "https://github.com/sponsors/ljharb"
4281 + }
4282 + },
4283 + "node_modules/has-proto": {
4284 + "version": "1.2.0",
4285 + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
4286 + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
4287 + "dev": true,
4288 + "license": "MIT",
4289 + "dependencies": {
4290 + "dunder-proto": "^1.0.0"
4291 + },
4292 + "engines": {
4293 + "node": ">= 0.4"
4294 + },
4295 + "funding": {
4296 + "url": "https://github.com/sponsors/ljharb"
4297 + }
4298 + },
4299 + "node_modules/has-symbols": {
4300 + "version": "1.1.0",
4301 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
4302 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
4303 + "dev": true,
4304 + "license": "MIT",
4305 + "engines": {
4306 + "node": ">= 0.4"
4307 + },
4308 + "funding": {
4309 + "url": "https://github.com/sponsors/ljharb"
4310 + }
4311 + },
4312 + "node_modules/has-tostringtag": {
4313 + "version": "1.0.2",
4314 + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
4315 + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
4316 + "dev": true,
4317 + "license": "MIT",
4318 + "dependencies": {
4319 + "has-symbols": "^1.0.3"
4320 + },
4321 + "engines": {
4322 + "node": ">= 0.4"
4323 + },
4324 + "funding": {
4325 + "url": "https://github.com/sponsors/ljharb"
4326 + }
4327 + },
4328 + "node_modules/hasown": {
4329 + "version": "2.0.4",
4330 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
4331 + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
4332 + "dev": true,
4333 + "license": "MIT",
4334 + "dependencies": {
4335 + "function-bind": "^1.1.2"
4336 + },
4337 + "engines": {
4338 + "node": ">= 0.4"
4339 + }
4340 + },
4341 + "node_modules/hast-util-from-dom": {
4342 + "version": "5.0.1",
4343 + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
4344 + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
4345 + "license": "ISC",
4346 + "dependencies": {
4347 + "@types/hast": "^3.0.0",
4348 + "hastscript": "^9.0.0",
4349 + "web-namespaces": "^2.0.0"
4350 + },
4351 + "funding": {
4352 + "type": "opencollective",
4353 + "url": "https://opencollective.com/unified"
4354 + }
4355 + },
4356 + "node_modules/hast-util-from-html": {
4357 + "version": "2.0.3",
4358 + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
4359 + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
4360 + "license": "MIT",
4361 + "dependencies": {
4362 + "@types/hast": "^3.0.0",
4363 + "devlop": "^1.1.0",
4364 + "hast-util-from-parse5": "^8.0.0",
4365 + "parse5": "^7.0.0",
4366 + "vfile": "^6.0.0",
4367 + "vfile-message": "^4.0.0"
4368 + },
4369 + "funding": {
4370 + "type": "opencollective",
4371 + "url": "https://opencollective.com/unified"
4372 + }
4373 + },
4374 + "node_modules/hast-util-from-html-isomorphic": {
4375 + "version": "2.0.0",
4376 + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
4377 + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
4378 + "license": "MIT",
4379 + "dependencies": {
4380 + "@types/hast": "^3.0.0",
4381 + "hast-util-from-dom": "^5.0.0",
4382 + "hast-util-from-html": "^2.0.0",
4383 + "unist-util-remove-position": "^5.0.0"
4384 + },
4385 + "funding": {
4386 + "type": "opencollective",
4387 + "url": "https://opencollective.com/unified"
4388 + }
4389 + },
4390 + "node_modules/hast-util-from-parse5": {
4391 + "version": "8.0.3",
4392 + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
4393 + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
4394 + "license": "MIT",
4395 + "dependencies": {
4396 + "@types/hast": "^3.0.0",
4397 + "@types/unist": "^3.0.0",
4398 + "devlop": "^1.0.0",
4399 + "hastscript": "^9.0.0",
4400 + "property-information": "^7.0.0",
4401 + "vfile": "^6.0.0",
4402 + "vfile-location": "^5.0.0",
4403 + "web-namespaces": "^2.0.0"
4404 + },
4405 + "funding": {
4406 + "type": "opencollective",
4407 + "url": "https://opencollective.com/unified"
4408 + }
4409 + },
4410 + "node_modules/hast-util-is-element": {
4411 + "version": "3.0.0",
4412 + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
4413 + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
4414 + "license": "MIT",
4415 + "dependencies": {
4416 + "@types/hast": "^3.0.0"
4417 + },
4418 + "funding": {
4419 + "type": "opencollective",
4420 + "url": "https://opencollective.com/unified"
4421 + }
4422 + },
4423 + "node_modules/hast-util-parse-selector": {
4424 + "version": "4.0.0",
4425 + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
4426 + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
4427 + "license": "MIT",
4428 + "dependencies": {
4429 + "@types/hast": "^3.0.0"
4430 + },
4431 + "funding": {
4432 + "type": "opencollective",
4433 + "url": "https://opencollective.com/unified"
4434 + }
4435 + },
4436 + "node_modules/hast-util-to-jsx-runtime": {
4437 + "version": "2.3.6",
4438 + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
4439 + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
4440 + "license": "MIT",
4441 + "dependencies": {
4442 + "@types/estree": "^1.0.0",
4443 + "@types/hast": "^3.0.0",
4444 + "@types/unist": "^3.0.0",
4445 + "comma-separated-tokens": "^2.0.0",
4446 + "devlop": "^1.0.0",
4447 + "estree-util-is-identifier-name": "^3.0.0",
4448 + "hast-util-whitespace": "^3.0.0",
4449 + "mdast-util-mdx-expression": "^2.0.0",
4450 + "mdast-util-mdx-jsx": "^3.0.0",
4451 + "mdast-util-mdxjs-esm": "^2.0.0",
4452 + "property-information": "^7.0.0",
4453 + "space-separated-tokens": "^2.0.0",
4454 + "style-to-js": "^1.0.0",
4455 + "unist-util-position": "^5.0.0",
4456 + "vfile-message": "^4.0.0"
4457 + },
4458 + "funding": {
4459 + "type": "opencollective",
4460 + "url": "https://opencollective.com/unified"
4461 + }
4462 + },
4463 + "node_modules/hast-util-to-text": {
4464 + "version": "4.0.2",
4465 + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
4466 + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
4467 + "license": "MIT",
4468 + "dependencies": {
4469 + "@types/hast": "^3.0.0",
4470 + "@types/unist": "^3.0.0",
4471 + "hast-util-is-element": "^3.0.0",
4472 + "unist-util-find-after": "^5.0.0"
4473 + },
4474 + "funding": {
4475 + "type": "opencollective",
4476 + "url": "https://opencollective.com/unified"
4477 + }
4478 + },
4479 + "node_modules/hast-util-whitespace": {
4480 + "version": "3.0.0",
4481 + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
4482 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
4483 + "license": "MIT",
4484 + "dependencies": {
4485 + "@types/hast": "^3.0.0"
4486 + },
4487 + "funding": {
4488 + "type": "opencollective",
4489 + "url": "https://opencollective.com/unified"
4490 + }
4491 + },
4492 + "node_modules/hastscript": {
4493 + "version": "9.0.1",
4494 + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
4495 + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
4496 + "license": "MIT",
4497 + "dependencies": {
4498 + "@types/hast": "^3.0.0",
4499 + "comma-separated-tokens": "^2.0.0",
4500 + "hast-util-parse-selector": "^4.0.0",
4501 + "property-information": "^7.0.0",
4502 + "space-separated-tokens": "^2.0.0"
4503 + },
4504 + "funding": {
4505 + "type": "opencollective",
4506 + "url": "https://opencollective.com/unified"
4507 + }
4508 + },
4509 + "node_modules/hermes-estree": {
4510 + "version": "0.25.1",
4511 + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
4512 + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
4513 + "dev": true,
4514 + "license": "MIT"
4515 + },
4516 + "node_modules/hermes-parser": {
4517 + "version": "0.25.1",
4518 + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
4519 + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
4520 + "dev": true,
4521 + "license": "MIT",
4522 + "dependencies": {
4523 + "hermes-estree": "0.25.1"
4524 + }
4525 + },
4526 + "node_modules/highlight.js": {
4527 + "version": "11.12.0",
4528 + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz",
4529 + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==",
4530 + "license": "BSD-3-Clause",
4531 + "engines": {
4532 + "node": ">=12.0.0"
4533 + }
4534 + },
4535 + "node_modules/html-url-attributes": {
4536 + "version": "3.0.1",
4537 + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
4538 + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
4539 + "license": "MIT",
4540 + "funding": {
4541 + "type": "opencollective",
4542 + "url": "https://opencollective.com/unified"
4543 + }
4544 + },
4545 + "node_modules/ignore": {
4546 + "version": "5.3.2",
4547 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
4548 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
4549 + "dev": true,
4550 + "license": "MIT",
4551 + "engines": {
4552 + "node": ">= 4"
4553 + }
4554 + },
4555 + "node_modules/import-fresh": {
4556 + "version": "3.3.1",
4557 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
4558 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
4559 + "dev": true,
4560 + "license": "MIT",
4561 + "dependencies": {
4562 + "parent-module": "^1.0.0",
4563 + "resolve-from": "^4.0.0"
4564 + },
4565 + "engines": {
4566 + "node": ">=6"
4567 + },
4568 + "funding": {
4569 + "url": "https://github.com/sponsors/sindresorhus"
4570 + }
4571 + },
4572 + "node_modules/imurmurhash": {
4573 + "version": "0.1.4",
4574 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
4575 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
4576 + "dev": true,
4577 + "license": "MIT",
4578 + "engines": {
4579 + "node": ">=0.8.19"
4580 + }
4581 + },
4582 + "node_modules/inline-style-parser": {
4583 + "version": "0.2.7",
4584 + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
4585 + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
4586 + "license": "MIT"
4587 + },
4588 + "node_modules/internal-slot": {
4589 + "version": "1.1.0",
4590 + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
4591 + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
4592 + "dev": true,
4593 + "license": "MIT",
4594 + "dependencies": {
4595 + "es-errors": "^1.3.0",
4596 + "hasown": "^2.0.2",
4597 + "side-channel": "^1.1.0"
4598 + },
4599 + "engines": {
4600 + "node": ">= 0.4"
4601 + }
4602 + },
4603 + "node_modules/is-alphabetical": {
4604 + "version": "2.0.1",
4605 + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
4606 + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
4607 + "license": "MIT",
4608 + "funding": {
4609 + "type": "github",
4610 + "url": "https://github.com/sponsors/wooorm"
4611 + }
4612 + },
4613 + "node_modules/is-alphanumerical": {
4614 + "version": "2.0.1",
4615 + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
4616 + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
4617 + "license": "MIT",
4618 + "dependencies": {
4619 + "is-alphabetical": "^2.0.0",
4620 + "is-decimal": "^2.0.0"
4621 + },
4622 + "funding": {
4623 + "type": "github",
4624 + "url": "https://github.com/sponsors/wooorm"
4625 + }
4626 + },
4627 + "node_modules/is-array-buffer": {
4628 + "version": "3.0.5",
4629 + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
4630 + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
4631 + "dev": true,
4632 + "license": "MIT",
4633 + "dependencies": {
4634 + "call-bind": "^1.0.8",
4635 + "call-bound": "^1.0.3",
4636 + "get-intrinsic": "^1.2.6"
4637 + },
4638 + "engines": {
4639 + "node": ">= 0.4"
4640 + },
4641 + "funding": {
4642 + "url": "https://github.com/sponsors/ljharb"
4643 + }
4644 + },
4645 + "node_modules/is-async-function": {
4646 + "version": "2.1.1",
4647 + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
4648 + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
4649 + "dev": true,
4650 + "license": "MIT",
4651 + "dependencies": {
4652 + "async-function": "^1.0.0",
4653 + "call-bound": "^1.0.3",
4654 + "get-proto": "^1.0.1",
4655 + "has-tostringtag": "^1.0.2",
4656 + "safe-regex-test": "^1.1.0"
4657 + },
4658 + "engines": {
4659 + "node": ">= 0.4"
4660 + },
4661 + "funding": {
4662 + "url": "https://github.com/sponsors/ljharb"
4663 + }
4664 + },
4665 + "node_modules/is-bigint": {
4666 + "version": "1.1.0",
4667 + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
4668 + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
4669 + "dev": true,
4670 + "license": "MIT",
4671 + "dependencies": {
4672 + "has-bigints": "^1.0.2"
4673 + },
4674 + "engines": {
4675 + "node": ">= 0.4"
4676 + },
4677 + "funding": {
4678 + "url": "https://github.com/sponsors/ljharb"
4679 + }
4680 + },
4681 + "node_modules/is-boolean-object": {
4682 + "version": "1.2.2",
4683 + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
4684 + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
4685 + "dev": true,
4686 + "license": "MIT",
4687 + "dependencies": {
4688 + "call-bound": "^1.0.3",
4689 + "has-tostringtag": "^1.0.2"
4690 + },
4691 + "engines": {
4692 + "node": ">= 0.4"
4693 + },
4694 + "funding": {
4695 + "url": "https://github.com/sponsors/ljharb"
4696 + }
4697 + },
4698 + "node_modules/is-bun-module": {
4699 + "version": "2.0.0",
4700 + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
4701 + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
4702 + "dev": true,
4703 + "license": "MIT",
4704 + "dependencies": {
4705 + "semver": "^7.7.1"
4706 + }
4707 + },
4708 + "node_modules/is-bun-module/node_modules/semver": {
4709 + "version": "7.8.5",
4710 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
4711 + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
4712 + "dev": true,
4713 + "license": "ISC",
4714 + "bin": {
4715 + "semver": "bin/semver.js"
4716 + },
4717 + "engines": {
4718 + "node": ">=10"
4719 + }
4720 + },
4721 + "node_modules/is-callable": {
4722 + "version": "1.2.7",
4723 + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
4724 + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
4725 + "dev": true,
4726 + "license": "MIT",
4727 + "engines": {
4728 + "node": ">= 0.4"
4729 + },
4730 + "funding": {
4731 + "url": "https://github.com/sponsors/ljharb"
4732 + }
4733 + },
4734 + "node_modules/is-core-module": {
4735 + "version": "2.16.2",
4736 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
4737 + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
4738 + "dev": true,
4739 + "license": "MIT",
4740 + "dependencies": {
4741 + "hasown": "^2.0.3"
4742 + },
4743 + "engines": {
4744 + "node": ">= 0.4"
4745 + },
4746 + "funding": {
4747 + "url": "https://github.com/sponsors/ljharb"
4748 + }
4749 + },
4750 + "node_modules/is-data-view": {
4751 + "version": "1.0.2",
4752 + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
4753 + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
4754 + "dev": true,
4755 + "license": "MIT",
4756 + "dependencies": {
4757 + "call-bound": "^1.0.2",
4758 + "get-intrinsic": "^1.2.6",
4759 + "is-typed-array": "^1.1.13"
4760 + },
4761 + "engines": {
4762 + "node": ">= 0.4"
4763 + },
4764 + "funding": {
4765 + "url": "https://github.com/sponsors/ljharb"
4766 + }
4767 + },
4768 + "node_modules/is-date-object": {
4769 + "version": "1.1.0",
4770 + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
4771 + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
4772 + "dev": true,
4773 + "license": "MIT",
4774 + "dependencies": {
4775 + "call-bound": "^1.0.2",
4776 + "has-tostringtag": "^1.0.2"
4777 + },
4778 + "engines": {
4779 + "node": ">= 0.4"
4780 + },
4781 + "funding": {
4782 + "url": "https://github.com/sponsors/ljharb"
4783 + }
4784 + },
4785 + "node_modules/is-decimal": {
4786 + "version": "2.0.1",
4787 + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
4788 + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
4789 + "license": "MIT",
4790 + "funding": {
4791 + "type": "github",
4792 + "url": "https://github.com/sponsors/wooorm"
4793 + }
4794 + },
4795 + "node_modules/is-document.all": {
4796 + "version": "1.0.0",
4797 + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz",
4798 + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
4799 + "dev": true,
4800 + "license": "MIT",
4801 + "dependencies": {
4802 + "call-bound": "^1.0.4"
4803 + },
4804 + "engines": {
4805 + "node": ">= 0.4"
4806 + },
4807 + "funding": {
4808 + "url": "https://github.com/sponsors/ljharb"
4809 + }
4810 + },
4811 + "node_modules/is-extglob": {
4812 + "version": "2.1.1",
4813 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
4814 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
4815 + "dev": true,
4816 + "license": "MIT",
4817 + "engines": {
4818 + "node": ">=0.10.0"
4819 + }
4820 + },
4821 + "node_modules/is-finalizationregistry": {
4822 + "version": "1.1.1",
4823 + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
4824 + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
4825 + "dev": true,
4826 + "license": "MIT",
4827 + "dependencies": {
4828 + "call-bound": "^1.0.3"
4829 + },
4830 + "engines": {
4831 + "node": ">= 0.4"
4832 + },
4833 + "funding": {
4834 + "url": "https://github.com/sponsors/ljharb"
4835 + }
4836 + },
4837 + "node_modules/is-generator-function": {
4838 + "version": "1.1.2",
4839 + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
4840 + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
4841 + "dev": true,
4842 + "license": "MIT",
4843 + "dependencies": {
4844 + "call-bound": "^1.0.4",
4845 + "generator-function": "^2.0.0",
4846 + "get-proto": "^1.0.1",
4847 + "has-tostringtag": "^1.0.2",
4848 + "safe-regex-test": "^1.1.0"
4849 + },
4850 + "engines": {
4851 + "node": ">= 0.4"
4852 + },
4853 + "funding": {
4854 + "url": "https://github.com/sponsors/ljharb"
4855 + }
4856 + },
4857 + "node_modules/is-glob": {
4858 + "version": "4.0.3",
4859 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
4860 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
4861 + "dev": true,
4862 + "license": "MIT",
4863 + "dependencies": {
4864 + "is-extglob": "^2.1.1"
4865 + },
4866 + "engines": {
4867 + "node": ">=0.10.0"
4868 + }
4869 + },
4870 + "node_modules/is-hexadecimal": {
4871 + "version": "2.0.1",
4872 + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
4873 + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
4874 + "license": "MIT",
4875 + "funding": {
4876 + "type": "github",
4877 + "url": "https://github.com/sponsors/wooorm"
4878 + }
4879 + },
4880 + "node_modules/is-map": {
4881 + "version": "2.0.3",
4882 + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
4883 + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
4884 + "dev": true,
4885 + "license": "MIT",
4886 + "engines": {
4887 + "node": ">= 0.4"
4888 + },
4889 + "funding": {
4890 + "url": "https://github.com/sponsors/ljharb"
4891 + }
4892 + },
4893 + "node_modules/is-negative-zero": {
4894 + "version": "2.0.3",
4895 + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
4896 + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
4897 + "dev": true,
4898 + "license": "MIT",
4899 + "engines": {
4900 + "node": ">= 0.4"
4901 + },
4902 + "funding": {
4903 + "url": "https://github.com/sponsors/ljharb"
4904 + }
4905 + },
4906 + "node_modules/is-number": {
4907 + "version": "7.0.0",
4908 + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
4909 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
4910 + "dev": true,
4911 + "license": "MIT",
4912 + "engines": {
4913 + "node": ">=0.12.0"
4914 + }
4915 + },
4916 + "node_modules/is-number-object": {
4917 + "version": "1.1.1",
4918 + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
4919 + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
4920 + "dev": true,
4921 + "license": "MIT",
4922 + "dependencies": {
4923 + "call-bound": "^1.0.3",
4924 + "has-tostringtag": "^1.0.2"
4925 + },
4926 + "engines": {
4927 + "node": ">= 0.4"
4928 + },
4929 + "funding": {
4930 + "url": "https://github.com/sponsors/ljharb"
4931 + }
4932 + },
4933 + "node_modules/is-plain-obj": {
4934 + "version": "4.1.0",
4935 + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
4936 + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
4937 + "license": "MIT",
4938 + "engines": {
4939 + "node": ">=12"
4940 + },
4941 + "funding": {
4942 + "url": "https://github.com/sponsors/sindresorhus"
4943 + }
4944 + },
4945 + "node_modules/is-regex": {
4946 + "version": "1.2.1",
4947 + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
4948 + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
4949 + "dev": true,
4950 + "license": "MIT",
4951 + "dependencies": {
4952 + "call-bound": "^1.0.2",
4953 + "gopd": "^1.2.0",
4954 + "has-tostringtag": "^1.0.2",
4955 + "hasown": "^2.0.2"
4956 + },
4957 + "engines": {
4958 + "node": ">= 0.4"
4959 + },
4960 + "funding": {
4961 + "url": "https://github.com/sponsors/ljharb"
4962 + }
4963 + },
4964 + "node_modules/is-set": {
4965 + "version": "2.0.3",
4966 + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
4967 + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
4968 + "dev": true,
4969 + "license": "MIT",
4970 + "engines": {
4971 + "node": ">= 0.4"
4972 + },
4973 + "funding": {
4974 + "url": "https://github.com/sponsors/ljharb"
4975 + }
4976 + },
4977 + "node_modules/is-shared-array-buffer": {
4978 + "version": "1.0.4",
4979 + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
4980 + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
4981 + "dev": true,
4982 + "license": "MIT",
4983 + "dependencies": {
4984 + "call-bound": "^1.0.3"
4985 + },
4986 + "engines": {
4987 + "node": ">= 0.4"
4988 + },
4989 + "funding": {
4990 + "url": "https://github.com/sponsors/ljharb"
4991 + }
4992 + },
4993 + "node_modules/is-string": {
4994 + "version": "1.1.1",
4995 + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
4996 + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
4997 + "dev": true,
4998 + "license": "MIT",
4999 + "dependencies": {
5000 + "call-bound": "^1.0.3",
5001 + "has-tostringtag": "^1.0.2"
5002 + },
5003 + "engines": {
5004 + "node": ">= 0.4"
5005 + },
5006 + "funding": {
5007 + "url": "https://github.com/sponsors/ljharb"
5008 + }
5009 + },
5010 + "node_modules/is-symbol": {
5011 + "version": "1.1.1",
5012 + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
5013 + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
5014 + "dev": true,
5015 + "license": "MIT",
5016 + "dependencies": {
5017 + "call-bound": "^1.0.2",
5018 + "has-symbols": "^1.1.0",
5019 + "safe-regex-test": "^1.1.0"
5020 + },
5021 + "engines": {
5022 + "node": ">= 0.4"
5023 + },
5024 + "funding": {
5025 + "url": "https://github.com/sponsors/ljharb"
5026 + }
5027 + },
5028 + "node_modules/is-typed-array": {
5029 + "version": "1.1.15",
5030 + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
5031 + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
5032 + "dev": true,
5033 + "license": "MIT",
5034 + "dependencies": {
5035 + "which-typed-array": "^1.1.16"
5036 + },
5037 + "engines": {
5038 + "node": ">= 0.4"
5039 + },
5040 + "funding": {
5041 + "url": "https://github.com/sponsors/ljharb"
5042 + }
5043 + },
5044 + "node_modules/is-weakmap": {
5045 + "version": "2.0.2",
5046 + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
5047 + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
5048 + "dev": true,
5049 + "license": "MIT",
5050 + "engines": {
5051 + "node": ">= 0.4"
5052 + },
5053 + "funding": {
5054 + "url": "https://github.com/sponsors/ljharb"
5055 + }
5056 + },
5057 + "node_modules/is-weakref": {
5058 + "version": "1.1.1",
5059 + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
5060 + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
5061 + "dev": true,
5062 + "license": "MIT",
5063 + "dependencies": {
5064 + "call-bound": "^1.0.3"
5065 + },
5066 + "engines": {
5067 + "node": ">= 0.4"
5068 + },
5069 + "funding": {
5070 + "url": "https://github.com/sponsors/ljharb"
5071 + }
5072 + },
5073 + "node_modules/is-weakset": {
5074 + "version": "2.0.4",
5075 + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
5076 + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
5077 + "dev": true,
5078 + "license": "MIT",
5079 + "dependencies": {
5080 + "call-bound": "^1.0.3",
5081 + "get-intrinsic": "^1.2.6"
5082 + },
5083 + "engines": {
5084 + "node": ">= 0.4"
5085 + },
5086 + "funding": {
5087 + "url": "https://github.com/sponsors/ljharb"
5088 + }
5089 + },
5090 + "node_modules/isarray": {
5091 + "version": "2.0.5",
5092 + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
5093 + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
5094 + "dev": true,
5095 + "license": "MIT"
5096 + },
5097 + "node_modules/isexe": {
5098 + "version": "2.0.0",
5099 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
5100 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
5101 + "dev": true,
5102 + "license": "ISC"
5103 + },
5104 + "node_modules/iterator.prototype": {
5105 + "version": "1.1.5",
5106 + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
5107 + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
5108 + "dev": true,
5109 + "license": "MIT",
5110 + "dependencies": {
5111 + "define-data-property": "^1.1.4",
5112 + "es-object-atoms": "^1.0.0",
5113 + "get-intrinsic": "^1.2.6",
5114 + "get-proto": "^1.0.0",
5115 + "has-symbols": "^1.1.0",
5116 + "set-function-name": "^2.0.2"
5117 + },
5118 + "engines": {
5119 + "node": ">= 0.4"
5120 + }
5121 + },
5122 + "node_modules/js-tokens": {
5123 + "version": "4.0.0",
5124 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
5125 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
5126 + "dev": true,
5127 + "license": "MIT"
5128 + },
5129 + "node_modules/js-yaml": {
5130 + "version": "4.3.1",
5131 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
5132 + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
5133 + "dev": true,
5134 + "funding": [
5135 + {
5136 + "type": "github",
5137 + "url": "https://github.com/sponsors/puzrin"
5138 + },
5139 + {
5140 + "type": "github",
5141 + "url": "https://github.com/sponsors/nodeca"
5142 + }
5143 + ],
5144 + "license": "MIT",
5145 + "dependencies": {
5146 + "argparse": "^2.0.1"
5147 + },
5148 + "bin": {
5149 + "js-yaml": "bin/js-yaml.js"
5150 + }
5151 + },
5152 + "node_modules/jsesc": {
5153 + "version": "3.1.0",
5154 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
5155 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
5156 + "dev": true,
5157 + "license": "MIT",
5158 + "bin": {
5159 + "jsesc": "bin/jsesc"
5160 + },
5161 + "engines": {
5162 + "node": ">=6"
5163 + }
5164 + },
5165 + "node_modules/json-buffer": {
5166 + "version": "3.0.1",
5167 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
5168 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
5169 + "dev": true,
5170 + "license": "MIT"
5171 + },
5172 + "node_modules/json-schema-traverse": {
5173 + "version": "0.4.1",
5174 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
5175 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
5176 + "dev": true,
5177 + "license": "MIT"
5178 + },
5179 + "node_modules/json-stable-stringify-without-jsonify": {
5180 + "version": "1.0.1",
5181 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
5182 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
5183 + "dev": true,
5184 + "license": "MIT"
5185 + },
5186 + "node_modules/json5": {
5187 + "version": "2.2.3",
5188 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
5189 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
5190 + "dev": true,
5191 + "license": "MIT",
5192 + "bin": {
5193 + "json5": "lib/cli.js"
5194 + },
5195 + "engines": {
5196 + "node": ">=6"
5197 + }
5198 + },
5199 + "node_modules/jsx-ast-utils": {
5200 + "version": "3.3.5",
5201 + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
5202 + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
5203 + "dev": true,
5204 + "license": "MIT",
5205 + "dependencies": {
5206 + "array-includes": "^3.1.6",
5207 + "array.prototype.flat": "^1.3.1",
5208 + "object.assign": "^4.1.4",
5209 + "object.values": "^1.1.6"
5210 + },
5211 + "engines": {
5212 + "node": ">=4.0"
5213 + }
5214 + },
5215 + "node_modules/katex": {
5216 + "version": "0.18.4",
5217 + "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.4.tgz",
5218 + "integrity": "sha512-IMPntbRLOU+eu88XDiFKqQ8Akhr9Tv7jDMXqPhjG9SI1JMA4DIgXk4x9k4skJz2NZJXBRbC+2pYBLj9olqcZow==",
5219 + "funding": [
5220 + "https://opencollective.com/katex",
5221 + "https://github.com/sponsors/katex"
5222 + ],
5223 + "license": "MIT",
5224 + "dependencies": {
5225 + "commander": "^8.3.0"
5226 + },
5227 + "bin": {
5228 + "katex": "cli.js"
5229 + }
5230 + },
5231 + "node_modules/keyv": {
5232 + "version": "4.5.4",
5233 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
5234 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
5235 + "dev": true,
5236 + "license": "MIT",
5237 + "dependencies": {
5238 + "json-buffer": "3.0.1"
5239 + }
5240 + },
5241 + "node_modules/language-subtag-registry": {
5242 + "version": "0.3.23",
5243 + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
5244 + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
5245 + "dev": true,
5246 + "license": "CC0-1.0"
5247 + },
5248 + "node_modules/language-tags": {
5249 + "version": "1.0.9",
5250 + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
5251 + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
5252 + "dev": true,
5253 + "license": "MIT",
5254 + "dependencies": {
5255 + "language-subtag-registry": "^0.3.20"
5256 + },
5257 + "engines": {
5258 + "node": ">=0.10"
5259 + }
5260 + },
5261 + "node_modules/levn": {
5262 + "version": "0.4.1",
5263 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
5264 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
5265 + "dev": true,
5266 + "license": "MIT",
5267 + "dependencies": {
5268 + "prelude-ls": "^1.2.1",
5269 + "type-check": "~0.4.0"
5270 + },
5271 + "engines": {
5272 + "node": ">= 0.8.0"
5273 + }
5274 + },
5275 + "node_modules/locate-path": {
5276 + "version": "6.0.0",
5277 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
5278 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
5279 + "dev": true,
5280 + "license": "MIT",
5281 + "dependencies": {
5282 + "p-locate": "^5.0.0"
5283 + },
5284 + "engines": {
5285 + "node": ">=10"
5286 + },
5287 + "funding": {
5288 + "url": "https://github.com/sponsors/sindresorhus"
5289 + }
5290 + },
5291 + "node_modules/lodash.merge": {
5292 + "version": "4.6.2",
5293 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
5294 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
5295 + "dev": true,
5296 + "license": "MIT"
5297 + },
5298 + "node_modules/longest-streak": {
5299 + "version": "3.1.0",
5300 + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
5301 + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
5302 + "license": "MIT",
5303 + "funding": {
5304 + "type": "github",
5305 + "url": "https://github.com/sponsors/wooorm"
5306 + }
5307 + },
5308 + "node_modules/loose-envify": {
5309 + "version": "1.4.0",
5310 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
5311 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
5312 + "dev": true,
5313 + "license": "MIT",
5314 + "dependencies": {
5315 + "js-tokens": "^3.0.0 || ^4.0.0"
5316 + },
5317 + "bin": {
5318 + "loose-envify": "cli.js"
5319 + }
5320 + },
5321 + "node_modules/lowlight": {
5322 + "version": "3.3.0",
5323 + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
5324 + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
5325 + "license": "MIT",
5326 + "dependencies": {
5327 + "@types/hast": "^3.0.0",
5328 + "devlop": "^1.0.0",
5329 + "highlight.js": "~11.11.0"
5330 + },
5331 + "funding": {
5332 + "type": "github",
5333 + "url": "https://github.com/sponsors/wooorm"
5334 + }
5335 + },
5336 + "node_modules/lowlight/node_modules/highlight.js": {
5337 + "version": "11.11.2",
5338 + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.2.tgz",
5339 + "integrity": "sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ==",
5340 + "license": "BSD-3-Clause",
5341 + "engines": {
5342 + "node": ">=12.0.0"
5343 + }
5344 + },
5345 + "node_modules/lru-cache": {
5346 + "version": "5.1.1",
5347 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
5348 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
5349 + "dev": true,
5350 + "license": "ISC",
5351 + "dependencies": {
5352 + "yallist": "^3.0.2"
5353 + }
5354 + },
5355 + "node_modules/markdown-table": {
5356 + "version": "3.0.4",
5357 + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
5358 + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
5359 + "license": "MIT",
5360 + "funding": {
5361 + "type": "github",
5362 + "url": "https://github.com/sponsors/wooorm"
5363 + }
5364 + },
5365 + "node_modules/math-intrinsics": {
5366 + "version": "1.1.0",
5367 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
5368 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
5369 + "dev": true,
5370 + "license": "MIT",
5371 + "engines": {
5372 + "node": ">= 0.4"
5373 + }
5374 + },
5375 + "node_modules/mdast-util-find-and-replace": {
5376 + "version": "3.0.2",
5377 + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
5378 + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
5379 + "license": "MIT",
5380 + "dependencies": {
5381 + "@types/mdast": "^4.0.0",
5382 + "escape-string-regexp": "^5.0.0",
5383 + "unist-util-is": "^6.0.0",
5384 + "unist-util-visit-parents": "^6.0.0"
5385 + },
5386 + "funding": {
5387 + "type": "opencollective",
5388 + "url": "https://opencollective.com/unified"
5389 + }
5390 + },
5391 + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
5392 + "version": "5.0.0",
5393 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
5394 + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
5395 + "license": "MIT",
5396 + "engines": {
5397 + "node": ">=12"
5398 + },
5399 + "funding": {
5400 + "url": "https://github.com/sponsors/sindresorhus"
5401 + }
5402 + },
5403 + "node_modules/mdast-util-from-markdown": {
5404 + "version": "2.0.3",
5405 + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
5406 + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
5407 + "license": "MIT",
5408 + "dependencies": {
5409 + "@types/mdast": "^4.0.0",
5410 + "@types/unist": "^3.0.0",
5411 + "decode-named-character-reference": "^1.0.0",
5412 + "devlop": "^1.0.0",
5413 + "mdast-util-to-string": "^4.0.0",
5414 + "micromark": "^4.0.0",
5415 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
5416 + "micromark-util-decode-string": "^2.0.0",
5417 + "micromark-util-normalize-identifier": "^2.0.0",
5418 + "micromark-util-symbol": "^2.0.0",
5419 + "micromark-util-types": "^2.0.0",
5420 + "unist-util-stringify-position": "^4.0.0"
5421 + },
5422 + "funding": {
5423 + "type": "opencollective",
5424 + "url": "https://opencollective.com/unified"
5425 + }
5426 + },
5427 + "node_modules/mdast-util-gfm": {
5428 + "version": "3.1.0",
5429 + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
5430 + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
5431 + "license": "MIT",
5432 + "dependencies": {
5433 + "mdast-util-from-markdown": "^2.0.0",
5434 + "mdast-util-gfm-autolink-literal": "^2.0.0",
5435 + "mdast-util-gfm-footnote": "^2.0.0",
5436 + "mdast-util-gfm-strikethrough": "^2.0.0",
5437 + "mdast-util-gfm-table": "^2.0.0",
5438 + "mdast-util-gfm-task-list-item": "^2.0.0",
5439 + "mdast-util-to-markdown": "^2.0.0"
5440 + },
5441 + "funding": {
5442 + "type": "opencollective",
5443 + "url": "https://opencollective.com/unified"
5444 + }
5445 + },
5446 + "node_modules/mdast-util-gfm-autolink-literal": {
5447 + "version": "2.0.1",
5448 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
5449 + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
5450 + "license": "MIT",
5451 + "dependencies": {
5452 + "@types/mdast": "^4.0.0",
5453 + "ccount": "^2.0.0",
5454 + "devlop": "^1.0.0",
5455 + "mdast-util-find-and-replace": "^3.0.0",
5456 + "micromark-util-character": "^2.0.0"
5457 + },
5458 + "funding": {
5459 + "type": "opencollective",
5460 + "url": "https://opencollective.com/unified"
5461 + }
5462 + },
5463 + "node_modules/mdast-util-gfm-footnote": {
5464 + "version": "2.1.0",
5465 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
5466 + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
5467 + "license": "MIT",
5468 + "dependencies": {
5469 + "@types/mdast": "^4.0.0",
5470 + "devlop": "^1.1.0",
5471 + "mdast-util-from-markdown": "^2.0.0",
5472 + "mdast-util-to-markdown": "^2.0.0",
5473 + "micromark-util-normalize-identifier": "^2.0.0"
5474 + },
5475 + "funding": {
5476 + "type": "opencollective",
5477 + "url": "https://opencollective.com/unified"
5478 + }
5479 + },
5480 + "node_modules/mdast-util-gfm-strikethrough": {
5481 + "version": "2.0.0",
5482 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
5483 + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
5484 + "license": "MIT",
5485 + "dependencies": {
5486 + "@types/mdast": "^4.0.0",
5487 + "mdast-util-from-markdown": "^2.0.0",
5488 + "mdast-util-to-markdown": "^2.0.0"
5489 + },
5490 + "funding": {
5491 + "type": "opencollective",
5492 + "url": "https://opencollective.com/unified"
5493 + }
5494 + },
5495 + "node_modules/mdast-util-gfm-table": {
5496 + "version": "2.0.0",
5497 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
5498 + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
5499 + "license": "MIT",
5500 + "dependencies": {
5501 + "@types/mdast": "^4.0.0",
5502 + "devlop": "^1.0.0",
5503 + "markdown-table": "^3.0.0",
5504 + "mdast-util-from-markdown": "^2.0.0",
5505 + "mdast-util-to-markdown": "^2.0.0"
5506 + },
5507 + "funding": {
5508 + "type": "opencollective",
5509 + "url": "https://opencollective.com/unified"
5510 + }
5511 + },
5512 + "node_modules/mdast-util-gfm-task-list-item": {
5513 + "version": "2.0.0",
5514 + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
5515 + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
5516 + "license": "MIT",
5517 + "dependencies": {
5518 + "@types/mdast": "^4.0.0",
5519 + "devlop": "^1.0.0",
5520 + "mdast-util-from-markdown": "^2.0.0",
5521 + "mdast-util-to-markdown": "^2.0.0"
5522 + },
5523 + "funding": {
5524 + "type": "opencollective",
5525 + "url": "https://opencollective.com/unified"
5526 + }
5527 + },
5528 + "node_modules/mdast-util-math": {
5529 + "version": "3.0.0",
5530 + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz",
5531 + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==",
5532 + "license": "MIT",
5533 + "dependencies": {
5534 + "@types/hast": "^3.0.0",
5535 + "@types/mdast": "^4.0.0",
5536 + "devlop": "^1.0.0",
5537 + "longest-streak": "^3.0.0",
5538 + "mdast-util-from-markdown": "^2.0.0",
5539 + "mdast-util-to-markdown": "^2.1.0",
5540 + "unist-util-remove-position": "^5.0.0"
5541 + },
5542 + "funding": {
5543 + "type": "opencollective",
5544 + "url": "https://opencollective.com/unified"
5545 + }
5546 + },
5547 + "node_modules/mdast-util-mdx-expression": {
5548 + "version": "2.0.1",
5549 + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
5550 + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
5551 + "license": "MIT",
5552 + "dependencies": {
5553 + "@types/estree-jsx": "^1.0.0",
5554 + "@types/hast": "^3.0.0",
5555 + "@types/mdast": "^4.0.0",
5556 + "devlop": "^1.0.0",
5557 + "mdast-util-from-markdown": "^2.0.0",
5558 + "mdast-util-to-markdown": "^2.0.0"
5559 + },
5560 + "funding": {
5561 + "type": "opencollective",
5562 + "url": "https://opencollective.com/unified"
5563 + }
5564 + },
5565 + "node_modules/mdast-util-mdx-jsx": {
5566 + "version": "3.2.0",
5567 + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
5568 + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
5569 + "license": "MIT",
5570 + "dependencies": {
5571 + "@types/estree-jsx": "^1.0.0",
5572 + "@types/hast": "^3.0.0",
5573 + "@types/mdast": "^4.0.0",
5574 + "@types/unist": "^3.0.0",
5575 + "ccount": "^2.0.0",
5576 + "devlop": "^1.1.0",
5577 + "mdast-util-from-markdown": "^2.0.0",
5578 + "mdast-util-to-markdown": "^2.0.0",
5579 + "parse-entities": "^4.0.0",
5580 + "stringify-entities": "^4.0.0",
5581 + "unist-util-stringify-position": "^4.0.0",
5582 + "vfile-message": "^4.0.0"
5583 + },
5584 + "funding": {
5585 + "type": "opencollective",
5586 + "url": "https://opencollective.com/unified"
5587 + }
5588 + },
5589 + "node_modules/mdast-util-mdxjs-esm": {
5590 + "version": "2.0.1",
5591 + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
5592 + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
5593 + "license": "MIT",
5594 + "dependencies": {
5595 + "@types/estree-jsx": "^1.0.0",
5596 + "@types/hast": "^3.0.0",
5597 + "@types/mdast": "^4.0.0",
5598 + "devlop": "^1.0.0",
5599 + "mdast-util-from-markdown": "^2.0.0",
5600 + "mdast-util-to-markdown": "^2.0.0"
5601 + },
5602 + "funding": {
5603 + "type": "opencollective",
5604 + "url": "https://opencollective.com/unified"
5605 + }
5606 + },
5607 + "node_modules/mdast-util-phrasing": {
5608 + "version": "4.1.0",
5609 + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
5610 + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
5611 + "license": "MIT",
5612 + "dependencies": {
5613 + "@types/mdast": "^4.0.0",
5614 + "unist-util-is": "^6.0.0"
5615 + },
5616 + "funding": {
5617 + "type": "opencollective",
5618 + "url": "https://opencollective.com/unified"
5619 + }
5620 + },
5621 + "node_modules/mdast-util-to-hast": {
5622 + "version": "13.2.1",
5623 + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
5624 + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
5625 + "license": "MIT",
5626 + "dependencies": {
5627 + "@types/hast": "^3.0.0",
5628 + "@types/mdast": "^4.0.0",
5629 + "@ungap/structured-clone": "^1.0.0",
5630 + "devlop": "^1.0.0",
5631 + "micromark-util-sanitize-uri": "^2.0.0",
5632 + "trim-lines": "^3.0.0",
5633 + "unist-util-position": "^5.0.0",
5634 + "unist-util-visit": "^5.0.0",
5635 + "vfile": "^6.0.0"
5636 + },
5637 + "funding": {
5638 + "type": "opencollective",
5639 + "url": "https://opencollective.com/unified"
5640 + }
5641 + },
5642 + "node_modules/mdast-util-to-markdown": {
5643 + "version": "2.1.2",
5644 + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
5645 + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
5646 + "license": "MIT",
5647 + "dependencies": {
5648 + "@types/mdast": "^4.0.0",
5649 + "@types/unist": "^3.0.0",
5650 + "longest-streak": "^3.0.0",
5651 + "mdast-util-phrasing": "^4.0.0",
5652 + "mdast-util-to-string": "^4.0.0",
5653 + "micromark-util-classify-character": "^2.0.0",
5654 + "micromark-util-decode-string": "^2.0.0",
5655 + "unist-util-visit": "^5.0.0",
5656 + "zwitch": "^2.0.0"
5657 + },
5658 + "funding": {
5659 + "type": "opencollective",
5660 + "url": "https://opencollective.com/unified"
5661 + }
5662 + },
5663 + "node_modules/mdast-util-to-string": {
5664 + "version": "4.0.0",
5665 + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
5666 + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
5667 + "license": "MIT",
5668 + "dependencies": {
5669 + "@types/mdast": "^4.0.0"
5670 + },
5671 + "funding": {
5672 + "type": "opencollective",
5673 + "url": "https://opencollective.com/unified"
5674 + }
5675 + },
5676 + "node_modules/merge2": {
5677 + "version": "1.4.1",
5678 + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
5679 + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
5680 + "dev": true,
5681 + "license": "MIT",
5682 + "engines": {
5683 + "node": ">= 8"
5684 + }
5685 + },
5686 + "node_modules/micromark": {
5687 + "version": "4.0.2",
5688 + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
5689 + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
5690 + "funding": [
5691 + {
5692 + "type": "GitHub Sponsors",
5693 + "url": "https://github.com/sponsors/unifiedjs"
5694 + },
5695 + {
5696 + "type": "OpenCollective",
5697 + "url": "https://opencollective.com/unified"
5698 + }
5699 + ],
5700 + "license": "MIT",
5701 + "dependencies": {
5702 + "@types/debug": "^4.0.0",
5703 + "debug": "^4.0.0",
5704 + "decode-named-character-reference": "^1.0.0",
5705 + "devlop": "^1.0.0",
5706 + "micromark-core-commonmark": "^2.0.0",
5707 + "micromark-factory-space": "^2.0.0",
5708 + "micromark-util-character": "^2.0.0",
5709 + "micromark-util-chunked": "^2.0.0",
5710 + "micromark-util-combine-extensions": "^2.0.0",
5711 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
5712 + "micromark-util-encode": "^2.0.0",
5713 + "micromark-util-normalize-identifier": "^2.0.0",
5714 + "micromark-util-resolve-all": "^2.0.0",
5715 + "micromark-util-sanitize-uri": "^2.0.0",
5716 + "micromark-util-subtokenize": "^2.0.0",
5717 + "micromark-util-symbol": "^2.0.0",
5718 + "micromark-util-types": "^2.0.0"
5719 + }
5720 + },
5721 + "node_modules/micromark-core-commonmark": {
5722 + "version": "2.0.3",
5723 + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
5724 + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
5725 + "funding": [
5726 + {
5727 + "type": "GitHub Sponsors",
5728 + "url": "https://github.com/sponsors/unifiedjs"
5729 + },
5730 + {
5731 + "type": "OpenCollective",
5732 + "url": "https://opencollective.com/unified"
5733 + }
5734 + ],
5735 + "license": "MIT",
5736 + "dependencies": {
5737 + "decode-named-character-reference": "^1.0.0",
5738 + "devlop": "^1.0.0",
5739 + "micromark-factory-destination": "^2.0.0",
5740 + "micromark-factory-label": "^2.0.0",
5741 + "micromark-factory-space": "^2.0.0",
5742 + "micromark-factory-title": "^2.0.0",
5743 + "micromark-factory-whitespace": "^2.0.0",
5744 + "micromark-util-character": "^2.0.0",
5745 + "micromark-util-chunked": "^2.0.0",
5746 + "micromark-util-classify-character": "^2.0.0",
5747 + "micromark-util-html-tag-name": "^2.0.0",
5748 + "micromark-util-normalize-identifier": "^2.0.0",
5749 + "micromark-util-resolve-all": "^2.0.0",
5750 + "micromark-util-subtokenize": "^2.0.0",
5751 + "micromark-util-symbol": "^2.0.0",
5752 + "micromark-util-types": "^2.0.0"
5753 + }
5754 + },
5755 + "node_modules/micromark-extension-gfm": {
5756 + "version": "3.0.0",
5757 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
5758 + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
5759 + "license": "MIT",
5760 + "dependencies": {
5761 + "micromark-extension-gfm-autolink-literal": "^2.0.0",
5762 + "micromark-extension-gfm-footnote": "^2.0.0",
5763 + "micromark-extension-gfm-strikethrough": "^2.0.0",
5764 + "micromark-extension-gfm-table": "^2.0.0",
5765 + "micromark-extension-gfm-tagfilter": "^2.0.0",
5766 + "micromark-extension-gfm-task-list-item": "^2.0.0",
5767 + "micromark-util-combine-extensions": "^2.0.0",
5768 + "micromark-util-types": "^2.0.0"
5769 + },
5770 + "funding": {
5771 + "type": "opencollective",
5772 + "url": "https://opencollective.com/unified"
5773 + }
5774 + },
5775 + "node_modules/micromark-extension-gfm-autolink-literal": {
5776 + "version": "2.1.0",
5777 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
5778 + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
5779 + "license": "MIT",
5780 + "dependencies": {
5781 + "micromark-util-character": "^2.0.0",
5782 + "micromark-util-sanitize-uri": "^2.0.0",
5783 + "micromark-util-symbol": "^2.0.0",
5784 + "micromark-util-types": "^2.0.0"
5785 + },
5786 + "funding": {
5787 + "type": "opencollective",
5788 + "url": "https://opencollective.com/unified"
5789 + }
5790 + },
5791 + "node_modules/micromark-extension-gfm-footnote": {
5792 + "version": "2.1.0",
5793 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
5794 + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
5795 + "license": "MIT",
5796 + "dependencies": {
5797 + "devlop": "^1.0.0",
5798 + "micromark-core-commonmark": "^2.0.0",
5799 + "micromark-factory-space": "^2.0.0",
5800 + "micromark-util-character": "^2.0.0",
5801 + "micromark-util-normalize-identifier": "^2.0.0",
5802 + "micromark-util-sanitize-uri": "^2.0.0",
5803 + "micromark-util-symbol": "^2.0.0",
5804 + "micromark-util-types": "^2.0.0"
5805 + },
5806 + "funding": {
5807 + "type": "opencollective",
5808 + "url": "https://opencollective.com/unified"
5809 + }
5810 + },
5811 + "node_modules/micromark-extension-gfm-strikethrough": {
5812 + "version": "2.1.0",
5813 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
5814 + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
5815 + "license": "MIT",
5816 + "dependencies": {
5817 + "devlop": "^1.0.0",
5818 + "micromark-util-chunked": "^2.0.0",
5819 + "micromark-util-classify-character": "^2.0.0",
5820 + "micromark-util-resolve-all": "^2.0.0",
5821 + "micromark-util-symbol": "^2.0.0",
5822 + "micromark-util-types": "^2.0.0"
5823 + },
5824 + "funding": {
5825 + "type": "opencollective",
5826 + "url": "https://opencollective.com/unified"
5827 + }
5828 + },
5829 + "node_modules/micromark-extension-gfm-table": {
5830 + "version": "2.1.1",
5831 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
5832 + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
5833 + "license": "MIT",
5834 + "dependencies": {
5835 + "devlop": "^1.0.0",
5836 + "micromark-factory-space": "^2.0.0",
5837 + "micromark-util-character": "^2.0.0",
5838 + "micromark-util-symbol": "^2.0.0",
5839 + "micromark-util-types": "^2.0.0"
5840 + },
5841 + "funding": {
5842 + "type": "opencollective",
5843 + "url": "https://opencollective.com/unified"
5844 + }
5845 + },
5846 + "node_modules/micromark-extension-gfm-tagfilter": {
5847 + "version": "2.0.0",
5848 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
5849 + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
5850 + "license": "MIT",
5851 + "dependencies": {
5852 + "micromark-util-types": "^2.0.0"
5853 + },
5854 + "funding": {
5855 + "type": "opencollective",
5856 + "url": "https://opencollective.com/unified"
5857 + }
5858 + },
5859 + "node_modules/micromark-extension-gfm-task-list-item": {
5860 + "version": "2.1.0",
5861 + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
5862 + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
5863 + "license": "MIT",
5864 + "dependencies": {
5865 + "devlop": "^1.0.0",
5866 + "micromark-factory-space": "^2.0.0",
5867 + "micromark-util-character": "^2.0.0",
5868 + "micromark-util-symbol": "^2.0.0",
5869 + "micromark-util-types": "^2.0.0"
5870 + },
5871 + "funding": {
5872 + "type": "opencollective",
5873 + "url": "https://opencollective.com/unified"
5874 + }
5875 + },
5876 + "node_modules/micromark-extension-math": {
5877 + "version": "3.1.0",
5878 + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
5879 + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
5880 + "license": "MIT",
5881 + "dependencies": {
5882 + "@types/katex": "^0.16.0",
5883 + "devlop": "^1.0.0",
5884 + "katex": "^0.16.0",
5885 + "micromark-factory-space": "^2.0.0",
5886 + "micromark-util-character": "^2.0.0",
5887 + "micromark-util-symbol": "^2.0.0",
5888 + "micromark-util-types": "^2.0.0"
5889 + },
5890 + "funding": {
5891 + "type": "opencollective",
5892 + "url": "https://opencollective.com/unified"
5893 + }
5894 + },
5895 + "node_modules/micromark-extension-math/node_modules/katex": {
5896 + "version": "0.16.47",
5897 + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
5898 + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
5899 + "funding": [
5900 + "https://opencollective.com/katex",
5901 + "https://github.com/sponsors/katex"
5902 + ],
5903 + "license": "MIT",
5904 + "dependencies": {
5905 + "commander": "^8.3.0"
5906 + },
5907 + "bin": {
5908 + "katex": "cli.js"
5909 + }
5910 + },
5911 + "node_modules/micromark-factory-destination": {
5912 + "version": "2.0.1",
5913 + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
5914 + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
5915 + "funding": [
5916 + {
5917 + "type": "GitHub Sponsors",
5918 + "url": "https://github.com/sponsors/unifiedjs"
5919 + },
5920 + {
5921 + "type": "OpenCollective",
5922 + "url": "https://opencollective.com/unified"
5923 + }
5924 + ],
5925 + "license": "MIT",
5926 + "dependencies": {
5927 + "micromark-util-character": "^2.0.0",
5928 + "micromark-util-symbol": "^2.0.0",
5929 + "micromark-util-types": "^2.0.0"
5930 + }
5931 + },
5932 + "node_modules/micromark-factory-label": {
5933 + "version": "2.0.1",
5934 + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
5935 + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
5936 + "funding": [
5937 + {
5938 + "type": "GitHub Sponsors",
5939 + "url": "https://github.com/sponsors/unifiedjs"
5940 + },
5941 + {
5942 + "type": "OpenCollective",
5943 + "url": "https://opencollective.com/unified"
5944 + }
5945 + ],
5946 + "license": "MIT",
5947 + "dependencies": {
5948 + "devlop": "^1.0.0",
5949 + "micromark-util-character": "^2.0.0",
5950 + "micromark-util-symbol": "^2.0.0",
5951 + "micromark-util-types": "^2.0.0"
5952 + }
5953 + },
5954 + "node_modules/micromark-factory-space": {
5955 + "version": "2.0.1",
5956 + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
5957 + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
5958 + "funding": [
5959 + {
5960 + "type": "GitHub Sponsors",
5961 + "url": "https://github.com/sponsors/unifiedjs"
5962 + },
5963 + {
5964 + "type": "OpenCollective",
5965 + "url": "https://opencollective.com/unified"
5966 + }
5967 + ],
5968 + "license": "MIT",
5969 + "dependencies": {
5970 + "micromark-util-character": "^2.0.0",
5971 + "micromark-util-types": "^2.0.0"
5972 + }
5973 + },
5974 + "node_modules/micromark-factory-title": {
5975 + "version": "2.0.1",
5976 + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
5977 + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
5978 + "funding": [
5979 + {
5980 + "type": "GitHub Sponsors",
5981 + "url": "https://github.com/sponsors/unifiedjs"
5982 + },
5983 + {
5984 + "type": "OpenCollective",
5985 + "url": "https://opencollective.com/unified"
5986 + }
5987 + ],
5988 + "license": "MIT",
5989 + "dependencies": {
5990 + "micromark-factory-space": "^2.0.0",
5991 + "micromark-util-character": "^2.0.0",
5992 + "micromark-util-symbol": "^2.0.0",
5993 + "micromark-util-types": "^2.0.0"
5994 + }
5995 + },
5996 + "node_modules/micromark-factory-whitespace": {
5997 + "version": "2.0.1",
5998 + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
5999 + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
6000 + "funding": [
6001 + {
6002 + "type": "GitHub Sponsors",
6003 + "url": "https://github.com/sponsors/unifiedjs"
6004 + },
6005 + {
6006 + "type": "OpenCollective",
6007 + "url": "https://opencollective.com/unified"
6008 + }
6009 + ],
6010 + "license": "MIT",
6011 + "dependencies": {
6012 + "micromark-factory-space": "^2.0.0",
6013 + "micromark-util-character": "^2.0.0",
6014 + "micromark-util-symbol": "^2.0.0",
6015 + "micromark-util-types": "^2.0.0"
6016 + }
6017 + },
6018 + "node_modules/micromark-util-character": {
6019 + "version": "2.1.1",
6020 + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
6021 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
6022 + "funding": [
6023 + {
6024 + "type": "GitHub Sponsors",
6025 + "url": "https://github.com/sponsors/unifiedjs"
6026 + },
6027 + {
6028 + "type": "OpenCollective",
6029 + "url": "https://opencollective.com/unified"
6030 + }
6031 + ],
6032 + "license": "MIT",
6033 + "dependencies": {
6034 + "micromark-util-symbol": "^2.0.0",
6035 + "micromark-util-types": "^2.0.0"
6036 + }
6037 + },
6038 + "node_modules/micromark-util-chunked": {
6039 + "version": "2.0.1",
6040 + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
6041 + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
6042 + "funding": [
6043 + {
6044 + "type": "GitHub Sponsors",
6045 + "url": "https://github.com/sponsors/unifiedjs"
6046 + },
6047 + {
6048 + "type": "OpenCollective",
6049 + "url": "https://opencollective.com/unified"
6050 + }
6051 + ],
6052 + "license": "MIT",
6053 + "dependencies": {
6054 + "micromark-util-symbol": "^2.0.0"
6055 + }
6056 + },
6057 + "node_modules/micromark-util-classify-character": {
6058 + "version": "2.0.1",
6059 + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
6060 + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
6061 + "funding": [
6062 + {
6063 + "type": "GitHub Sponsors",
6064 + "url": "https://github.com/sponsors/unifiedjs"
6065 + },
6066 + {
6067 + "type": "OpenCollective",
6068 + "url": "https://opencollective.com/unified"
6069 + }
6070 + ],
6071 + "license": "MIT",
6072 + "dependencies": {
6073 + "micromark-util-character": "^2.0.0",
6074 + "micromark-util-symbol": "^2.0.0",
6075 + "micromark-util-types": "^2.0.0"
6076 + }
6077 + },
6078 + "node_modules/micromark-util-combine-extensions": {
6079 + "version": "2.0.1",
6080 + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
6081 + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
6082 + "funding": [
6083 + {
6084 + "type": "GitHub Sponsors",
6085 + "url": "https://github.com/sponsors/unifiedjs"
6086 + },
6087 + {
6088 + "type": "OpenCollective",
6089 + "url": "https://opencollective.com/unified"
6090 + }
6091 + ],
6092 + "license": "MIT",
6093 + "dependencies": {
6094 + "micromark-util-chunked": "^2.0.0",
6095 + "micromark-util-types": "^2.0.0"
6096 + }
6097 + },
6098 + "node_modules/micromark-util-decode-numeric-character-reference": {
6099 + "version": "2.0.2",
6100 + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
6101 + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
6102 + "funding": [
6103 + {
6104 + "type": "GitHub Sponsors",
6105 + "url": "https://github.com/sponsors/unifiedjs"
6106 + },
6107 + {
6108 + "type": "OpenCollective",
6109 + "url": "https://opencollective.com/unified"
6110 + }
6111 + ],
6112 + "license": "MIT",
6113 + "dependencies": {
6114 + "micromark-util-symbol": "^2.0.0"
6115 + }
6116 + },
6117 + "node_modules/micromark-util-decode-string": {
6118 + "version": "2.0.1",
6119 + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
6120 + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
6121 + "funding": [
6122 + {
6123 + "type": "GitHub Sponsors",
6124 + "url": "https://github.com/sponsors/unifiedjs"
6125 + },
6126 + {
6127 + "type": "OpenCollective",
6128 + "url": "https://opencollective.com/unified"
6129 + }
6130 + ],
6131 + "license": "MIT",
6132 + "dependencies": {
6133 + "decode-named-character-reference": "^1.0.0",
6134 + "micromark-util-character": "^2.0.0",
6135 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
6136 + "micromark-util-symbol": "^2.0.0"
6137 + }
6138 + },
6139 + "node_modules/micromark-util-encode": {
6140 + "version": "2.0.1",
6141 + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
6142 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
6143 + "funding": [
6144 + {
6145 + "type": "GitHub Sponsors",
6146 + "url": "https://github.com/sponsors/unifiedjs"
6147 + },
6148 + {
6149 + "type": "OpenCollective",
6150 + "url": "https://opencollective.com/unified"
6151 + }
6152 + ],
6153 + "license": "MIT"
6154 + },
6155 + "node_modules/micromark-util-html-tag-name": {
6156 + "version": "2.0.1",
6157 + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
6158 + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
6159 + "funding": [
6160 + {
6161 + "type": "GitHub Sponsors",
6162 + "url": "https://github.com/sponsors/unifiedjs"
6163 + },
6164 + {
6165 + "type": "OpenCollective",
6166 + "url": "https://opencollective.com/unified"
6167 + }
6168 + ],
6169 + "license": "MIT"
6170 + },
6171 + "node_modules/micromark-util-normalize-identifier": {
6172 + "version": "2.0.1",
6173 + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
6174 + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
6175 + "funding": [
6176 + {
6177 + "type": "GitHub Sponsors",
6178 + "url": "https://github.com/sponsors/unifiedjs"
6179 + },
6180 + {
6181 + "type": "OpenCollective",
6182 + "url": "https://opencollective.com/unified"
6183 + }
6184 + ],
6185 + "license": "MIT",
6186 + "dependencies": {
6187 + "micromark-util-symbol": "^2.0.0"
6188 + }
6189 + },
6190 + "node_modules/micromark-util-resolve-all": {
6191 + "version": "2.0.1",
6192 + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
6193 + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
6194 + "funding": [
6195 + {
6196 + "type": "GitHub Sponsors",
6197 + "url": "https://github.com/sponsors/unifiedjs"
6198 + },
6199 + {
6200 + "type": "OpenCollective",
6201 + "url": "https://opencollective.com/unified"
6202 + }
6203 + ],
6204 + "license": "MIT",
6205 + "dependencies": {
6206 + "micromark-util-types": "^2.0.0"
6207 + }
6208 + },
6209 + "node_modules/micromark-util-sanitize-uri": {
6210 + "version": "2.0.1",
6211 + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
6212 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
6213 + "funding": [
6214 + {
6215 + "type": "GitHub Sponsors",
6216 + "url": "https://github.com/sponsors/unifiedjs"
6217 + },
6218 + {
6219 + "type": "OpenCollective",
6220 + "url": "https://opencollective.com/unified"
6221 + }
6222 + ],
6223 + "license": "MIT",
6224 + "dependencies": {
6225 + "micromark-util-character": "^2.0.0",
6226 + "micromark-util-encode": "^2.0.0",
6227 + "micromark-util-symbol": "^2.0.0"
6228 + }
6229 + },
6230 + "node_modules/micromark-util-subtokenize": {
6231 + "version": "2.1.0",
6232 + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
6233 + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
6234 + "funding": [
6235 + {
6236 + "type": "GitHub Sponsors",
6237 + "url": "https://github.com/sponsors/unifiedjs"
6238 + },
6239 + {
6240 + "type": "OpenCollective",
6241 + "url": "https://opencollective.com/unified"
6242 + }
6243 + ],
6244 + "license": "MIT",
6245 + "dependencies": {
6246 + "devlop": "^1.0.0",
6247 + "micromark-util-chunked": "^2.0.0",
6248 + "micromark-util-symbol": "^2.0.0",
6249 + "micromark-util-types": "^2.0.0"
6250 + }
6251 + },
6252 + "node_modules/micromark-util-symbol": {
6253 + "version": "2.0.1",
6254 + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
6255 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
6256 + "funding": [
6257 + {
6258 + "type": "GitHub Sponsors",
6259 + "url": "https://github.com/sponsors/unifiedjs"
6260 + },
6261 + {
6262 + "type": "OpenCollective",
6263 + "url": "https://opencollective.com/unified"
6264 + }
6265 + ],
6266 + "license": "MIT"
6267 + },
6268 + "node_modules/micromark-util-types": {
6269 + "version": "2.0.2",
6270 + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
6271 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
6272 + "funding": [
6273 + {
6274 + "type": "GitHub Sponsors",
6275 + "url": "https://github.com/sponsors/unifiedjs"
6276 + },
6277 + {
6278 + "type": "OpenCollective",
6279 + "url": "https://opencollective.com/unified"
6280 + }
6281 + ],
6282 + "license": "MIT"
6283 + },
6284 + "node_modules/micromatch": {
6285 + "version": "4.0.8",
6286 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
6287 + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
6288 + "dev": true,
6289 + "license": "MIT",
6290 + "dependencies": {
6291 + "braces": "^3.0.3",
6292 + "picomatch": "^2.3.1"
6293 + },
6294 + "engines": {
6295 + "node": ">=8.6"
6296 + }
6297 + },
6298 + "node_modules/minimatch": {
6299 + "version": "3.1.5",
6300 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
6301 + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
6302 + "dev": true,
6303 + "license": "ISC",
6304 + "dependencies": {
6305 + "brace-expansion": "^1.1.7"
6306 + },
6307 + "engines": {
6308 + "node": "*"
6309 + }
6310 + },
6311 + "node_modules/minimist": {
6312 + "version": "1.2.8",
6313 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
6314 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
6315 + "dev": true,
6316 + "license": "MIT",
6317 + "funding": {
6318 + "url": "https://github.com/sponsors/ljharb"
6319 + }
6320 + },
6321 + "node_modules/ms": {
6322 + "version": "2.1.3",
6323 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
6324 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
6325 + "license": "MIT"
6326 + },
6327 + "node_modules/nanoid": {
6328 + "version": "3.3.18",
6329 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
6330 + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
6331 + "funding": [
6332 + {
6333 + "type": "github",
6334 + "url": "https://github.com/sponsors/ai"
6335 + }
6336 + ],
6337 + "license": "MIT",
6338 + "bin": {
6339 + "nanoid": "bin/nanoid.cjs"
6340 + },
6341 + "engines": {
6342 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
6343 + }
6344 + },
6345 + "node_modules/napi-postinstall": {
6346 + "version": "0.3.4",
6347 + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
6348 + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
6349 + "dev": true,
6350 + "license": "MIT",
6351 + "bin": {
6352 + "napi-postinstall": "lib/cli.js"
6353 + },
6354 + "engines": {
6355 + "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
6356 + },
6357 + "funding": {
6358 + "url": "https://opencollective.com/napi-postinstall"
6359 + }
6360 + },
6361 + "node_modules/natural-compare": {
6362 + "version": "1.4.0",
6363 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
6364 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
6365 + "dev": true,
6366 + "license": "MIT"
6367 + },
6368 + "node_modules/next": {
6369 + "version": "16.3.0",
6370 + "resolved": "https://registry.npmjs.org/next/-/next-16.3.0.tgz",
6371 + "integrity": "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==",
6372 + "license": "MIT",
6373 + "dependencies": {
6374 + "@next/env": "16.3.0",
6375 + "@swc/helpers": "0.5.15",
6376 + "baseline-browser-mapping": "^2.9.19",
6377 + "caniuse-lite": "^1.0.30001579",
6378 + "postcss": "8.5.23",
6379 + "styled-jsx": "5.1.6"
6380 + },
6381 + "bin": {
6382 + "next": "dist/bin/next"
6383 + },
6384 + "engines": {
6385 + "node": ">=20.9.0"
6386 + },
6387 + "optionalDependencies": {
6388 + "@next/swc-darwin-arm64": "16.3.0",
6389 + "@next/swc-darwin-x64": "16.3.0",
6390 + "@next/swc-linux-arm64-gnu": "16.3.0",
6391 + "@next/swc-linux-arm64-musl": "16.3.0",
6392 + "@next/swc-linux-x64-gnu": "16.3.0",
6393 + "@next/swc-linux-x64-musl": "16.3.0",
6394 + "@next/swc-win32-arm64-msvc": "16.3.0",
6395 + "@next/swc-win32-x64-msvc": "16.3.0",
6396 + "sharp": "^0.35.3"
6397 + },
6398 + "peerDependencies": {
6399 + "@opentelemetry/api": "^1.1.0",
6400 + "@playwright/test": "^1.51.1",
6401 + "babel-plugin-react-compiler": "*",
6402 + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
6403 + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
6404 + "sass": "^1.3.0"
6405 + },
6406 + "peerDependenciesMeta": {
6407 + "@opentelemetry/api": {
6408 + "optional": true
6409 + },
6410 + "@playwright/test": {
6411 + "optional": true
6412 + },
6413 + "babel-plugin-react-compiler": {
6414 + "optional": true
6415 + },
6416 + "sass": {
6417 + "optional": true
6418 + }
6419 + }
6420 + },
6421 + "node_modules/node-addon-api": {
6422 + "version": "8.9.1",
6423 + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.1.tgz",
6424 + "integrity": "sha512-4eUQWVPCUUUiBjLnHS3cXWeC6ryoPUc0U3rP7IuzapoGbzMqd/r6KKO0clr0b+snQhsrueFEhCZDdK+LK7hxKg==",
6425 + "license": "MIT",
6426 + "engines": {
6427 + "node": "^18 || ^20 || >= 21"
6428 + }
6429 + },
6430 + "node_modules/node-exports-info": {
6431 + "version": "1.6.2",
6432 + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz",
6433 + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==",
6434 + "dev": true,
6435 + "license": "MIT",
6436 + "dependencies": {
6437 + "array.prototype.flatmap": "^1.3.3",
6438 + "es-errors": "^1.3.0",
6439 + "object.entries": "^1.1.9",
6440 + "semver": "^6.3.1"
6441 + },
6442 + "engines": {
6443 + "node": ">= 0.4"
6444 + },
6445 + "funding": {
6446 + "url": "https://github.com/sponsors/ljharb"
6447 + }
6448 + },
6449 + "node_modules/node-releases": {
6450 + "version": "2.0.53",
6451 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
6452 + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
6453 + "dev": true,
6454 + "license": "MIT",
6455 + "engines": {
6456 + "node": ">=18"
6457 + }
6458 + },
6459 + "node_modules/object-assign": {
6460 + "version": "4.1.1",
6461 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
6462 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
6463 + "dev": true,
6464 + "license": "MIT",
6465 + "engines": {
6466 + "node": ">=0.10.0"
6467 + }
6468 + },
6469 + "node_modules/object-inspect": {
6470 + "version": "1.13.4",
6471 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
6472 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
6473 + "dev": true,
6474 + "license": "MIT",
6475 + "engines": {
6476 + "node": ">= 0.4"
6477 + },
6478 + "funding": {
6479 + "url": "https://github.com/sponsors/ljharb"
6480 + }
6481 + },
6482 + "node_modules/object-keys": {
6483 + "version": "1.1.1",
6484 + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
6485 + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
6486 + "dev": true,
6487 + "license": "MIT",
6488 + "engines": {
6489 + "node": ">= 0.4"
6490 + }
6491 + },
6492 + "node_modules/object.assign": {
6493 + "version": "4.1.7",
6494 + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
6495 + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
6496 + "dev": true,
6497 + "license": "MIT",
6498 + "dependencies": {
6499 + "call-bind": "^1.0.8",
6500 + "call-bound": "^1.0.3",
6501 + "define-properties": "^1.2.1",
6502 + "es-object-atoms": "^1.0.0",
6503 + "has-symbols": "^1.1.0",
6504 + "object-keys": "^1.1.1"
6505 + },
6506 + "engines": {
6507 + "node": ">= 0.4"
6508 + },
6509 + "funding": {
6510 + "url": "https://github.com/sponsors/ljharb"
6511 + }
6512 + },
6513 + "node_modules/object.entries": {
6514 + "version": "1.1.9",
6515 + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
6516 + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
6517 + "dev": true,
6518 + "license": "MIT",
6519 + "dependencies": {
6520 + "call-bind": "^1.0.8",
6521 + "call-bound": "^1.0.4",
6522 + "define-properties": "^1.2.1",
6523 + "es-object-atoms": "^1.1.1"
6524 + },
6525 + "engines": {
6526 + "node": ">= 0.4"
6527 + }
6528 + },
6529 + "node_modules/object.fromentries": {
6530 + "version": "2.0.8",
6531 + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
6532 + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
6533 + "dev": true,
6534 + "license": "MIT",
6535 + "dependencies": {
6536 + "call-bind": "^1.0.7",
6537 + "define-properties": "^1.2.1",
6538 + "es-abstract": "^1.23.2",
6539 + "es-object-atoms": "^1.0.0"
6540 + },
6541 + "engines": {
6542 + "node": ">= 0.4"
6543 + },
6544 + "funding": {
6545 + "url": "https://github.com/sponsors/ljharb"
6546 + }
6547 + },
6548 + "node_modules/object.groupby": {
6549 + "version": "1.0.3",
6550 + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
6551 + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
6552 + "dev": true,
6553 + "license": "MIT",
6554 + "dependencies": {
6555 + "call-bind": "^1.0.7",
6556 + "define-properties": "^1.2.1",
6557 + "es-abstract": "^1.23.2"
6558 + },
6559 + "engines": {
6560 + "node": ">= 0.4"
6561 + }
6562 + },
6563 + "node_modules/object.values": {
6564 + "version": "1.2.1",
6565 + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
6566 + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
6567 + "dev": true,
6568 + "license": "MIT",
6569 + "dependencies": {
6570 + "call-bind": "^1.0.8",
6571 + "call-bound": "^1.0.3",
6572 + "define-properties": "^1.2.1",
6573 + "es-object-atoms": "^1.0.0"
6574 + },
6575 + "engines": {
6576 + "node": ">= 0.4"
6577 + },
6578 + "funding": {
6579 + "url": "https://github.com/sponsors/ljharb"
6580 + }
6581 + },
6582 + "node_modules/optionator": {
6583 + "version": "0.9.4",
6584 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
6585 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
6586 + "dev": true,
6587 + "license": "MIT",
6588 + "dependencies": {
6589 + "deep-is": "^0.1.3",
6590 + "fast-levenshtein": "^2.0.6",
6591 + "levn": "^0.4.1",
6592 + "prelude-ls": "^1.2.1",
6593 + "type-check": "^0.4.0",
6594 + "word-wrap": "^1.2.5"
6595 + },
6596 + "engines": {
6597 + "node": ">= 0.8.0"
6598 + }
6599 + },
6600 + "node_modules/own-keys": {
6601 + "version": "1.0.2",
6602 + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz",
6603 + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==",
6604 + "dev": true,
6605 + "license": "MIT",
6606 + "dependencies": {
6607 + "call-bound": "^1.0.4",
6608 + "get-intrinsic": "^1.3.0",
6609 + "object-keys": "^1.1.1",
6610 + "safe-push-apply": "^1.0.0"
6611 + },
6612 + "engines": {
6613 + "node": ">= 0.4"
6614 + },
6615 + "funding": {
6616 + "url": "https://github.com/sponsors/ljharb"
6617 + }
6618 + },
6619 + "node_modules/p-limit": {
6620 + "version": "3.1.0",
6621 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
6622 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
6623 + "dev": true,
6624 + "license": "MIT",
6625 + "dependencies": {
6626 + "yocto-queue": "^0.1.0"
6627 + },
6628 + "engines": {
6629 + "node": ">=10"
6630 + },
6631 + "funding": {
6632 + "url": "https://github.com/sponsors/sindresorhus"
6633 + }
6634 + },
6635 + "node_modules/p-locate": {
6636 + "version": "5.0.0",
6637 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
6638 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
6639 + "dev": true,
6640 + "license": "MIT",
6641 + "dependencies": {
6642 + "p-limit": "^3.0.2"
6643 + },
6644 + "engines": {
6645 + "node": ">=10"
6646 + },
6647 + "funding": {
6648 + "url": "https://github.com/sponsors/sindresorhus"
6649 + }
6650 + },
6651 + "node_modules/parent-module": {
6652 + "version": "1.0.1",
6653 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
6654 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
6655 + "dev": true,
6656 + "license": "MIT",
6657 + "dependencies": {
6658 + "callsites": "^3.0.0"
6659 + },
6660 + "engines": {
6661 + "node": ">=6"
6662 + }
6663 + },
6664 + "node_modules/parse-entities": {
6665 + "version": "4.0.2",
6666 + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
6667 + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
6668 + "license": "MIT",
6669 + "dependencies": {
6670 + "@types/unist": "^2.0.0",
6671 + "character-entities-legacy": "^3.0.0",
6672 + "character-reference-invalid": "^2.0.0",
6673 + "decode-named-character-reference": "^1.0.0",
6674 + "is-alphanumerical": "^2.0.0",
6675 + "is-decimal": "^2.0.0",
6676 + "is-hexadecimal": "^2.0.0"
6677 + },
6678 + "funding": {
6679 + "type": "github",
6680 + "url": "https://github.com/sponsors/wooorm"
6681 + }
6682 + },
6683 + "node_modules/parse-entities/node_modules/@types/unist": {
6684 + "version": "2.0.11",
6685 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
6686 + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
6687 + "license": "MIT"
6688 + },
6689 + "node_modules/parse5": {
6690 + "version": "7.3.0",
6691 + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
6692 + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
6693 + "license": "MIT",
6694 + "dependencies": {
6695 + "entities": "^6.0.0"
6696 + },
6697 + "funding": {
6698 + "url": "https://github.com/inikulin/parse5?sponsor=1"
6699 + }
6700 + },
6701 + "node_modules/path-exists": {
6702 + "version": "4.0.0",
6703 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
6704 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
6705 + "dev": true,
6706 + "license": "MIT",
6707 + "engines": {
6708 + "node": ">=8"
6709 + }
6710 + },
6711 + "node_modules/path-key": {
6712 + "version": "3.1.1",
6713 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
6714 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
6715 + "dev": true,
6716 + "license": "MIT",
6717 + "engines": {
6718 + "node": ">=8"
6719 + }
6720 + },
6721 + "node_modules/path-parse": {
6722 + "version": "1.0.7",
6723 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
6724 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
6725 + "dev": true,
6726 + "license": "MIT"
6727 + },
6728 + "node_modules/picocolors": {
6729 + "version": "1.1.1",
6730 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
6731 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
6732 + "license": "ISC"
6733 + },
6734 + "node_modules/picomatch": {
6735 + "version": "2.3.2",
6736 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
6737 + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
6738 + "dev": true,
6739 + "license": "MIT",
6740 + "engines": {
6741 + "node": ">=8.6"
6742 + },
6743 + "funding": {
6744 + "url": "https://github.com/sponsors/jonschlinkert"
6745 + }
6746 + },
6747 + "node_modules/possible-typed-array-names": {
6748 + "version": "1.1.0",
6749 + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
6750 + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
6751 + "dev": true,
6752 + "license": "MIT",
6753 + "engines": {
6754 + "node": ">= 0.4"
6755 + }
6756 + },
6757 + "node_modules/postcss": {
6758 + "version": "8.5.23",
6759 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
6760 + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
6761 + "funding": [
6762 + {
6763 + "type": "opencollective",
6764 + "url": "https://opencollective.com/postcss/"
6765 + },
6766 + {
6767 + "type": "tidelift",
6768 + "url": "https://tidelift.com/funding/github/npm/postcss"
6769 + },
6770 + {
6771 + "type": "github",
6772 + "url": "https://github.com/sponsors/ai"
6773 + }
6774 + ],
6775 + "license": "MIT",
6776 + "dependencies": {
6777 + "nanoid": "^3.3.16",
6778 + "picocolors": "^1.1.1",
6779 + "source-map-js": "^1.2.1"
6780 + },
6781 + "engines": {
6782 + "node": "^10 || ^12 || >=14"
6783 + }
6784 + },
6785 + "node_modules/prelude-ls": {
6786 + "version": "1.2.1",
6787 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
6788 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
6789 + "dev": true,
6790 + "license": "MIT",
6791 + "engines": {
6792 + "node": ">= 0.8.0"
6793 + }
6794 + },
6795 + "node_modules/prop-types": {
6796 + "version": "15.8.1",
6797 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
6798 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
6799 + "dev": true,
6800 + "license": "MIT",
6801 + "dependencies": {
6802 + "loose-envify": "^1.4.0",
6803 + "object-assign": "^4.1.1",
6804 + "react-is": "^16.13.1"
6805 + }
6806 + },
6807 + "node_modules/property-information": {
6808 + "version": "7.2.0",
6809 + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
6810 + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
6811 + "license": "MIT",
6812 + "funding": {
6813 + "type": "github",
6814 + "url": "https://github.com/sponsors/wooorm"
6815 + }
6816 + },
6817 + "node_modules/punycode": {
6818 + "version": "2.3.1",
6819 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
6820 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
6821 + "dev": true,
6822 + "license": "MIT",
6823 + "engines": {
6824 + "node": ">=6"
6825 + }
6826 + },
6827 + "node_modules/queue-microtask": {
6828 + "version": "1.2.3",
6829 + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
6830 + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
6831 + "dev": true,
6832 + "funding": [
6833 + {
6834 + "type": "github",
6835 + "url": "https://github.com/sponsors/feross"
6836 + },
6837 + {
6838 + "type": "patreon",
6839 + "url": "https://www.patreon.com/feross"
6840 + },
6841 + {
6842 + "type": "consulting",
6843 + "url": "https://feross.org/support"
6844 + }
6845 + ],
6846 + "license": "MIT"
6847 + },
6848 + "node_modules/react": {
6849 + "version": "19.2.8",
6850 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
6851 + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
6852 + "license": "MIT",
6853 + "engines": {
6854 + "node": ">=0.10.0"
6855 + }
6856 + },
6857 + "node_modules/react-dom": {
6858 + "version": "19.2.8",
6859 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
6860 + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
6861 + "license": "MIT",
6862 + "dependencies": {
6863 + "scheduler": "^0.27.0"
6864 + },
6865 + "peerDependencies": {
6866 + "react": "^19.2.8"
6867 + }
6868 + },
6869 + "node_modules/react-is": {
6870 + "version": "16.13.1",
6871 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
6872 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
6873 + "dev": true,
6874 + "license": "MIT"
6875 + },
6876 + "node_modules/react-markdown": {
6877 + "version": "10.1.0",
6878 + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
6879 + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
6880 + "license": "MIT",
6881 + "dependencies": {
6882 + "@types/hast": "^3.0.0",
6883 + "@types/mdast": "^4.0.0",
6884 + "devlop": "^1.0.0",
6885 + "hast-util-to-jsx-runtime": "^2.0.0",
6886 + "html-url-attributes": "^3.0.0",
6887 + "mdast-util-to-hast": "^13.0.0",
6888 + "remark-parse": "^11.0.0",
6889 + "remark-rehype": "^11.0.0",
6890 + "unified": "^11.0.0",
6891 + "unist-util-visit": "^5.0.0",
6892 + "vfile": "^6.0.0"
6893 + },
6894 + "funding": {
6895 + "type": "opencollective",
6896 + "url": "https://opencollective.com/unified"
6897 + },
6898 + "peerDependencies": {
6899 + "@types/react": ">=18",
6900 + "react": ">=18"
6901 + }
6902 + },
6903 + "node_modules/reflect.getprototypeof": {
6904 + "version": "1.0.10",
6905 + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
6906 + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
6907 + "dev": true,
6908 + "license": "MIT",
6909 + "dependencies": {
6910 + "call-bind": "^1.0.8",
6911 + "define-properties": "^1.2.1",
6912 + "es-abstract": "^1.23.9",
6913 + "es-errors": "^1.3.0",
6914 + "es-object-atoms": "^1.0.0",
6915 + "get-intrinsic": "^1.2.7",
6916 + "get-proto": "^1.0.1",
6917 + "which-builtin-type": "^1.2.1"
6918 + },
6919 + "engines": {
6920 + "node": ">= 0.4"
6921 + },
6922 + "funding": {
6923 + "url": "https://github.com/sponsors/ljharb"
6924 + }
6925 + },
6926 + "node_modules/regexp.prototype.flags": {
6927 + "version": "1.5.4",
6928 + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
6929 + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
6930 + "dev": true,
6931 + "license": "MIT",
6932 + "dependencies": {
6933 + "call-bind": "^1.0.8",
6934 + "define-properties": "^1.2.1",
6935 + "es-errors": "^1.3.0",
6936 + "get-proto": "^1.0.1",
6937 + "gopd": "^1.2.0",
6938 + "set-function-name": "^2.0.2"
6939 + },
6940 + "engines": {
6941 + "node": ">= 0.4"
6942 + },
6943 + "funding": {
6944 + "url": "https://github.com/sponsors/ljharb"
6945 + }
6946 + },
6947 + "node_modules/rehype-highlight": {
6948 + "version": "7.0.2",
6949 + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
6950 + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
6951 + "license": "MIT",
6952 + "dependencies": {
6953 + "@types/hast": "^3.0.0",
6954 + "hast-util-to-text": "^4.0.0",
6955 + "lowlight": "^3.0.0",
6956 + "unist-util-visit": "^5.0.0",
6957 + "vfile": "^6.0.0"
6958 + },
6959 + "funding": {
6960 + "type": "opencollective",
6961 + "url": "https://opencollective.com/unified"
6962 + }
6963 + },
6964 + "node_modules/rehype-katex": {
6965 + "version": "7.0.1",
6966 + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
6967 + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==",
6968 + "license": "MIT",
6969 + "dependencies": {
6970 + "@types/hast": "^3.0.0",
6971 + "@types/katex": "^0.16.0",
6972 + "hast-util-from-html-isomorphic": "^2.0.0",
6973 + "hast-util-to-text": "^4.0.0",
6974 + "katex": "^0.16.0",
6975 + "unist-util-visit-parents": "^6.0.0",
6976 + "vfile": "^6.0.0"
6977 + },
6978 + "funding": {
6979 + "type": "opencollective",
6980 + "url": "https://opencollective.com/unified"
6981 + }
6982 + },
6983 + "node_modules/rehype-katex/node_modules/katex": {
6984 + "version": "0.16.47",
6985 + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
6986 + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
6987 + "funding": [
6988 + "https://opencollective.com/katex",
6989 + "https://github.com/sponsors/katex"
6990 + ],
6991 + "license": "MIT",
6992 + "dependencies": {
6993 + "commander": "^8.3.0"
6994 + },
6995 + "bin": {
6996 + "katex": "cli.js"
6997 + }
6998 + },
6999 + "node_modules/remark-gfm": {
7000 + "version": "4.0.1",
7001 + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
7002 + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
7003 + "license": "MIT",
7004 + "dependencies": {
7005 + "@types/mdast": "^4.0.0",
7006 + "mdast-util-gfm": "^3.0.0",
7007 + "micromark-extension-gfm": "^3.0.0",
7008 + "remark-parse": "^11.0.0",
7009 + "remark-stringify": "^11.0.0",
7010 + "unified": "^11.0.0"
7011 + },
7012 + "funding": {
7013 + "type": "opencollective",
7014 + "url": "https://opencollective.com/unified"
7015 + }
7016 + },
7017 + "node_modules/remark-math": {
7018 + "version": "6.0.0",
7019 + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
7020 + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==",
7021 + "license": "MIT",
7022 + "dependencies": {
7023 + "@types/mdast": "^4.0.0",
7024 + "mdast-util-math": "^3.0.0",
7025 + "micromark-extension-math": "^3.0.0",
7026 + "unified": "^11.0.0"
7027 + },
7028 + "funding": {
7029 + "type": "opencollective",
7030 + "url": "https://opencollective.com/unified"
7031 + }
7032 + },
7033 + "node_modules/remark-parse": {
7034 + "version": "11.0.0",
7035 + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
7036 + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
7037 + "license": "MIT",
7038 + "dependencies": {
7039 + "@types/mdast": "^4.0.0",
7040 + "mdast-util-from-markdown": "^2.0.0",
7041 + "micromark-util-types": "^2.0.0",
7042 + "unified": "^11.0.0"
7043 + },
7044 + "funding": {
7045 + "type": "opencollective",
7046 + "url": "https://opencollective.com/unified"
7047 + }
7048 + },
7049 + "node_modules/remark-rehype": {
7050 + "version": "11.1.2",
7051 + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
7052 + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
7053 + "license": "MIT",
7054 + "dependencies": {
7055 + "@types/hast": "^3.0.0",
7056 + "@types/mdast": "^4.0.0",
7057 + "mdast-util-to-hast": "^13.0.0",
7058 + "unified": "^11.0.0",
7059 + "vfile": "^6.0.0"
7060 + },
7061 + "funding": {
7062 + "type": "opencollective",
7063 + "url": "https://opencollective.com/unified"
7064 + }
7065 + },
7066 + "node_modules/remark-stringify": {
7067 + "version": "11.0.0",
7068 + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
7069 + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
7070 + "license": "MIT",
7071 + "dependencies": {
7072 + "@types/mdast": "^4.0.0",
7073 + "mdast-util-to-markdown": "^2.0.0",
7074 + "unified": "^11.0.0"
7075 + },
7076 + "funding": {
7077 + "type": "opencollective",
7078 + "url": "https://opencollective.com/unified"
7079 + }
7080 + },
7081 + "node_modules/resolve": {
7082 + "version": "2.0.0-next.7",
7083 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
7084 + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
7085 + "dev": true,
7086 + "license": "MIT",
7087 + "dependencies": {
7088 + "es-errors": "^1.3.0",
7089 + "is-core-module": "^2.16.2",
7090 + "node-exports-info": "^1.6.0",
7091 + "object-keys": "^1.1.1",
7092 + "path-parse": "^1.0.7",
7093 + "supports-preserve-symlinks-flag": "^1.0.0"
7094 + },
7095 + "bin": {
7096 + "resolve": "bin/resolve"
7097 + },
7098 + "engines": {
7099 + "node": ">= 0.4"
7100 + },
7101 + "funding": {
7102 + "url": "https://github.com/sponsors/ljharb"
7103 + }
7104 + },
7105 + "node_modules/resolve-from": {
7106 + "version": "4.0.0",
7107 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
7108 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
7109 + "dev": true,
7110 + "license": "MIT",
7111 + "engines": {
7112 + "node": ">=4"
7113 + }
7114 + },
7115 + "node_modules/resolve-pkg-maps": {
7116 + "version": "1.0.0",
7117 + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
7118 + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
7119 + "dev": true,
7120 + "license": "MIT",
7121 + "funding": {
7122 + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
7123 + }
7124 + },
7125 + "node_modules/reusify": {
7126 + "version": "1.1.0",
7127 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
7128 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
7129 + "dev": true,
7130 + "license": "MIT",
7131 + "engines": {
7132 + "iojs": ">=1.0.0",
7133 + "node": ">=0.10.0"
7134 + }
7135 + },
7136 + "node_modules/run-parallel": {
7137 + "version": "1.2.0",
7138 + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
7139 + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
7140 + "dev": true,
7141 + "funding": [
7142 + {
7143 + "type": "github",
7144 + "url": "https://github.com/sponsors/feross"
7145 + },
7146 + {
7147 + "type": "patreon",
7148 + "url": "https://www.patreon.com/feross"
7149 + },
7150 + {
7151 + "type": "consulting",
7152 + "url": "https://feross.org/support"
7153 + }
7154 + ],
7155 + "license": "MIT",
7156 + "dependencies": {
7157 + "queue-microtask": "^1.2.2"
7158 + }
7159 + },
7160 + "node_modules/safe-array-concat": {
7161 + "version": "1.1.4",
7162 + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
7163 + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
7164 + "dev": true,
7165 + "license": "MIT",
7166 + "dependencies": {
7167 + "call-bind": "^1.0.9",
7168 + "call-bound": "^1.0.4",
7169 + "get-intrinsic": "^1.3.0",
7170 + "has-symbols": "^1.1.0",
7171 + "isarray": "^2.0.5"
7172 + },
7173 + "engines": {
7174 + "node": ">=0.4"
7175 + },
7176 + "funding": {
7177 + "url": "https://github.com/sponsors/ljharb"
7178 + }
7179 + },
7180 + "node_modules/safe-push-apply": {
7181 + "version": "1.0.0",
7182 + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
7183 + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
7184 + "dev": true,
7185 + "license": "MIT",
7186 + "dependencies": {
7187 + "es-errors": "^1.3.0",
7188 + "isarray": "^2.0.5"
7189 + },
7190 + "engines": {
7191 + "node": ">= 0.4"
7192 + },
7193 + "funding": {
7194 + "url": "https://github.com/sponsors/ljharb"
7195 + }
7196 + },
7197 + "node_modules/safe-regex-test": {
7198 + "version": "1.1.0",
7199 + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
7200 + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
7201 + "dev": true,
7202 + "license": "MIT",
7203 + "dependencies": {
7204 + "call-bound": "^1.0.2",
7205 + "es-errors": "^1.3.0",
7206 + "is-regex": "^1.2.1"
7207 + },
7208 + "engines": {
7209 + "node": ">= 0.4"
7210 + },
7211 + "funding": {
7212 + "url": "https://github.com/sponsors/ljharb"
7213 + }
7214 + },
7215 + "node_modules/scheduler": {
7216 + "version": "0.27.0",
7217 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
7218 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
7219 + "license": "MIT"
7220 + },
7221 + "node_modules/semver": {
7222 + "version": "6.3.1",
7223 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
7224 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
7225 + "dev": true,
7226 + "license": "ISC",
7227 + "bin": {
7228 + "semver": "bin/semver.js"
7229 + }
7230 + },
7231 + "node_modules/set-function-length": {
7232 + "version": "1.2.2",
7233 + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
7234 + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
7235 + "dev": true,
7236 + "license": "MIT",
7237 + "dependencies": {
7238 + "define-data-property": "^1.1.4",
7239 + "es-errors": "^1.3.0",
7240 + "function-bind": "^1.1.2",
7241 + "get-intrinsic": "^1.2.4",
7242 + "gopd": "^1.0.1",
7243 + "has-property-descriptors": "^1.0.2"
7244 + },
7245 + "engines": {
7246 + "node": ">= 0.4"
7247 + }
7248 + },
7249 + "node_modules/set-function-name": {
7250 + "version": "2.0.2",
7251 + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
7252 + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
7253 + "dev": true,
7254 + "license": "MIT",
7255 + "dependencies": {
7256 + "define-data-property": "^1.1.4",
7257 + "es-errors": "^1.3.0",
7258 + "functions-have-names": "^1.2.3",
7259 + "has-property-descriptors": "^1.0.2"
7260 + },
7261 + "engines": {
7262 + "node": ">= 0.4"
7263 + }
7264 + },
7265 + "node_modules/set-proto": {
7266 + "version": "1.0.0",
7267 + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
7268 + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
7269 + "dev": true,
7270 + "license": "MIT",
7271 + "dependencies": {
7272 + "dunder-proto": "^1.0.1",
7273 + "es-errors": "^1.3.0",
7274 + "es-object-atoms": "^1.0.0"
7275 + },
7276 + "engines": {
7277 + "node": ">= 0.4"
7278 + }
7279 + },
7280 + "node_modules/sharp": {
7281 + "version": "0.35.3",
7282 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
7283 + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
7284 + "license": "Apache-2.0",
7285 + "optional": true,
7286 + "dependencies": {
7287 + "@img/colour": "^1.1.0",
7288 + "detect-libc": "^2.1.2",
7289 + "semver": "^7.8.5"
7290 + },
7291 + "engines": {
7292 + "node": ">=20.9.0"
7293 + },
7294 + "funding": {
7295 + "url": "https://opencollective.com/libvips"
7296 + },
7297 + "optionalDependencies": {
7298 + "@img/sharp-darwin-arm64": "0.35.3",
7299 + "@img/sharp-darwin-x64": "0.35.3",
7300 + "@img/sharp-freebsd-wasm32": "0.35.3",
7301 + "@img/sharp-libvips-darwin-arm64": "1.3.2",
7302 + "@img/sharp-libvips-darwin-x64": "1.3.2",
7303 + "@img/sharp-libvips-linux-arm": "1.3.2",
7304 + "@img/sharp-libvips-linux-arm64": "1.3.2",
7305 + "@img/sharp-libvips-linux-ppc64": "1.3.2",
7306 + "@img/sharp-libvips-linux-riscv64": "1.3.2",
7307 + "@img/sharp-libvips-linux-s390x": "1.3.2",
7308 + "@img/sharp-libvips-linux-x64": "1.3.2",
7309 + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
7310 + "@img/sharp-libvips-linuxmusl-x64": "1.3.2",
7311 + "@img/sharp-linux-arm": "0.35.3",
7312 + "@img/sharp-linux-arm64": "0.35.3",
7313 + "@img/sharp-linux-ppc64": "0.35.3",
7314 + "@img/sharp-linux-riscv64": "0.35.3",
7315 + "@img/sharp-linux-s390x": "0.35.3",
7316 + "@img/sharp-linux-x64": "0.35.3",
7317 + "@img/sharp-linuxmusl-arm64": "0.35.3",
7318 + "@img/sharp-linuxmusl-x64": "0.35.3",
7319 + "@img/sharp-webcontainers-wasm32": "0.35.3",
7320 + "@img/sharp-win32-arm64": "0.35.3",
7321 + "@img/sharp-win32-ia32": "0.35.3",
7322 + "@img/sharp-win32-x64": "0.35.3"
7323 + },
7324 + "peerDependenciesMeta": {
7325 + "@types/node": {
7326 + "optional": true
7327 + }
7328 + }
7329 + },
7330 + "node_modules/sharp/node_modules/semver": {
7331 + "version": "7.8.5",
7332 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
7333 + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
7334 + "license": "ISC",
7335 + "optional": true,
7336 + "bin": {
7337 + "semver": "bin/semver.js"
7338 + },
7339 + "engines": {
7340 + "node": ">=10"
7341 + }
7342 + },
7343 + "node_modules/shebang-command": {
7344 + "version": "2.0.0",
7345 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
7346 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
7347 + "dev": true,
7348 + "license": "MIT",
7349 + "dependencies": {
7350 + "shebang-regex": "^3.0.0"
7351 + },
7352 + "engines": {
7353 + "node": ">=8"
7354 + }
7355 + },
7356 + "node_modules/shebang-regex": {
7357 + "version": "3.0.0",
7358 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
7359 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
7360 + "dev": true,
7361 + "license": "MIT",
7362 + "engines": {
7363 + "node": ">=8"
7364 + }
7365 + },
7366 + "node_modules/side-channel": {
7367 + "version": "1.1.1",
7368 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
7369 + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
7370 + "dev": true,
7371 + "license": "MIT",
7372 + "dependencies": {
7373 + "es-errors": "^1.3.0",
7374 + "object-inspect": "^1.13.4",
7375 + "side-channel-list": "^1.0.1",
7376 + "side-channel-map": "^1.0.1",
7377 + "side-channel-weakmap": "^1.0.2"
7378 + },
7379 + "engines": {
7380 + "node": ">= 0.4"
7381 + },
7382 + "funding": {
7383 + "url": "https://github.com/sponsors/ljharb"
7384 + }
7385 + },
7386 + "node_modules/side-channel-list": {
7387 + "version": "1.0.1",
7388 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
7389 + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
7390 + "dev": true,
7391 + "license": "MIT",
7392 + "dependencies": {
7393 + "es-errors": "^1.3.0",
7394 + "object-inspect": "^1.13.4"
7395 + },
7396 + "engines": {
7397 + "node": ">= 0.4"
7398 + },
7399 + "funding": {
7400 + "url": "https://github.com/sponsors/ljharb"
7401 + }
7402 + },
7403 + "node_modules/side-channel-map": {
7404 + "version": "1.0.1",
7405 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
7406 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
7407 + "dev": true,
7408 + "license": "MIT",
7409 + "dependencies": {
7410 + "call-bound": "^1.0.2",
7411 + "es-errors": "^1.3.0",
7412 + "get-intrinsic": "^1.2.5",
7413 + "object-inspect": "^1.13.3"
7414 + },
7415 + "engines": {
7416 + "node": ">= 0.4"
7417 + },
7418 + "funding": {
7419 + "url": "https://github.com/sponsors/ljharb"
7420 + }
7421 + },
7422 + "node_modules/side-channel-weakmap": {
7423 + "version": "1.0.2",
7424 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
7425 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
7426 + "dev": true,
7427 + "license": "MIT",
7428 + "dependencies": {
7429 + "call-bound": "^1.0.2",
7430 + "es-errors": "^1.3.0",
7431 + "get-intrinsic": "^1.2.5",
7432 + "object-inspect": "^1.13.3",
7433 + "side-channel-map": "^1.0.1"
7434 + },
7435 + "engines": {
7436 + "node": ">= 0.4"
7437 + },
7438 + "funding": {
7439 + "url": "https://github.com/sponsors/ljharb"
7440 + }
7441 + },
7442 + "node_modules/source-map-js": {
7443 + "version": "1.2.1",
7444 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
7445 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
7446 + "license": "BSD-3-Clause",
7447 + "engines": {
7448 + "node": ">=0.10.0"
7449 + }
7450 + },
7451 + "node_modules/space-separated-tokens": {
7452 + "version": "2.0.2",
7453 + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
7454 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
7455 + "license": "MIT",
7456 + "funding": {
7457 + "type": "github",
7458 + "url": "https://github.com/sponsors/wooorm"
7459 + }
7460 + },
7461 + "node_modules/stable-hash": {
7462 + "version": "0.0.5",
7463 + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
7464 + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
7465 + "dev": true,
7466 + "license": "MIT"
7467 + },
7468 + "node_modules/stop-iteration-iterator": {
7469 + "version": "1.1.0",
7470 + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
7471 + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
7472 + "dev": true,
7473 + "license": "MIT",
7474 + "dependencies": {
7475 + "es-errors": "^1.3.0",
7476 + "internal-slot": "^1.1.0"
7477 + },
7478 + "engines": {
7479 + "node": ">= 0.4"
7480 + }
7481 + },
7482 + "node_modules/string.prototype.includes": {
7483 + "version": "2.0.1",
7484 + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
7485 + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
7486 + "dev": true,
7487 + "license": "MIT",
7488 + "dependencies": {
7489 + "call-bind": "^1.0.7",
7490 + "define-properties": "^1.2.1",
7491 + "es-abstract": "^1.23.3"
7492 + },
7493 + "engines": {
7494 + "node": ">= 0.4"
7495 + }
7496 + },
7497 + "node_modules/string.prototype.matchall": {
7498 + "version": "4.0.12",
7499 + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
7500 + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
7501 + "dev": true,
7502 + "license": "MIT",
7503 + "dependencies": {
7504 + "call-bind": "^1.0.8",
7505 + "call-bound": "^1.0.3",
7506 + "define-properties": "^1.2.1",
7507 + "es-abstract": "^1.23.6",
7508 + "es-errors": "^1.3.0",
7509 + "es-object-atoms": "^1.0.0",
7510 + "get-intrinsic": "^1.2.6",
7511 + "gopd": "^1.2.0",
7512 + "has-symbols": "^1.1.0",
7513 + "internal-slot": "^1.1.0",
7514 + "regexp.prototype.flags": "^1.5.3",
7515 + "set-function-name": "^2.0.2",
7516 + "side-channel": "^1.1.0"
7517 + },
7518 + "engines": {
7519 + "node": ">= 0.4"
7520 + },
7521 + "funding": {
7522 + "url": "https://github.com/sponsors/ljharb"
7523 + }
7524 + },
7525 + "node_modules/string.prototype.repeat": {
7526 + "version": "1.0.0",
7527 + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
7528 + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
7529 + "dev": true,
7530 + "license": "MIT",
7531 + "dependencies": {
7532 + "define-properties": "^1.1.3",
7533 + "es-abstract": "^1.17.5"
7534 + }
7535 + },
7536 + "node_modules/string.prototype.trim": {
7537 + "version": "1.2.11",
7538 + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz",
7539 + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==",
7540 + "dev": true,
7541 + "license": "MIT",
7542 + "dependencies": {
7543 + "call-bind": "^1.0.9",
7544 + "call-bound": "^1.0.4",
7545 + "define-data-property": "^1.1.4",
7546 + "define-properties": "^1.2.1",
7547 + "es-abstract": "^1.24.2",
7548 + "es-object-atoms": "^1.1.2",
7549 + "has-property-descriptors": "^1.0.2",
7550 + "safe-regex-test": "^1.1.0"
7551 + },
7552 + "engines": {
7553 + "node": ">= 0.4"
7554 + },
7555 + "funding": {
7556 + "url": "https://github.com/sponsors/ljharb"
7557 + }
7558 + },
7559 + "node_modules/string.prototype.trimend": {
7560 + "version": "1.0.10",
7561 + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz",
7562 + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==",
7563 + "dev": true,
7564 + "license": "MIT",
7565 + "dependencies": {
7566 + "call-bind": "^1.0.9",
7567 + "call-bound": "^1.0.4",
7568 + "define-properties": "^1.2.1",
7569 + "es-object-atoms": "^1.1.2"
7570 + },
7571 + "engines": {
7572 + "node": ">= 0.4"
7573 + },
7574 + "funding": {
7575 + "url": "https://github.com/sponsors/ljharb"
7576 + }
7577 + },
7578 + "node_modules/string.prototype.trimstart": {
7579 + "version": "1.0.8",
7580 + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
7581 + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
7582 + "dev": true,
7583 + "license": "MIT",
7584 + "dependencies": {
7585 + "call-bind": "^1.0.7",
7586 + "define-properties": "^1.2.1",
7587 + "es-object-atoms": "^1.0.0"
7588 + },
7589 + "engines": {
7590 + "node": ">= 0.4"
7591 + },
7592 + "funding": {
7593 + "url": "https://github.com/sponsors/ljharb"
7594 + }
7595 + },
7596 + "node_modules/stringify-entities": {
7597 + "version": "4.0.4",
7598 + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
7599 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
7600 + "license": "MIT",
7601 + "dependencies": {
7602 + "character-entities-html4": "^2.0.0",
7603 + "character-entities-legacy": "^3.0.0"
7604 + },
7605 + "funding": {
7606 + "type": "github",
7607 + "url": "https://github.com/sponsors/wooorm"
7608 + }
7609 + },
7610 + "node_modules/strip-bom": {
7611 + "version": "3.0.0",
7612 + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
7613 + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
7614 + "dev": true,
7615 + "license": "MIT",
7616 + "engines": {
7617 + "node": ">=4"
7618 + }
7619 + },
7620 + "node_modules/strip-json-comments": {
7621 + "version": "3.1.1",
7622 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
7623 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
7624 + "dev": true,
7625 + "license": "MIT",
7626 + "engines": {
7627 + "node": ">=8"
7628 + },
7629 + "funding": {
7630 + "url": "https://github.com/sponsors/sindresorhus"
7631 + }
7632 + },
7633 + "node_modules/style-to-js": {
7634 + "version": "1.1.21",
7635 + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
7636 + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
7637 + "license": "MIT",
7638 + "dependencies": {
7639 + "style-to-object": "1.0.14"
7640 + }
7641 + },
7642 + "node_modules/style-to-object": {
7643 + "version": "1.0.14",
7644 + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
7645 + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
7646 + "license": "MIT",
7647 + "dependencies": {
7648 + "inline-style-parser": "0.2.7"
7649 + }
7650 + },
7651 + "node_modules/styled-jsx": {
7652 + "version": "5.1.6",
7653 + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
7654 + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
7655 + "license": "MIT",
7656 + "dependencies": {
7657 + "client-only": "0.0.1"
7658 + },
7659 + "engines": {
7660 + "node": ">= 12.0.0"
7661 + },
7662 + "peerDependencies": {
7663 + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
7664 + },
7665 + "peerDependenciesMeta": {
7666 + "@babel/core": {
7667 + "optional": true
7668 + },
7669 + "babel-plugin-macros": {
7670 + "optional": true
7671 + }
7672 + }
7673 + },
7674 + "node_modules/supports-color": {
7675 + "version": "7.2.0",
7676 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
7677 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
7678 + "dev": true,
7679 + "license": "MIT",
7680 + "dependencies": {
7681 + "has-flag": "^4.0.0"
7682 + },
7683 + "engines": {
7684 + "node": ">=8"
7685 + }
7686 + },
7687 + "node_modules/supports-preserve-symlinks-flag": {
7688 + "version": "1.0.0",
7689 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
7690 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
7691 + "dev": true,
7692 + "license": "MIT",
7693 + "engines": {
7694 + "node": ">= 0.4"
7695 + },
7696 + "funding": {
7697 + "url": "https://github.com/sponsors/ljharb"
7698 + }
7699 + },
7700 + "node_modules/tinyglobby": {
7701 + "version": "0.2.17",
7702 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
7703 + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
7704 + "dev": true,
7705 + "license": "MIT",
7706 + "dependencies": {
7707 + "fdir": "^6.5.0",
7708 + "picomatch": "^4.0.4"
7709 + },
7710 + "engines": {
7711 + "node": ">=12.0.0"
7712 + },
7713 + "funding": {
7714 + "url": "https://github.com/sponsors/SuperchupuDev"
7715 + }
7716 + },
7717 + "node_modules/tinyglobby/node_modules/fdir": {
7718 + "version": "6.5.0",
7719 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
7720 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
7721 + "dev": true,
7722 + "license": "MIT",
7723 + "engines": {
7724 + "node": ">=12.0.0"
7725 + },
7726 + "peerDependencies": {
7727 + "picomatch": "^3 || ^4"
7728 + },
7729 + "peerDependenciesMeta": {
7730 + "picomatch": {
7731 + "optional": true
7732 + }
7733 + }
7734 + },
7735 + "node_modules/tinyglobby/node_modules/picomatch": {
7736 + "version": "4.0.5",
7737 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
7738 + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
7739 + "dev": true,
7740 + "license": "MIT",
7741 + "engines": {
7742 + "node": ">=12"
7743 + },
7744 + "funding": {
7745 + "url": "https://github.com/sponsors/jonschlinkert"
7746 + }
7747 + },
7748 + "node_modules/to-regex-range": {
7749 + "version": "5.0.1",
7750 + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
7751 + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
7752 + "dev": true,
7753 + "license": "MIT",
7754 + "dependencies": {
7755 + "is-number": "^7.0.0"
7756 + },
7757 + "engines": {
7758 + "node": ">=8.0"
7759 + }
7760 + },
7761 + "node_modules/trim-lines": {
7762 + "version": "3.0.1",
7763 + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
7764 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
7765 + "license": "MIT",
7766 + "funding": {
7767 + "type": "github",
7768 + "url": "https://github.com/sponsors/wooorm"
7769 + }
7770 + },
7771 + "node_modules/trough": {
7772 + "version": "2.2.0",
7773 + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
7774 + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
7775 + "license": "MIT",
7776 + "funding": {
7777 + "type": "github",
7778 + "url": "https://github.com/sponsors/wooorm"
7779 + }
7780 + },
7781 + "node_modules/ts-api-utils": {
7782 + "version": "2.5.0",
7783 + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
7784 + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
7785 + "dev": true,
7786 + "license": "MIT",
7787 + "engines": {
7788 + "node": ">=18.12"
7789 + },
7790 + "peerDependencies": {
7791 + "typescript": ">=4.8.4"
7792 + }
7793 + },
7794 + "node_modules/tsconfig-paths": {
7795 + "version": "3.15.0",
7796 + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
7797 + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
7798 + "dev": true,
7799 + "license": "MIT",
7800 + "dependencies": {
7801 + "@types/json5": "^0.0.29",
7802 + "json5": "^1.0.2",
7803 + "minimist": "^1.2.6",
7804 + "strip-bom": "^3.0.0"
7805 + }
7806 + },
7807 + "node_modules/tsconfig-paths/node_modules/json5": {
7808 + "version": "1.0.2",
7809 + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
7810 + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
7811 + "dev": true,
7812 + "license": "MIT",
7813 + "dependencies": {
7814 + "minimist": "^1.2.0"
7815 + },
7816 + "bin": {
7817 + "json5": "lib/cli.js"
7818 + }
7819 + },
7820 + "node_modules/tslib": {
7821 + "version": "2.8.1",
7822 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
7823 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
7824 + "license": "0BSD"
7825 + },
7826 + "node_modules/type-check": {
7827 + "version": "0.4.0",
7828 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
7829 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
7830 + "dev": true,
7831 + "license": "MIT",
7832 + "dependencies": {
7833 + "prelude-ls": "^1.2.1"
7834 + },
7835 + "engines": {
7836 + "node": ">= 0.8.0"
7837 + }
7838 + },
7839 + "node_modules/typed-array-buffer": {
7840 + "version": "1.0.3",
7841 + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
7842 + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
7843 + "dev": true,
7844 + "license": "MIT",
7845 + "dependencies": {
7846 + "call-bound": "^1.0.3",
7847 + "es-errors": "^1.3.0",
7848 + "is-typed-array": "^1.1.14"
7849 + },
7850 + "engines": {
7851 + "node": ">= 0.4"
7852 + }
7853 + },
7854 + "node_modules/typed-array-byte-length": {
7855 + "version": "1.0.3",
7856 + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
7857 + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
7858 + "dev": true,
7859 + "license": "MIT",
7860 + "dependencies": {
7861 + "call-bind": "^1.0.8",
7862 + "for-each": "^0.3.3",
7863 + "gopd": "^1.2.0",
7864 + "has-proto": "^1.2.0",
7865 + "is-typed-array": "^1.1.14"
7866 + },
7867 + "engines": {
7868 + "node": ">= 0.4"
7869 + },
7870 + "funding": {
7871 + "url": "https://github.com/sponsors/ljharb"
7872 + }
7873 + },
7874 + "node_modules/typed-array-byte-offset": {
7875 + "version": "1.0.4",
7876 + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
7877 + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
7878 + "dev": true,
7879 + "license": "MIT",
7880 + "dependencies": {
7881 + "available-typed-arrays": "^1.0.7",
7882 + "call-bind": "^1.0.8",
7883 + "for-each": "^0.3.3",
7884 + "gopd": "^1.2.0",
7885 + "has-proto": "^1.2.0",
7886 + "is-typed-array": "^1.1.15",
7887 + "reflect.getprototypeof": "^1.0.9"
7888 + },
7889 + "engines": {
7890 + "node": ">= 0.4"
7891 + },
7892 + "funding": {
7893 + "url": "https://github.com/sponsors/ljharb"
7894 + }
7895 + },
7896 + "node_modules/typed-array-length": {
7897 + "version": "1.0.8",
7898 + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz",
7899 + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==",
7900 + "dev": true,
7901 + "license": "MIT",
7902 + "dependencies": {
7903 + "call-bind": "^1.0.9",
7904 + "for-each": "^0.3.5",
7905 + "gopd": "^1.2.0",
7906 + "is-typed-array": "^1.1.15",
7907 + "possible-typed-array-names": "^1.1.0",
7908 + "reflect.getprototypeof": "^1.0.10"
7909 + },
7910 + "engines": {
7911 + "node": ">= 0.4"
7912 + },
7913 + "funding": {
7914 + "url": "https://github.com/sponsors/ljharb"
7915 + }
7916 + },
7917 + "node_modules/typescript": {
7918 + "version": "5.9.3",
7919 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
7920 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
7921 + "dev": true,
7922 + "license": "Apache-2.0",
7923 + "bin": {
7924 + "tsc": "bin/tsc",
7925 + "tsserver": "bin/tsserver"
7926 + },
7927 + "engines": {
7928 + "node": ">=14.17"
7929 + }
7930 + },
7931 + "node_modules/typescript-eslint": {
7932 + "version": "8.67.0",
7933 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz",
7934 + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==",
7935 + "dev": true,
7936 + "license": "MIT",
7937 + "dependencies": {
7938 + "@typescript-eslint/eslint-plugin": "8.67.0",
7939 + "@typescript-eslint/parser": "8.67.0",
7940 + "@typescript-eslint/typescript-estree": "8.67.0",
7941 + "@typescript-eslint/utils": "8.67.0"
7942 + },
7943 + "engines": {
7944 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
7945 + },
7946 + "funding": {
7947 + "type": "opencollective",
7948 + "url": "https://opencollective.com/typescript-eslint"
7949 + },
7950 + "peerDependencies": {
7951 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
7952 + "typescript": ">=4.8.4 <6.1.0"
7953 + }
7954 + },
7955 + "node_modules/unbox-primitive": {
7956 + "version": "1.1.0",
7957 + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
7958 + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
7959 + "dev": true,
7960 + "license": "MIT",
7961 + "dependencies": {
7962 + "call-bound": "^1.0.3",
7963 + "has-bigints": "^1.0.2",
7964 + "has-symbols": "^1.1.0",
7965 + "which-boxed-primitive": "^1.1.1"
7966 + },
7967 + "engines": {
7968 + "node": ">= 0.4"
7969 + },
7970 + "funding": {
7971 + "url": "https://github.com/sponsors/ljharb"
7972 + }
7973 + },
7974 + "node_modules/undici-types": {
7975 + "version": "6.21.0",
7976 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
7977 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
7978 + "dev": true,
7979 + "license": "MIT"
7980 + },
7981 + "node_modules/unified": {
7982 + "version": "11.0.5",
7983 + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
7984 + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
7985 + "license": "MIT",
7986 + "dependencies": {
7987 + "@types/unist": "^3.0.0",
7988 + "bail": "^2.0.0",
7989 + "devlop": "^1.0.0",
7990 + "extend": "^3.0.0",
7991 + "is-plain-obj": "^4.0.0",
7992 + "trough": "^2.0.0",
7993 + "vfile": "^6.0.0"
7994 + },
7995 + "funding": {
7996 + "type": "opencollective",
7997 + "url": "https://opencollective.com/unified"
7998 + }
7999 + },
8000 + "node_modules/unist-util-find-after": {
8001 + "version": "5.0.0",
8002 + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
8003 + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
8004 + "license": "MIT",
8005 + "dependencies": {
8006 + "@types/unist": "^3.0.0",
8007 + "unist-util-is": "^6.0.0"
8008 + },
8009 + "funding": {
8010 + "type": "opencollective",
8011 + "url": "https://opencollective.com/unified"
8012 + }
8013 + },
8014 + "node_modules/unist-util-is": {
8015 + "version": "6.0.1",
8016 + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
8017 + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
8018 + "license": "MIT",
8019 + "dependencies": {
8020 + "@types/unist": "^3.0.0"
8021 + },
8022 + "funding": {
8023 + "type": "opencollective",
8024 + "url": "https://opencollective.com/unified"
8025 + }
8026 + },
8027 + "node_modules/unist-util-position": {
8028 + "version": "5.0.0",
8029 + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
8030 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
8031 + "license": "MIT",
8032 + "dependencies": {
8033 + "@types/unist": "^3.0.0"
8034 + },
8035 + "funding": {
8036 + "type": "opencollective",
8037 + "url": "https://opencollective.com/unified"
8038 + }
8039 + },
8040 + "node_modules/unist-util-remove-position": {
8041 + "version": "5.0.0",
8042 + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
8043 + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
8044 + "license": "MIT",
8045 + "dependencies": {
8046 + "@types/unist": "^3.0.0",
8047 + "unist-util-visit": "^5.0.0"
8048 + },
8049 + "funding": {
8050 + "type": "opencollective",
8051 + "url": "https://opencollective.com/unified"
8052 + }
8053 + },
8054 + "node_modules/unist-util-stringify-position": {
8055 + "version": "4.0.0",
8056 + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
8057 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
8058 + "license": "MIT",
8059 + "dependencies": {
8060 + "@types/unist": "^3.0.0"
8061 + },
8062 + "funding": {
8063 + "type": "opencollective",
8064 + "url": "https://opencollective.com/unified"
8065 + }
8066 + },
8067 + "node_modules/unist-util-visit": {
8068 + "version": "5.1.0",
8069 + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
8070 + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
8071 + "license": "MIT",
8072 + "dependencies": {
8073 + "@types/unist": "^3.0.0",
8074 + "unist-util-is": "^6.0.0",
8075 + "unist-util-visit-parents": "^6.0.0"
8076 + },
8077 + "funding": {
8078 + "type": "opencollective",
8079 + "url": "https://opencollective.com/unified"
8080 + }
8081 + },
8082 + "node_modules/unist-util-visit-parents": {
8083 + "version": "6.0.2",
8084 + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
8085 + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
8086 + "license": "MIT",
8087 + "dependencies": {
8088 + "@types/unist": "^3.0.0",
8089 + "unist-util-is": "^6.0.0"
8090 + },
8091 + "funding": {
8092 + "type": "opencollective",
8093 + "url": "https://opencollective.com/unified"
8094 + }
8095 + },
8096 + "node_modules/unrs-resolver": {
8097 + "version": "1.12.2",
8098 + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
8099 + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==",
8100 + "dev": true,
8101 + "hasInstallScript": true,
8102 + "license": "MIT",
8103 + "dependencies": {
8104 + "napi-postinstall": "^0.3.4"
8105 + },
8106 + "funding": {
8107 + "url": "https://opencollective.com/unrs-resolver"
8108 + },
8109 + "optionalDependencies": {
8110 + "@unrs/resolver-binding-android-arm-eabi": "1.12.2",
8111 + "@unrs/resolver-binding-android-arm64": "1.12.2",
8112 + "@unrs/resolver-binding-darwin-arm64": "1.12.2",
8113 + "@unrs/resolver-binding-darwin-x64": "1.12.2",
8114 + "@unrs/resolver-binding-freebsd-x64": "1.12.2",
8115 + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2",
8116 + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2",
8117 + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2",
8118 + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2",
8119 + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2",
8120 + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2",
8121 + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2",
8122 + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2",
8123 + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2",
8124 + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2",
8125 + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2",
8126 + "@unrs/resolver-binding-linux-x64-musl": "1.12.2",
8127 + "@unrs/resolver-binding-openharmony-arm64": "1.12.2",
8128 + "@unrs/resolver-binding-wasm32-wasi": "1.12.2",
8129 + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2",
8130 + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2",
8131 + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2"
8132 + }
8133 + },
8134 + "node_modules/update-browserslist-db": {
8135 + "version": "1.3.1",
8136 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
8137 + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==",
8138 + "dev": true,
8139 + "funding": [
8140 + {
8141 + "type": "opencollective",
8142 + "url": "https://opencollective.com/browserslist"
8143 + },
8144 + {
8145 + "type": "tidelift",
8146 + "url": "https://tidelift.com/funding/github/npm/browserslist"
8147 + },
8148 + {
8149 + "type": "github",
8150 + "url": "https://github.com/sponsors/ai"
8151 + }
8152 + ],
8153 + "license": "MIT",
8154 + "dependencies": {
8155 + "escalade": "^3.2.0",
8156 + "picocolors": "^1.1.1"
8157 + },
8158 + "bin": {
8159 + "update-browserslist-db": "cli.js"
8160 + },
8161 + "peerDependencies": {
8162 + "browserslist": ">= 4.21.0"
8163 + }
8164 + },
8165 + "node_modules/uri-js": {
8166 + "version": "4.4.1",
8167 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
8168 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
8169 + "dev": true,
8170 + "license": "BSD-2-Clause",
8171 + "dependencies": {
8172 + "punycode": "^2.1.0"
8173 + }
8174 + },
8175 + "node_modules/vfile": {
8176 + "version": "6.0.3",
8177 + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
8178 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
8179 + "license": "MIT",
8180 + "dependencies": {
8181 + "@types/unist": "^3.0.0",
8182 + "vfile-message": "^4.0.0"
8183 + },
8184 + "funding": {
8185 + "type": "opencollective",
8186 + "url": "https://opencollective.com/unified"
8187 + }
8188 + },
8189 + "node_modules/vfile-location": {
8190 + "version": "5.0.3",
8191 + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
8192 + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
8193 + "license": "MIT",
8194 + "dependencies": {
8195 + "@types/unist": "^3.0.0",
8196 + "vfile": "^6.0.0"
8197 + },
8198 + "funding": {
8199 + "type": "opencollective",
8200 + "url": "https://opencollective.com/unified"
8201 + }
8202 + },
8203 + "node_modules/vfile-message": {
8204 + "version": "4.0.3",
8205 + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
8206 + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
8207 + "license": "MIT",
8208 + "dependencies": {
8209 + "@types/unist": "^3.0.0",
8210 + "unist-util-stringify-position": "^4.0.0"
8211 + },
8212 + "funding": {
8213 + "type": "opencollective",
8214 + "url": "https://opencollective.com/unified"
8215 + }
8216 + },
8217 + "node_modules/web-namespaces": {
8218 + "version": "2.0.1",
8219 + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
8220 + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
8221 + "license": "MIT",
8222 + "funding": {
8223 + "type": "github",
8224 + "url": "https://github.com/sponsors/wooorm"
8225 + }
8226 + },
8227 + "node_modules/which": {
8228 + "version": "2.0.2",
8229 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
8230 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
8231 + "dev": true,
8232 + "license": "ISC",
8233 + "dependencies": {
8234 + "isexe": "^2.0.0"
8235 + },
8236 + "bin": {
8237 + "node-which": "bin/node-which"
8238 + },
8239 + "engines": {
8240 + "node": ">= 8"
8241 + }
8242 + },
8243 + "node_modules/which-boxed-primitive": {
8244 + "version": "1.1.1",
8245 + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
8246 + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
8247 + "dev": true,
8248 + "license": "MIT",
8249 + "dependencies": {
8250 + "is-bigint": "^1.1.0",
8251 + "is-boolean-object": "^1.2.1",
8252 + "is-number-object": "^1.1.1",
8253 + "is-string": "^1.1.1",
8254 + "is-symbol": "^1.1.1"
8255 + },
8256 + "engines": {
8257 + "node": ">= 0.4"
8258 + },
8259 + "funding": {
8260 + "url": "https://github.com/sponsors/ljharb"
8261 + }
8262 + },
8263 + "node_modules/which-builtin-type": {
8264 + "version": "1.2.1",
8265 + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
8266 + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
8267 + "dev": true,
8268 + "license": "MIT",
8269 + "dependencies": {
8270 + "call-bound": "^1.0.2",
8271 + "function.prototype.name": "^1.1.6",
8272 + "has-tostringtag": "^1.0.2",
8273 + "is-async-function": "^2.0.0",
8274 + "is-date-object": "^1.1.0",
8275 + "is-finalizationregistry": "^1.1.0",
8276 + "is-generator-function": "^1.0.10",
8277 + "is-regex": "^1.2.1",
8278 + "is-weakref": "^1.0.2",
8279 + "isarray": "^2.0.5",
8280 + "which-boxed-primitive": "^1.1.0",
8281 + "which-collection": "^1.0.2",
8282 + "which-typed-array": "^1.1.16"
8283 + },
8284 + "engines": {
8285 + "node": ">= 0.4"
8286 + },
8287 + "funding": {
8288 + "url": "https://github.com/sponsors/ljharb"
8289 + }
8290 + },
8291 + "node_modules/which-collection": {
8292 + "version": "1.0.2",
8293 + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
8294 + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
8295 + "dev": true,
8296 + "license": "MIT",
8297 + "dependencies": {
8298 + "is-map": "^2.0.3",
8299 + "is-set": "^2.0.3",
8300 + "is-weakmap": "^2.0.2",
8301 + "is-weakset": "^2.0.3"
8302 + },
8303 + "engines": {
8304 + "node": ">= 0.4"
8305 + },
8306 + "funding": {
8307 + "url": "https://github.com/sponsors/ljharb"
8308 + }
8309 + },
8310 + "node_modules/which-typed-array": {
8311 + "version": "1.1.22",
8312 + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
8313 + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
8314 + "dev": true,
8315 + "license": "MIT",
8316 + "dependencies": {
8317 + "available-typed-arrays": "^1.0.7",
8318 + "call-bind": "^1.0.9",
8319 + "call-bound": "^1.0.4",
8320 + "for-each": "^0.3.5",
8321 + "get-proto": "^1.0.1",
8322 + "gopd": "^1.2.0",
8323 + "has-tostringtag": "^1.0.2"
8324 + },
8325 + "engines": {
8326 + "node": ">= 0.4"
8327 + },
8328 + "funding": {
8329 + "url": "https://github.com/sponsors/ljharb"
8330 + }
8331 + },
8332 + "node_modules/word-wrap": {
8333 + "version": "1.2.5",
8334 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
8335 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
8336 + "dev": true,
8337 + "license": "MIT",
8338 + "engines": {
8339 + "node": ">=0.10.0"
8340 + }
8341 + },
8342 + "node_modules/yallist": {
8343 + "version": "3.1.1",
8344 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
8345 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
8346 + "dev": true,
8347 + "license": "ISC"
8348 + },
8349 + "node_modules/yocto-queue": {
8350 + "version": "0.1.0",
8351 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
8352 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
8353 + "dev": true,
8354 + "license": "MIT",
8355 + "engines": {
8356 + "node": ">=10"
8357 + },
8358 + "funding": {
8359 + "url": "https://github.com/sponsors/sindresorhus"
8360 + }
8361 + },
8362 + "node_modules/zod": {
8363 + "version": "4.4.3",
8364 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
8365 + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
8366 + "dev": true,
8367 + "license": "MIT",
8368 + "funding": {
8369 + "url": "https://github.com/sponsors/colinhacks"
8370 + }
8371 + },
8372 + "node_modules/zod-validation-error": {
8373 + "version": "4.0.2",
8374 + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
8375 + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
8376 + "dev": true,
8377 + "license": "MIT",
8378 + "engines": {
8379 + "node": ">=18.0.0"
8380 + },
8381 + "peerDependencies": {
8382 + "zod": "^3.25.0 || ^4.0.0"
8383 + }
8384 + },
8385 + "node_modules/zwitch": {
8386 + "version": "2.0.4",
8387 + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
8388 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
8389 + "license": "MIT",
8390 + "funding": {
8391 + "type": "github",
8392 + "url": "https://github.com/sponsors/wooorm"
8393 + }
8394 + }
8395 + }
8396 +}
modified package.json +14 −2
@@ -6,14 +6,26 @@
6 6 "dev": "next dev",
7 7 "build": "next build",
8 8 "start": "next start",
9 "lint": "eslint"
9 + "lint": "eslint",
10 + "set-password": "node scripts/set-password.mjs",
11 + "make-icons": "node scripts/make-icons.mjs"
10 12 },
11 13 "dependencies": {
14 + "@node-rs/argon2": "^2.0.2",
15 + "better-sqlite3": "^13.0.3",
16 + "highlight.js": "^11.12.0",
17 + "katex": "^0.18.4",
12 18 "next": "16.3.0",
13 19 "react": "19.2.8",
14 "react-dom": "19.2.8"
20 + "react-dom": "19.2.8",
21 + "react-markdown": "^10.1.0",
22 + "rehype-highlight": "^7.0.2",
23 + "rehype-katex": "^7.0.1",
24 + "remark-gfm": "^4.0.1",
25 + "remark-math": "^6.0.0"
15 26 },
16 27 "devDependencies": {
28 + "@types/better-sqlite3": "^9.6.0",
17 29 "@types/node": "^20",
18 30 "@types/react": "^19",
19 31 "@types/react-dom": "^19",
deleted public/file.svg +0 −1
@@ -1 +0,0 @@
1 <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
\ No newline at end of file
deleted public/globe.svg +0 −1
@@ -1 +0,0 @@
1 <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
\ No newline at end of file
added public/icons/apple-touch-icon.png +0 −0

Binary file not shown.

added public/icons/icon-192.png +0 −0

Binary file not shown.

added public/icons/icon-512.png +0 −0

Binary file not shown.

added public/icons/icon-maskable-512.png +0 −0

Binary file not shown.

added public/icons/icon.svg +6 −0
@@ -0,0 +1,6 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2 + <rect width="512" height="512" rx="112" fill="#0E1116"/>
3 + <rect x="138" y="156" width="56" height="200" rx="12" fill="#3FB6A8"/>
4 + <rect x="228" y="156" width="56" height="200" rx="12" fill="none" stroke="#3FB6A8" stroke-width="14"/>
5 + <rect x="318" y="156" width="56" height="200" rx="12" fill="#3FB6A8"/>
6 +</svg>
added public/manifest.webmanifest +29 −0
@@ -0,0 +1,29 @@
1 +{
2 + "name": "chat.spboucher.ai",
3 + "short_name": "spboucher.ai",
4 + "description": "Personal instrument for operating hundreds of AI models.",
5 + "start_url": "/",
6 + "display": "standalone",
7 + "background_color": "#0E1116",
8 + "theme_color": "#0E1116",
9 + "icons": [
10 + {
11 + "src": "/icons/icon-192.png",
12 + "sizes": "192x192",
13 + "type": "image/png",
14 + "purpose": "any"
15 + },
16 + {
17 + "src": "/icons/icon-512.png",
18 + "sizes": "512x512",
19 + "type": "image/png",
20 + "purpose": "any"
21 + },
22 + {
23 + "src": "/icons/icon-maskable-512.png",
24 + "sizes": "512x512",
25 + "type": "image/png",
26 + "purpose": "maskable"
27 + }
28 + ]
29 +}
deleted public/next.svg +0 −1
@@ -1 +0,0 @@
1 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file
added public/sw.js +58 −0
@@ -0,0 +1,58 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Service worker: cache the app shell + static assets for instant cold starts.
6 +// Conversation data stays network-first — the server DB is the single source of truth.
7 +
8 +const CACHE = "spb-shell-v1";
9 +const SHELL = ["/manifest.webmanifest", "/icons/icon.svg"];
10 +
11 +self.addEventListener("install", (event) => {
12 + event.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)));
13 + self.skipWaiting();
14 +});
15 +
16 +self.addEventListener("activate", (event) => {
17 + event.waitUntil(
18 + caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
19 + );
20 + self.clients.claim();
21 +});
22 +
23 +self.addEventListener("fetch", (event) => {
24 + const url = new URL(event.request.url);
25 + if (event.request.method !== "GET") return;
26 +
27 + // Never cache API traffic or streams.
28 + if (url.pathname.startsWith("/api/")) return;
29 +
30 + // Static assets: cache-first (immutable Next chunks + icons).
31 + if (url.pathname.startsWith("/_next/static/") || url.pathname.startsWith("/icons/")) {
32 + event.respondWith(
33 + caches.match(event.request).then(
34 + (hit) =>
35 + hit ||
36 + fetch(event.request).then((res) => {
37 + const copy = res.clone();
38 + caches.open(CACHE).then((c) => c.put(event.request, copy));
39 + return res;
40 + })
41 + )
42 + );
43 + return;
44 + }
45 +
46 + // Pages: network-first with cache fallback so the shell opens offline.
47 + event.respondWith(
48 + fetch(event.request)
49 + .then((res) => {
50 + if (res.ok && (url.pathname === "/" || url.pathname === "/login")) {
51 + const copy = res.clone();
52 + caches.open(CACHE).then((c) => c.put(event.request, copy));
53 + }
54 + return res;
55 + })
56 + .catch(() => caches.match(event.request).then((hit) => hit || caches.match("/")))
57 + );
58 +});
deleted public/vercel.svg +0 −1
@@ -1 +0,0 @@
1 <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
\ No newline at end of file
deleted public/window.svg +0 −1
@@ -1 +0,0 @@
1 <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
\ No newline at end of file
added scripts/make-icons.mjs +106 −0
@@ -0,0 +1,106 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Dependency-free PNG icon generator: rasterizes the ▮▯▮ instrument mark
6 +// (three rounded bars, signal teal on ink) at the sizes the PWA needs.
7 +
8 +import { deflateSync } from "node:zlib";
9 +import { writeFileSync, mkdirSync } from "node:fs";
10 +
11 +const INK = [0x0e, 0x11, 0x16, 255];
12 +const TEAL = [0x3f, 0xb6, 0xa8, 255];
13 +
14 +function crc32(buf) {
15 + let table = crc32.table;
16 + if (!table) {
17 + table = crc32.table = new Int32Array(256);
18 + for (let n = 0; n < 256; n++) {
19 + let c = n;
20 + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
21 + table[n] = c;
22 + }
23 + }
24 + let c = 0xffffffff;
25 + for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
26 + return (c ^ 0xffffffff) >>> 0;
27 +}
28 +
29 +function chunk(type, data) {
30 + const len = Buffer.alloc(4);
31 + len.writeUInt32BE(data.length);
32 + const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
33 + const crc = Buffer.alloc(4);
34 + crc.writeUInt32BE(crc32(body));
35 + return Buffer.concat([len, body, crc]);
36 +}
37 +
38 +function encodePng(size, pixels) {
39 + const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
40 + const ihdr = Buffer.alloc(13);
41 + ihdr.writeUInt32BE(size, 0);
42 + ihdr.writeUInt32BE(size, 4);
43 + ihdr[8] = 8; // bit depth
44 + ihdr[9] = 6; // RGBA
45 + const raw = Buffer.alloc((size * 4 + 1) * size);
46 + for (let y = 0; y < size; y++) {
47 + raw[y * (size * 4 + 1)] = 0; // filter none
48 + pixels.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4);
49 + }
50 + return Buffer.concat([
51 + sig,
52 + chunk("IHDR", ihdr),
53 + chunk("IDAT", deflateSync(raw, { level: 9 })),
54 + chunk("IEND", Buffer.alloc(0)),
55 + ]);
56 +}
57 +
58 +function inRoundedRect(x, y, rx, ry, w, h, r) {
59 + if (x < rx || x >= rx + w || y < ry || y >= ry + h) return false;
60 + const cx = Math.max(rx + r, Math.min(x, rx + w - r));
61 + const cy = Math.max(ry + r, Math.min(y, ry + h - r));
62 + return (x - cx) ** 2 + (y - cy) ** 2 <= r * r || (x >= rx + r && x < rx + w - r) || (y >= ry + r && y < ry + h - r);
63 +}
64 +
65 +function drawIcon(size, { maskable = false } = {}) {
66 + const px = Buffer.alloc(size * size * 4);
67 + const s = size / 512;
68 + const bgRadius = maskable ? 0 : 112 * s;
69 + const bars = [
70 + { x: 138, filled: true },
71 + { x: 228, filled: false },
72 + { x: 318, filled: true },
73 + ];
74 + const stroke = 14 * s;
75 + for (let y = 0; y < size; y++) {
76 + for (let x = 0; x < size; x++) {
77 + let color = [0, 0, 0, 0];
78 + if (maskable || inRoundedRect(x, y, 0, 0, size, size, bgRadius)) color = INK;
79 + for (const b of bars) {
80 + const bx = b.x * s;
81 + const by = 156 * s;
82 + const bw = 56 * s;
83 + const bh = 200 * s;
84 + const br = 12 * s;
85 + if (inRoundedRect(x, y, bx, by, bw, bh, br)) {
86 + if (b.filled || !inRoundedRect(x, y, bx + stroke, by + stroke, bw - 2 * stroke, bh - 2 * stroke, Math.max(br - stroke, 0))) {
87 + color = TEAL;
88 + }
89 + }
90 + }
91 + const i = (y * size + x) * 4;
92 + px[i] = color[0];
93 + px[i + 1] = color[1];
94 + px[i + 2] = color[2];
95 + px[i + 3] = color[3];
96 + }
97 + }
98 + return encodePng(size, px);
99 +}
100 +
101 +mkdirSync("public/icons", { recursive: true });
102 +writeFileSync("public/icons/icon-192.png", drawIcon(192));
103 +writeFileSync("public/icons/icon-512.png", drawIcon(512));
104 +writeFileSync("public/icons/icon-maskable-512.png", drawIcon(512, { maskable: true }));
105 +writeFileSync("public/icons/apple-touch-icon.png", drawIcon(180, { maskable: true }));
106 +console.log("icons written");
added scripts/set-password.mjs +55 −0
@@ -0,0 +1,55 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Creates or updates the single user. Usage:
6 +// node scripts/set-password.mjs <username> [password]
7 +// If no password is given, a strong one is generated and printed once.
8 +
9 +import crypto from "node:crypto";
10 +import path from "node:path";
11 +import fs from "node:fs";
12 +import { createRequire } from "node:module";
13 +
14 +const require = createRequire(import.meta.url);
15 +const Database = require("better-sqlite3");
16 +const { hashSync } = require("@node-rs/argon2");
17 +
18 +const username = process.argv[2];
19 +const password = process.argv[3] ?? crypto.randomBytes(18).toString("base64url");
20 +if (!username) {
21 + console.error("Usage: node scripts/set-password.mjs <username> [password]");
22 + process.exit(1);
23 +}
24 +
25 +const dataDir = path.resolve(process.env.CHAT_DATA_DIR || "./data");
26 +fs.mkdirSync(dataDir, { recursive: true });
27 +const db = new Database(path.join(dataDir, "chat.db"));
28 +db.pragma("journal_mode = WAL");
29 +
30 +// Make sure the users table exists even before the app has booted once.
31 +db.exec(`CREATE TABLE IF NOT EXISTS users (
32 + id TEXT PRIMARY KEY,
33 + username TEXT NOT NULL UNIQUE,
34 + password_hash TEXT NOT NULL,
35 + totp_secret TEXT,
36 + created_at INTEGER NOT NULL,
37 + updated_at INTEGER NOT NULL
38 +)`);
39 +
40 +const hash = hashSync(password, { memoryCost: 65536, timeCost: 3, parallelism: 2 });
41 +const now = Date.now();
42 +const existing = db.prepare("SELECT id FROM users WHERE username = ?").get(username);
43 +if (existing) {
44 + db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?").run(hash, now, username);
45 + db.prepare("DELETE FROM sessions WHERE user_id = ?").run(existing.id);
46 + console.log(`Password updated for "${username}" (all sessions revoked).`);
47 +} else {
48 + db.prepare(
49 + "INSERT INTO users (id, username, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
50 + ).run(crypto.randomUUID(), username, hash, now, now);
51 + console.log(`User "${username}" created.`);
52 +}
53 +if (!process.argv[3]) {
54 + console.log(`Generated password (store it now, it is not shown again):\n${password}`);
55 +}
added src/app/api/auth/login/route.ts +48 −0
@@ -0,0 +1,48 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse, type NextRequest } from "next/server";
6 +import {
7 + verifyCredentials,
8 + createSession,
9 + loginRateLimited,
10 + recordLoginAttempt,
11 + sessionCookieHeader,
12 +} from "@/lib/auth/auth";
13 +import { clientIp } from "@/lib/auth/guard";
14 +
15 +export const runtime = "nodejs";
16 +
17 +export async function POST(req: NextRequest) {
18 + const ip = await clientIp();
19 + if (loginRateLimited(ip)) {
20 + return NextResponse.json(
21 + { error: "Too many attempts. Wait 15 minutes and try again." },
22 + { status: 429 }
23 + );
24 + }
25 +
26 + let body: { username?: string; password?: string };
27 + try {
28 + body = await req.json();
29 + } catch {
30 + return NextResponse.json({ error: "Invalid request." }, { status: 400 });
31 + }
32 + const username = String(body.username ?? "").trim();
33 + const password = String(body.password ?? "");
34 + if (!username || !password) {
35 + return NextResponse.json({ error: "Enter your username and password." }, { status: 400 });
36 + }
37 +
38 + const user = await verifyCredentials(username, password);
39 + recordLoginAttempt(ip, Boolean(user));
40 + if (!user) {
41 + return NextResponse.json({ error: "Wrong username or password." }, { status: 401 });
42 + }
43 +
44 + const token = createSession(user.id, req.headers.get("user-agent") ?? undefined, ip);
45 + const res = NextResponse.json({ ok: true, username: user.username });
46 + res.headers.set("Set-Cookie", sessionCookieHeader(token, 60 * 60 * 24 * 30));
47 + return res;
48 +}
added src/app/api/auth/logout/route.ts +20 −0
@@ -0,0 +1,20 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse } from "next/server";
6 +import { cookies } from "next/headers";
7 +import { destroySession, SESSION_COOKIE } from "@/lib/auth/auth";
8 +
9 +export const runtime = "nodejs";
10 +
11 +export async function POST() {
12 + const jar = await cookies();
13 + destroySession(jar.get(SESSION_COOKIE)?.value);
14 + const res = NextResponse.json({ ok: true });
15 + res.headers.set(
16 + "Set-Cookie",
17 + `${SESSION_COOKIE}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`
18 + );
19 + return res;
20 +}
added src/app/api/auth/session/route.ts +14 −0
@@ -0,0 +1,14 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +
8 +export const runtime = "nodejs";
9 +
10 +export async function GET() {
11 + const { session, unauthorized } = await requireSession();
12 + if (unauthorized) return unauthorized;
13 + return NextResponse.json({ username: session.username });
14 +}
added src/app/api/chat/route.ts +125 −0
@@ -0,0 +1,125 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse, type NextRequest } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +import { ensureCatalogFresh, getModel } from "@/lib/catalog";
8 +import {
9 + createConversation,
10 + getConversation,
11 + getMessage,
12 + insertMessage,
13 + updateConversation,
14 +} from "@/lib/conversations";
15 +import { startGeneration } from "@/lib/generate";
16 +
17 +export const runtime = "nodejs";
18 +export const dynamic = "force-dynamic";
19 +
20 +interface ChatRequestBody {
21 + conversationId?: string;
22 + content?: string;
23 + modelId?: string;
24 + /** Assistant message ID to regenerate: creates a sibling branch, never destroys the original. */
25 + regenerateOf?: string;
26 + /** Parent message for the new user message (branch editing); defaults to the current leaf. */
27 + parentId?: string | null;
28 +}
29 +
30 +export async function POST(req: NextRequest) {
31 + const { unauthorized } = await requireSession();
32 + if (unauthorized) return unauthorized;
33 +
34 + let body: ChatRequestBody;
35 + try {
36 + body = await req.json();
37 + } catch {
38 + return NextResponse.json({ error: "Invalid request." }, { status: 400 });
39 + }
40 +
41 + if (!body.modelId) {
42 + return NextResponse.json({ error: "Pick a model first." }, { status: 400 });
43 + }
44 + await ensureCatalogFresh().catch(() => {});
45 + const model = getModel(body.modelId);
46 + if (!model) {
47 + return NextResponse.json({ error: "Unknown model. Refresh the catalog." }, { status: 400 });
48 + }
49 +
50 + let parentMessage = null;
51 +
52 + if (body.regenerateOf) {
53 + // Regeneration: new assistant sibling under the same user message.
54 + const original = getMessage(body.regenerateOf);
55 + if (!original || original.role !== "assistant") {
56 + return NextResponse.json({ error: "Message to regenerate was not found." }, { status: 404 });
57 + }
58 + if (!getConversation(original.conversation_id)) {
59 + return NextResponse.json({ error: "Conversation not found." }, { status: 404 });
60 + }
61 + parentMessage = original.parent_id ? getMessage(original.parent_id) : null;
62 + const started = startGeneration({
63 + conversationId: original.conversation_id,
64 + parentMessage,
65 + model,
66 + });
67 + return sseResponse(started.stream);
68 + }
69 +
70 + const content = (body.content ?? "").trim();
71 + if (!content) {
72 + return NextResponse.json({ error: "Write a message first." }, { status: 400 });
73 + }
74 +
75 + // New message flow: resolve or create the conversation.
76 + let conversation = body.conversationId ? getConversation(body.conversationId) : null;
77 + if (body.conversationId && !conversation) {
78 + return NextResponse.json({ error: "Conversation not found." }, { status: 404 });
79 + }
80 + if (!conversation) {
81 + conversation = createConversation(content);
82 + } else if (conversation.title === "New conversation") {
83 + updateConversation(conversation.id, { title: content.slice(0, 80) });
84 + }
85 +
86 + const parentId =
87 + body.parentId !== undefined ? body.parentId : conversation.current_leaf_id;
88 + if (parentId) {
89 + const parent = getMessage(parentId);
90 + if (!parent || parent.conversation_id !== conversation.id) {
91 + return NextResponse.json({ error: "Invalid parent message." }, { status: 400 });
92 + }
93 + }
94 +
95 + const userMessage = insertMessage({
96 + conversationId: conversation.id,
97 + parentId: parentId ?? null,
98 + role: "user",
99 + content,
100 + status: "completed",
101 + });
102 +
103 + const started = startGeneration({
104 + conversationId: conversation.id,
105 + parentMessage: userMessage,
106 + model,
107 + });
108 +
109 + return sseResponse(started.stream, {
110 + "X-Conversation-Id": conversation.id,
111 + "X-User-Message-Id": userMessage.id,
112 + });
113 +}
114 +
115 +function sseResponse(stream: ReadableStream<Uint8Array>, extraHeaders?: Record<string, string>) {
116 + return new Response(stream, {
117 + headers: {
118 + "Content-Type": "text/event-stream; charset=utf-8",
119 + "Cache-Control": "no-cache, no-transform",
120 + Connection: "keep-alive",
121 + "X-Accel-Buffering": "no",
122 + ...extraHeaders,
123 + },
124 + });
125 +}
added src/app/api/conversations/[id]/route.ts +55 −0
@@ -0,0 +1,55 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse, type NextRequest } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +import {
8 + deleteConversation,
9 + getConversation,
10 + listMessages,
11 + getMessage,
12 + updateConversation,
13 +} from "@/lib/conversations";
14 +
15 +export const runtime = "nodejs";
16 +
17 +type Params = { params: Promise<{ id: string }> };
18 +
19 +export async function GET(_req: NextRequest, { params }: Params) {
20 + const { unauthorized } = await requireSession();
21 + if (unauthorized) return unauthorized;
22 + const { id } = await params;
23 + const conversation = getConversation(id);
24 + if (!conversation) return NextResponse.json({ error: "Conversation not found." }, { status: 404 });
25 + return NextResponse.json({ conversation, messages: listMessages(id) });
26 +}
27 +
28 +export async function PATCH(req: NextRequest, { params }: Params) {
29 + const { unauthorized } = await requireSession();
30 + if (unauthorized) return unauthorized;
31 + const { id } = await params;
32 + if (!getConversation(id)) return NextResponse.json({ error: "Conversation not found." }, { status: 404 });
33 + let body: { title?: string; pinned?: boolean; currentLeafId?: string | null };
34 + try {
35 + body = await req.json();
36 + } catch {
37 + return NextResponse.json({ error: "Invalid request." }, { status: 400 });
38 + }
39 + if (body.currentLeafId) {
40 + const leaf = getMessage(body.currentLeafId);
41 + if (!leaf || leaf.conversation_id !== id) {
42 + return NextResponse.json({ error: "Invalid leaf message." }, { status: 400 });
43 + }
44 + }
45 + updateConversation(id, body);
46 + return NextResponse.json({ conversation: getConversation(id) });
47 +}
48 +
49 +export async function DELETE(_req: NextRequest, { params }: Params) {
50 + const { unauthorized } = await requireSession();
51 + if (unauthorized) return unauthorized;
52 + const { id } = await params;
53 + deleteConversation(id);
54 + return NextResponse.json({ ok: true });
55 +}
added src/app/api/conversations/route.ts +15 −0
@@ -0,0 +1,15 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +import { listConversations } from "@/lib/conversations";
8 +
9 +export const runtime = "nodejs";
10 +
11 +export async function GET() {
12 + const { unauthorized } = await requireSession();
13 + if (unauthorized) return unauthorized;
14 + return NextResponse.json({ conversations: listConversations() });
15 +}
added src/app/api/generations/[id]/cancel/route.ts +25 −0
@@ -0,0 +1,25 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// "Stop" aborts the upstream OpenRouter stream — never fake cancellation.
6 +
7 +import { NextResponse, type NextRequest } from "next/server";
8 +import { requireSession } from "@/lib/auth/guard";
9 +import { cancelGeneration, generationState } from "@/lib/generate";
10 +
11 +export const runtime = "nodejs";
12 +
13 +type Params = { params: Promise<{ id: string }> };
14 +
15 +export async function POST(_req: NextRequest, { params }: Params) {
16 + const { unauthorized } = await requireSession();
17 + if (unauthorized) return unauthorized;
18 + const { id } = await params;
19 + const cancelled = cancelGeneration(id);
20 + const gen = generationState(id);
21 + if (!cancelled && !gen) {
22 + return NextResponse.json({ error: "Generation not found." }, { status: 404 });
23 + }
24 + return NextResponse.json({ ok: true, state: gen?.state ?? "cancelled" });
25 +}
added src/app/api/generations/[id]/route.ts +24 −0
@@ -0,0 +1,24 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Generation state resync — lets a client that lost its stream recover.
6 +
7 +import { NextResponse, type NextRequest } from "next/server";
8 +import { requireSession } from "@/lib/auth/guard";
9 +import { generationState } from "@/lib/generate";
10 +import { getMessage } from "@/lib/conversations";
11 +
12 +export const runtime = "nodejs";
13 +
14 +type Params = { params: Promise<{ id: string }> };
15 +
16 +export async function GET(_req: NextRequest, { params }: Params) {
17 + const { unauthorized } = await requireSession();
18 + if (unauthorized) return unauthorized;
19 + const { id } = await params;
20 + const gen = generationState(id);
21 + if (!gen) return NextResponse.json({ error: "Generation not found." }, { status: 404 });
22 + const message = getMessage(gen.message_id);
23 + return NextResponse.json({ generation: gen, message });
24 +}
added src/app/api/models/prefs/route.ts +25 −0
@@ -0,0 +1,25 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse, type NextRequest } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +import { setModelPref } from "@/lib/catalog";
8 +
9 +export const runtime = "nodejs";
10 +
11 +export async function POST(req: NextRequest) {
12 + const { unauthorized } = await requireSession();
13 + if (unauthorized) return unauthorized;
14 + let body: { modelId?: string; favorite?: boolean; pinned?: boolean };
15 + try {
16 + body = await req.json();
17 + } catch {
18 + return NextResponse.json({ error: "Invalid request." }, { status: 400 });
19 + }
20 + if (!body.modelId) {
21 + return NextResponse.json({ error: "modelId is required." }, { status: 400 });
22 + }
23 + setModelPref(body.modelId, { favorite: body.favorite, pinned: body.pinned });
24 + return NextResponse.json({ ok: true });
25 +}
added src/app/api/models/route.ts +23 −0
@@ -0,0 +1,23 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +import { ensureCatalogFresh, listCatalog, catalogSyncedAt } from "@/lib/catalog";
8 +
9 +export const runtime = "nodejs";
10 +
11 +export async function GET() {
12 + const { unauthorized } = await requireSession();
13 + if (unauthorized) return unauthorized;
14 + try {
15 + await ensureCatalogFresh();
16 + } catch {
17 + return NextResponse.json(
18 + { error: "Could not load the model catalog from OpenRouter. Check connectivity and retry." },
19 + { status: 502 }
20 + );
21 + }
22 + return NextResponse.json({ models: listCatalog(), syncedAt: catalogSyncedAt() });
23 +}
added src/app/api/models/sync/route.ts +23 −0
@@ -0,0 +1,23 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +import { syncCatalog } from "@/lib/catalog";
8 +
9 +export const runtime = "nodejs";
10 +
11 +export async function POST() {
12 + const { unauthorized } = await requireSession();
13 + if (unauthorized) return unauthorized;
14 + try {
15 + const result = await syncCatalog();
16 + return NextResponse.json(result);
17 + } catch {
18 + return NextResponse.json(
19 + { error: "Catalog sync failed. Check connectivity and retry." },
20 + { status: 502 }
21 + );
22 + }
23 +}
added src/app/api/usage/route.ts +68 −0
@@ -0,0 +1,68 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { NextResponse, type NextRequest } from "next/server";
6 +import { requireSession } from "@/lib/auth/guard";
7 +import { getDb } from "@/lib/db/database";
8 +
9 +export const runtime = "nodejs";
10 +
11 +const PERIODS: Record<string, number | null> = {
12 + today: 1000 * 60 * 60 * 24,
13 + "7d": 1000 * 60 * 60 * 24 * 7,
14 + "30d": 1000 * 60 * 60 * 24 * 30,
15 + all: null,
16 +};
17 +
18 +export async function GET(req: NextRequest) {
19 + const { unauthorized } = await requireSession();
20 + if (unauthorized) return unauthorized;
21 +
22 + const period = req.nextUrl.searchParams.get("period") ?? "7d";
23 + const windowMs = PERIODS[period] ?? PERIODS["7d"];
24 + const since = windowMs === null ? 0 : Date.now() - windowMs;
25 + const db = getDb();
26 +
27 + const totals = db
28 + .prepare(
29 + `SELECT COUNT(*) AS requests,
30 + COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens,
31 + COALESCE(SUM(completion_tokens), 0) AS completion_tokens,
32 + COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens,
33 + COALESCE(SUM(total_tokens), 0) AS total_tokens,
34 + COALESCE(SUM(COALESCE(reported_cost_usd, estimated_cost_usd)), 0) AS cost_usd
35 + FROM generation_usage WHERE created_at >= ?`
36 + )
37 + .get(since) as Record<string, number>;
38 +
39 + const byModel = db
40 + .prepare(
41 + `SELECT gu.model_id,
42 + COALESCE(m.name, gu.model_id) AS model_name,
43 + m.provider,
44 + COUNT(*) AS requests,
45 + COALESCE(SUM(gu.total_tokens), 0) AS total_tokens,
46 + COALESCE(SUM(COALESCE(gu.reported_cost_usd, gu.estimated_cost_usd)), 0) AS cost_usd
47 + FROM generation_usage gu
48 + LEFT JOIN models m ON m.openrouter_model_id = gu.model_id
49 + WHERE gu.created_at >= ?
50 + GROUP BY gu.model_id
51 + ORDER BY cost_usd DESC, requests DESC
52 + LIMIT 25`
53 + )
54 + .all(since);
55 +
56 + const byDay = db
57 + .prepare(
58 + `SELECT date(created_at / 1000, 'unixepoch', 'localtime') AS day,
59 + COUNT(*) AS requests,
60 + COALESCE(SUM(total_tokens), 0) AS total_tokens,
61 + COALESCE(SUM(COALESCE(reported_cost_usd, estimated_cost_usd)), 0) AS cost_usd
62 + FROM generation_usage WHERE created_at >= ?
63 + GROUP BY day ORDER BY day`
64 + )
65 + .all(since);
66 +
67 + return NextResponse.json({ period, totals, byModel, byDay });
68 +}
modified src/app/globals.css +1349 −26
@@ -1,49 +1,1372 @@
1 :root {
2 --background: #ffffff;
3 --foreground: #171717;
1 +/* Author: Simon-Pierre Boucher
2 + Contact: contact@spboucher.ai
3 + Project: chat.spboucher.ai
4 +
5 + Design system — "Instrument Panel".
6 + Dark-first, one accent for "alive" (signal teal), one for "cost" (amber).
7 + Mobile-first: base styles target ~390px, enhanced upward. */
8 +
9 +:root,
10 +:root[data-theme="dark"] {
11 + --ink-950: #0e1116;
12 + --ink-900: #151a22;
13 + --ink-850: #1a212b;
14 + --ink-700: #2a3240;
15 + --fog-300: #aab4c3;
16 + --fog-050: #eef2f7;
17 + --signal-500: #3fb6a8;
18 + --signal-600: #2f9a8e;
19 + --signal-alpha: rgba(63, 182, 168, 0.14);
20 + --amber-500: #e0a458;
21 + --amber-alpha: rgba(224, 164, 88, 0.15);
22 + --danger: #d97b7b;
23 +
24 + --surface: var(--ink-950);
25 + --surface-raised: var(--ink-900);
26 + --surface-raised-2: var(--ink-850);
27 + --border: var(--ink-700);
28 + --text: var(--fog-050);
29 + --text-dim: var(--fog-300);
30 + --user-bubble: #1d2634;
31 +
32 + --sheet-shadow: 0 -12px 48px rgba(0, 0, 0, 0.5);
33 + --card-shadow: 0 2px 16px rgba(0, 0, 0, 0.35);
4 34 }
5 35
6 @media (prefers-color-scheme: dark) {
7 :root {
8 --background: #0a0a0a;
9 --foreground: #ededed;
36 +:root[data-theme="light"] {
37 + --ink-950: #f6f8fa;
38 + --ink-900: #ffffff;
39 + --ink-850: #eef1f5;
40 + --ink-700: #d4dbe4;
41 + --fog-300: #5b6675;
42 + --fog-050: #171d26;
43 + --signal-500: #2b9a8e;
44 + --signal-600: #23857a;
45 + --signal-alpha: rgba(43, 154, 142, 0.12);
46 + --amber-500: #b97f35;
47 + --amber-alpha: rgba(185, 127, 53, 0.13);
48 + --danger: #b54545;
49 +
50 + --surface: var(--ink-950);
51 + --surface-raised: var(--ink-900);
52 + --surface-raised-2: var(--ink-850);
53 + --border: var(--ink-700);
54 + --text: var(--fog-050);
55 + --text-dim: var(--fog-300);
56 + --user-bubble: #e7edf4;
57 +
58 + --sheet-shadow: 0 -12px 48px rgba(23, 29, 38, 0.18);
59 + --card-shadow: 0 2px 16px rgba(23, 29, 38, 0.08);
60 +}
61 +
62 +@media (prefers-color-scheme: light) {
63 + :root:not([data-theme]) {
64 + --ink-950: #f6f8fa;
65 + --ink-900: #ffffff;
66 + --ink-850: #eef1f5;
67 + --ink-700: #d4dbe4;
68 + --fog-300: #5b6675;
69 + --fog-050: #171d26;
70 + --signal-500: #2b9a8e;
71 + --signal-600: #23857a;
72 + --signal-alpha: rgba(43, 154, 142, 0.12);
73 + --amber-500: #b97f35;
74 + --amber-alpha: rgba(185, 127, 53, 0.13);
75 + --danger: #b54545;
76 + --surface: var(--ink-950);
77 + --surface-raised: var(--ink-900);
78 + --surface-raised-2: var(--ink-850);
79 + --border: var(--ink-700);
80 + --text: var(--fog-050);
81 + --text-dim: var(--fog-300);
82 + --user-bubble: #e7edf4;
83 + --sheet-shadow: 0 -12px 48px rgba(23, 29, 38, 0.18);
84 + --card-shadow: 0 2px 16px rgba(23, 29, 38, 0.08);
10 85 }
11 86 }
12 87
13 html {
14 height: 100%;
88 +* {
89 + box-sizing: border-box;
15 90 }
16 91
17 92 html,
18 93 body {
19 max-width: 100vw;
20 overflow-x: hidden;
94 + margin: 0;
95 + padding: 0;
96 + height: 100%;
21 97 }
22 98
23 99 body {
24 min-height: 100%;
25 display: flex;
26 flex-direction: column;
27 color: var(--foreground);
28 background: var(--background);
29 font-family: Arial, Helvetica, sans-serif;
100 + background: var(--surface);
101 + color: var(--text);
102 + font-family: var(--font-ui), Inter, -apple-system, system-ui, sans-serif;
103 + font-size: 16px;
104 + line-height: 1.6;
30 105 -webkit-font-smoothing: antialiased;
31 -moz-osx-font-smoothing: grayscale;
106 + overscroll-behavior-y: none;
32 107 }
33 108
34 * {
35 box-sizing: border-box;
109 +code,
110 +kbd,
111 +pre,
112 +.mono {
113 + font-family: var(--font-mono), "Berkeley Mono", "JetBrains Mono", ui-monospace, monospace;
114 +}
115 +
116 +button {
117 + font-family: inherit;
118 + color: inherit;
119 + background: none;
120 + border: none;
121 + cursor: pointer;
36 122 padding: 0;
37 margin: 0;
38 123 }
39 124
40 a {
125 +button:focus-visible,
126 +a:focus-visible,
127 +input:focus-visible,
128 +textarea:focus-visible,
129 +[tabindex]:focus-visible {
130 + outline: 2px solid var(--signal-500);
131 + outline-offset: 2px;
132 + border-radius: 4px;
133 +}
134 +
135 +input,
136 +textarea {
137 + font-family: inherit;
138 + font-size: inherit;
41 139 color: inherit;
42 text-decoration: none;
43 140 }
44 141
45 @media (prefers-color-scheme: dark) {
46 html {
47 color-scheme: dark;
142 +a {
143 + color: var(--signal-500);
144 +}
145 +
146 +::selection {
147 + background: var(--signal-alpha);
148 +}
149 +
150 +/* ---------- App shell ---------- */
151 +
152 +.app {
153 + display: flex;
154 + height: 100dvh;
155 + overflow: hidden;
156 +}
157 +
158 +.main {
159 + flex: 1;
160 + display: flex;
161 + flex-direction: column;
162 + min-width: 0;
163 + position: relative;
164 +}
165 +
166 +/* ---------- Top bar ---------- */
167 +
168 +.topbar {
169 + display: flex;
170 + align-items: center;
171 + gap: 10px;
172 + padding: 10px 14px;
173 + padding-top: calc(10px + env(safe-area-inset-top));
174 + border-bottom: 1px solid var(--border);
175 + background: var(--surface);
176 + min-height: 54px;
177 +}
178 +
179 +.topbar-title {
180 + flex: 1;
181 + font-weight: 600;
182 + font-size: 15px;
183 + letter-spacing: -0.01em;
184 + white-space: nowrap;
185 + overflow: hidden;
186 + text-overflow: ellipsis;
187 +}
188 +
189 +.icon-btn {
190 + display: inline-flex;
191 + align-items: center;
192 + justify-content: center;
193 + width: 44px;
194 + height: 44px;
195 + border-radius: 10px;
196 + color: var(--text-dim);
197 + flex: none;
198 +}
199 +
200 +.icon-btn:hover {
201 + background: var(--surface-raised);
202 + color: var(--text);
203 +}
204 +
205 +/* ---------- Sidebar / drawer ---------- */
206 +
207 +.sidebar {
208 + width: 300px;
209 + flex: none;
210 + background: var(--surface-raised);
211 + border-right: 1px solid var(--border);
212 + display: flex;
213 + flex-direction: column;
214 + padding-top: env(safe-area-inset-top);
215 +}
216 +
217 +.sidebar-header {
218 + display: flex;
219 + align-items: center;
220 + gap: 8px;
221 + padding: 14px;
222 +}
223 +
224 +.wordmark {
225 + font-family: var(--font-mono), monospace;
226 + font-size: 13px;
227 + font-weight: 600;
228 + letter-spacing: 0.02em;
229 + color: var(--text);
230 + flex: 1;
231 + user-select: none;
232 +}
233 +
234 +.wordmark .tld {
235 + color: var(--signal-500);
236 +}
237 +
238 +.new-chat-btn {
239 + display: flex;
240 + align-items: center;
241 + gap: 8px;
242 + margin: 0 14px 10px;
243 + padding: 11px 14px;
244 + border-radius: 10px;
245 + border: 1px solid var(--border);
246 + background: var(--surface);
247 + font-weight: 500;
248 + font-size: 14px;
249 + transition: border-color 0.15s;
250 +}
251 +
252 +.new-chat-btn:hover {
253 + border-color: var(--signal-500);
254 +}
255 +
256 +.conv-list {
257 + flex: 1;
258 + overflow-y: auto;
259 + padding: 0 8px 14px;
260 +}
261 +
262 +.conv-item {
263 + display: flex;
264 + align-items: center;
265 + gap: 8px;
266 + width: 100%;
267 + text-align: left;
268 + padding: 10px 10px;
269 + border-radius: 9px;
270 + font-size: 14px;
271 + color: var(--text-dim);
272 + margin-bottom: 1px;
273 +}
274 +
275 +.conv-item.active {
276 + background: var(--surface-raised-2);
277 + color: var(--text);
278 +}
279 +
280 +.conv-item:hover {
281 + background: var(--surface-raised-2);
282 +}
283 +
284 +.conv-item .conv-title {
285 + flex: 1;
286 + white-space: nowrap;
287 + overflow: hidden;
288 + text-overflow: ellipsis;
289 +}
290 +
291 +.conv-item .conv-del {
292 + opacity: 0;
293 + color: var(--text-dim);
294 + width: 28px;
295 + height: 28px;
296 + border-radius: 6px;
297 + display: inline-flex;
298 + align-items: center;
299 + justify-content: center;
300 + flex: none;
301 +}
302 +
303 +.conv-item:hover .conv-del,
304 +.conv-item.active .conv-del {
305 + opacity: 1;
306 +}
307 +
308 +.conv-item .conv-del:hover {
309 + color: var(--danger);
310 + background: var(--surface);
311 +}
312 +
313 +.sidebar-footer {
314 + border-top: 1px solid var(--border);
315 + padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
316 + display: flex;
317 + gap: 6px;
318 +}
319 +
320 +.sidebar-footer .foot-btn {
321 + flex: 1;
322 + display: flex;
323 + align-items: center;
324 + gap: 8px;
325 + padding: 9px 10px;
326 + border-radius: 8px;
327 + font-size: 13px;
328 + color: var(--text-dim);
329 +}
330 +
331 +.sidebar-footer .foot-btn:hover {
332 + background: var(--surface-raised-2);
333 + color: var(--text);
334 +}
335 +
336 +/* Mobile: sidebar becomes an off-canvas drawer */
337 +@media (max-width: 1023px) {
338 + .sidebar {
339 + position: fixed;
340 + inset: 0 auto 0 0;
341 + z-index: 40;
342 + transform: translateX(-100%);
343 + transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1);
344 + width: min(84vw, 320px);
345 + box-shadow: none;
346 + }
347 +
348 + .sidebar.open {
349 + transform: translateX(0);
350 + box-shadow: 24px 0 64px rgba(0, 0, 0, 0.4);
351 + }
352 +
353 + .drawer-scrim {
354 + position: fixed;
355 + inset: 0;
356 + z-index: 39;
357 + background: rgba(0, 0, 0, 0.45);
358 + opacity: 0;
359 + pointer-events: none;
360 + transition: opacity 0.25s;
361 + }
362 +
363 + .drawer-scrim.open {
364 + opacity: 1;
365 + pointer-events: auto;
366 + }
367 +}
368 +
369 +@media (min-width: 1024px) {
370 + .drawer-scrim {
371 + display: none;
372 + }
373 +
374 + .topbar .menu-btn {
375 + display: none;
376 + }
377 +}
378 +
379 +/* ---------- Messages ---------- */
380 +
381 +.message-scroll {
382 + flex: 1;
383 + overflow-y: auto;
384 + overscroll-behavior: contain;
385 + scroll-behavior: smooth;
386 +}
387 +
388 +@media (prefers-reduced-motion: reduce) {
389 + .message-scroll {
390 + scroll-behavior: auto;
391 + }
392 + * {
393 + animation-duration: 0.001s !important;
394 + transition-duration: 0.001s !important;
395 + }
396 +}
397 +
398 +.message-column {
399 + max-width: 780px;
400 + margin: 0 auto;
401 + padding: 20px 16px 24px;
402 + display: flex;
403 + flex-direction: column;
404 + gap: 22px;
405 +}
406 +
407 +.empty-state {
408 + margin: auto;
409 + text-align: center;
410 + padding: 40px 24px;
411 + color: var(--text-dim);
412 +}
413 +
414 +.empty-state .glyph {
415 + font-family: var(--font-mono), monospace;
416 + font-size: 28px;
417 + color: var(--signal-500);
418 + margin-bottom: 14px;
419 +}
420 +
421 +.empty-state h2 {
422 + color: var(--text);
423 + font-size: 17px;
424 + font-weight: 600;
425 + margin: 0 0 6px;
426 +}
427 +
428 +.empty-state p {
429 + font-size: 14px;
430 + margin: 0;
431 +}
432 +
433 +/* User message: compact right-aligned card */
434 +.msg-user {
435 + align-self: flex-end;
436 + max-width: 85%;
437 + background: var(--user-bubble);
438 + border-radius: 14px 14px 4px 14px;
439 + padding: 10px 14px;
440 + white-space: pre-wrap;
441 + word-break: break-word;
442 + font-size: 16px;
443 +}
444 +
445 +.msg-user.pending {
446 + opacity: 0.6;
447 +}
448 +
449 +/* Assistant message: full-width block with slim attribution header */
450 +.msg-assistant {
451 + display: flex;
452 + flex-direction: column;
453 + gap: 6px;
454 + min-width: 0;
455 +}
456 +
457 +.msg-attribution {
458 + display: flex;
459 + align-items: center;
460 + gap: 8px;
461 + font-family: var(--font-mono), monospace;
462 + font-size: 11.5px;
463 + color: var(--text-dim);
464 + flex-wrap: wrap;
465 +}
466 +
467 +.msg-attribution .model-name {
468 + color: var(--text);
469 + font-weight: 500;
470 +}
471 +
472 +.msg-attribution .live-dot {
473 + width: 7px;
474 + height: 7px;
475 + border-radius: 50%;
476 + background: var(--signal-500);
477 + animation: pulse 1.4s ease-in-out infinite;
478 +}
479 +
480 +@keyframes pulse {
481 + 0%,
482 + 100% {
483 + opacity: 1;
484 + }
485 + 50% {
486 + opacity: 0.25;
487 + }
488 +}
489 +
490 +.cost-chip {
491 + color: var(--amber-500);
492 + background: var(--amber-alpha);
493 + padding: 1px 7px;
494 + border-radius: 6px;
495 + font-size: 11px;
496 +}
497 +
498 +.msg-body {
499 + font-size: 16.5px;
500 + line-height: 1.62;
501 + word-break: break-word;
502 + min-width: 0;
503 +}
504 +
505 +.msg-body > *:first-child {
506 + margin-top: 0;
507 +}
508 +
509 +.msg-body > *:last-child {
510 + margin-bottom: 0;
511 +}
512 +
513 +.msg-error {
514 + color: var(--danger);
515 + font-size: 14px;
516 + border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
517 + border-radius: 10px;
518 + padding: 10px 14px;
519 + background: color-mix(in srgb, var(--danger) 7%, transparent);
520 +}
521 +
522 +.streaming-caret::after {
523 + content: "";
524 + display: inline-block;
525 + width: 8px;
526 + height: 17px;
527 + margin-left: 3px;
528 + vertical-align: text-bottom;
529 + background: var(--signal-500);
530 + border-radius: 2px;
531 + animation: pulse 1.1s ease-in-out infinite;
532 +}
533 +
534 +.reasoning-block {
535 + border-left: 2px solid var(--border);
536 + padding: 2px 0 2px 12px;
537 + color: var(--text-dim);
538 + font-size: 13.5px;
539 + margin-bottom: 8px;
540 +}
541 +
542 +.reasoning-block summary {
543 + cursor: pointer;
544 + font-family: var(--font-mono), monospace;
545 + font-size: 11.5px;
546 + user-select: none;
547 + list-style: none;
548 +}
549 +
550 +.reasoning-block summary::-webkit-details-marker {
551 + display: none;
552 +}
553 +
554 +/* Message actions: tap-friendly row, never hover-only */
555 +.msg-actions {
556 + display: flex;
557 + align-items: center;
558 + gap: 4px;
559 + margin-top: 2px;
560 +}
561 +
562 +.msg-action-btn {
563 + min-width: 40px;
564 + min-height: 36px;
565 + padding: 4px 10px;
566 + border-radius: 8px;
567 + color: var(--text-dim);
568 + font-size: 12px;
569 + display: inline-flex;
570 + align-items: center;
571 + gap: 5px;
572 +}
573 +
574 +.msg-action-btn:hover {
575 + background: var(--surface-raised);
576 + color: var(--text);
577 +}
578 +
579 +.branch-nav {
580 + display: inline-flex;
581 + align-items: center;
582 + gap: 2px;
583 + font-family: var(--font-mono), monospace;
584 + font-size: 12px;
585 + color: var(--text-dim);
586 +}
587 +
588 +.branch-nav button {
589 + min-width: 34px;
590 + min-height: 36px;
591 + border-radius: 8px;
592 + display: inline-flex;
593 + align-items: center;
594 + justify-content: center;
595 +}
596 +
597 +.branch-nav button:disabled {
598 + opacity: 0.3;
599 + cursor: default;
600 +}
601 +
602 +.branch-nav button:not(:disabled):hover {
603 + background: var(--surface-raised);
604 + color: var(--text);
605 +}
606 +
607 +/* Markdown internals */
608 +.msg-body pre {
609 + background: var(--ink-900);
610 + border: 1px solid var(--border);
611 + border-radius: 12px;
612 + padding: 0;
613 + overflow: hidden;
614 + margin: 14px 0;
615 +}
616 +
617 +:root[data-theme="light"] .msg-body pre {
618 + background: #14181f;
619 +}
620 +
621 +.code-block-header {
622 + display: flex;
623 + align-items: center;
624 + justify-content: space-between;
625 + padding: 6px 12px;
626 + border-bottom: 1px solid var(--ink-700);
627 + font-family: var(--font-mono), monospace;
628 + font-size: 11px;
629 + color: #8b96a5;
630 +}
631 +
632 +.code-block-header button {
633 + color: #8b96a5;
634 + font-family: inherit;
635 + font-size: 11px;
636 + padding: 4px 8px;
637 + border-radius: 6px;
638 + min-height: 28px;
639 +}
640 +
641 +.code-block-header button:hover {
642 + color: #eef2f7;
643 + background: rgba(255, 255, 255, 0.06);
644 +}
645 +
646 +.msg-body pre code {
647 + display: block;
648 + padding: 12px 14px;
649 + overflow-x: auto;
650 + font-size: 13.5px;
651 + line-height: 1.55;
652 + background: transparent;
653 +}
654 +
655 +.msg-body code:not(pre code) {
656 + background: var(--surface-raised-2);
657 + border: 1px solid var(--border);
658 + padding: 1px 5px;
659 + border-radius: 5px;
660 + font-size: 0.86em;
661 +}
662 +
663 +.msg-body table {
664 + border-collapse: collapse;
665 + display: block;
666 + overflow-x: auto;
667 + max-width: 100%;
668 + margin: 14px 0;
669 + font-size: 14.5px;
670 +}
671 +
672 +.msg-body th,
673 +.msg-body td {
674 + border: 1px solid var(--border);
675 + padding: 6px 12px;
676 + text-align: left;
677 +}
678 +
679 +.msg-body th {
680 + background: var(--surface-raised);
681 +}
682 +
683 +.msg-body blockquote {
684 + border-left: 3px solid var(--signal-500);
685 + margin: 14px 0;
686 + padding: 2px 0 2px 14px;
687 + color: var(--text-dim);
688 +}
689 +
690 +.msg-body img {
691 + max-width: 100%;
692 + border-radius: 10px;
693 +}
694 +
695 +/* ---------- Jump-to-latest pill ---------- */
696 +
697 +.jump-pill {
698 + position: absolute;
699 + bottom: 150px;
700 + left: 50%;
701 + transform: translateX(-50%);
702 + z-index: 10;
703 + background: var(--surface-raised-2);
704 + border: 1px solid var(--border);
705 + color: var(--text);
706 + border-radius: 999px;
707 + padding: 8px 16px;
708 + font-size: 13px;
709 + display: flex;
710 + align-items: center;
711 + gap: 6px;
712 + box-shadow: var(--card-shadow);
713 +}
714 +
715 +/* ---------- Composer + Model Rail ---------- */
716 +
717 +.composer-wrap {
718 + border-top: 1px solid var(--border);
719 + background: var(--surface);
720 + padding: 0 12px calc(10px + env(safe-area-inset-bottom));
721 +}
722 +
723 +.composer {
724 + max-width: 780px;
725 + margin: 0 auto;
726 +}
727 +
728 +/* The Model Rail: machined cartridge attached to the composer */
729 +.model-rail {
730 + display: flex;
731 + align-items: center;
732 + gap: 10px;
733 + width: 100%;
734 + padding: 8px 12px;
735 + margin-top: 10px;
736 + border: 1px solid var(--border);
737 + border-radius: 12px 12px 0 0;
738 + border-bottom: none;
739 + background: var(--surface-raised);
740 + text-align: left;
741 + position: relative;
742 + overflow: hidden;
743 + transition: border-color 0.2s;
744 +}
745 +
746 +.model-rail:hover {
747 + border-color: color-mix(in srgb, var(--signal-500) 55%, var(--border));
748 +}
749 +
750 +.model-rail::before {
751 + /* signal edge: pulses teal while streaming */
752 + content: "";
753 + position: absolute;
754 + left: 0;
755 + top: 0;
756 + bottom: 0;
757 + width: 3px;
758 + background: var(--border);
759 + transition: background 0.3s;
760 +}
761 +
762 +.model-rail.live::before {
763 + background: var(--signal-500);
764 + animation: pulse 1.2s ease-in-out infinite;
765 +}
766 +
767 +.rail-glyph {
768 + font-family: var(--font-mono), monospace;
769 + font-size: 13px;
770 + font-weight: 600;
771 + color: var(--signal-500);
772 + border: 1px solid var(--border);
773 + border-radius: 7px;
774 + width: 30px;
775 + height: 30px;
776 + display: inline-flex;
777 + align-items: center;
778 + justify-content: center;
779 + flex: none;
780 + background: var(--surface);
781 +}
782 +
783 +.rail-main {
784 + flex: 1;
785 + min-width: 0;
786 + display: flex;
787 + flex-direction: column;
788 + gap: 3px;
789 +}
790 +
791 +.rail-model-id {
792 + font-family: var(--font-mono), monospace;
793 + font-size: 12.5px;
794 + font-weight: 500;
795 + color: var(--text);
796 + white-space: nowrap;
797 + overflow: hidden;
798 + text-overflow: ellipsis;
799 +}
800 +
801 +.rail-meta {
802 + display: flex;
803 + align-items: center;
804 + gap: 8px;
805 + font-family: var(--font-mono), monospace;
806 + font-size: 10.5px;
807 + color: var(--text-dim);
808 +}
809 +
810 +.ctx-meter {
811 + flex: 1;
812 + max-width: 120px;
813 + height: 3px;
814 + background: var(--border);
815 + border-radius: 2px;
816 + overflow: hidden;
817 +}
818 +
819 +.ctx-meter .fill {
820 + height: 100%;
821 + background: var(--amber-500);
822 + border-radius: 2px;
823 + transition: width 0.4s;
824 +}
825 +
826 +.price-chip {
827 + color: var(--amber-500);
828 + flex: none;
829 +}
830 +
831 +.rail-chevron {
832 + color: var(--text-dim);
833 + flex: none;
834 +}
835 +
836 +.composer-box {
837 + display: flex;
838 + align-items: flex-end;
839 + gap: 8px;
840 + border: 1px solid var(--border);
841 + border-radius: 0 0 14px 14px;
842 + background: var(--surface-raised);
843 + padding: 8px 8px 8px 14px;
844 + transition: border-color 0.2s;
845 +}
846 +
847 +.composer-box:focus-within {
848 + border-color: var(--signal-500);
849 +}
850 +
851 +.composer-box textarea {
852 + flex: 1;
853 + background: transparent;
854 + border: none;
855 + resize: none;
856 + outline: none;
857 + font-size: 16px; /* prevents iOS zoom */
858 + line-height: 1.5;
859 + max-height: 40dvh;
860 + padding: 6px 0;
861 + color: var(--text);
862 +}
863 +
864 +.composer-box textarea::placeholder {
865 + color: var(--text-dim);
866 + opacity: 0.7;
867 +}
868 +
869 +.send-btn {
870 + width: 44px;
871 + height: 44px;
872 + border-radius: 11px;
873 + background: var(--signal-500);
874 + color: #07110f;
875 + display: inline-flex;
876 + align-items: center;
877 + justify-content: center;
878 + flex: none;
879 + transition: background 0.15s, transform 0.1s;
880 +}
881 +
882 +.send-btn:hover {
883 + background: var(--signal-600);
884 +}
885 +
886 +.send-btn:active {
887 + transform: scale(0.94);
888 +}
889 +
890 +.send-btn:disabled {
891 + background: var(--border);
892 + color: var(--text-dim);
893 + cursor: default;
894 +}
895 +
896 +.send-btn.stop {
897 + background: var(--surface-raised-2);
898 + color: var(--danger);
899 + border: 1px solid var(--border);
900 +}
901 +
902 +/* ---------- Bottom sheet (model picker) ---------- */
903 +
904 +.sheet-scrim {
905 + position: fixed;
906 + inset: 0;
907 + z-index: 60;
908 + background: rgba(0, 0, 0, 0.5);
909 + animation: fadeIn 0.2s;
910 +}
911 +
912 +@keyframes fadeIn {
913 + from {
914 + opacity: 0;
915 + }
916 +}
917 +
918 +.sheet {
919 + position: fixed;
920 + z-index: 61;
921 + left: 0;
922 + right: 0;
923 + bottom: 0;
924 + top: max(24px, env(safe-area-inset-top));
925 + background: var(--surface-raised);
926 + border-radius: 18px 18px 0 0;
927 + box-shadow: var(--sheet-shadow);
928 + display: flex;
929 + flex-direction: column;
930 + animation: sheetUp 0.28s cubic-bezier(0.32, 0.72, 0.18, 1.02);
931 +}
932 +
933 +@keyframes sheetUp {
934 + from {
935 + transform: translateY(40px);
936 + opacity: 0.5;
937 + }
938 +}
939 +
940 +@media (min-width: 1024px) {
941 + .sheet {
942 + left: 50%;
943 + right: auto;
944 + top: 50%;
945 + bottom: auto;
946 + transform: translate(-50%, -50%);
947 + width: 660px;
948 + height: min(78vh, 740px);
949 + border-radius: 18px;
950 + animation: fadeIn 0.18s;
951 + }
952 +}
953 +
954 +.sheet-handle {
955 + width: 40px;
956 + height: 4px;
957 + border-radius: 2px;
958 + background: var(--border);
959 + margin: 10px auto 2px;
960 + flex: none;
961 +}
962 +
963 +@media (min-width: 1024px) {
964 + .sheet-handle {
965 + display: none;
48 966 }
49 967 }
968 +
969 +.sheet-search {
970 + position: sticky;
971 + top: 0;
972 + padding: 10px 14px;
973 + display: flex;
974 + gap: 8px;
975 + align-items: center;
976 +}
977 +
978 +.sheet-search input {
979 + flex: 1;
980 + background: var(--surface);
981 + border: 1px solid var(--border);
982 + border-radius: 11px;
983 + padding: 11px 14px;
984 + font-size: 16px;
985 + outline: none;
986 +}
987 +
988 +.sheet-search input:focus {
989 + border-color: var(--signal-500);
990 +}
991 +
992 +.filter-row {
993 + display: flex;
994 + gap: 6px;
995 + padding: 2px 14px 10px;
996 + overflow-x: auto;
997 + flex: none;
998 + scrollbar-width: none;
999 +}
1000 +
1001 +.filter-row::-webkit-scrollbar {
1002 + display: none;
1003 +}
1004 +
1005 +.filter-chip {
1006 + flex: none;
1007 + padding: 6px 12px;
1008 + border-radius: 999px;
1009 + border: 1px solid var(--border);
1010 + font-size: 12.5px;
1011 + color: var(--text-dim);
1012 + background: var(--surface);
1013 + min-height: 32px;
1014 +}
1015 +
1016 +.filter-chip.on {
1017 + border-color: var(--signal-500);
1018 + color: var(--signal-500);
1019 + background: var(--signal-alpha);
1020 +}
1021 +
1022 +.model-list {
1023 + flex: 1;
1024 + overflow-y: auto;
1025 + padding: 0 8px 20px;
1026 +}
1027 +
1028 +.model-section-label {
1029 + font-family: var(--font-mono), monospace;
1030 + font-size: 10.5px;
1031 + text-transform: uppercase;
1032 + letter-spacing: 0.08em;
1033 + color: var(--text-dim);
1034 + padding: 14px 10px 6px;
1035 +}
1036 +
1037 +.model-row {
1038 + display: flex;
1039 + align-items: center;
1040 + gap: 10px;
1041 + width: 100%;
1042 + text-align: left;
1043 + padding: 10px;
1044 + border-radius: 10px;
1045 + min-height: 56px;
1046 +}
1047 +
1048 +.model-row:hover,
1049 +.model-row.highlight {
1050 + background: var(--surface-raised-2);
1051 +}
1052 +
1053 +.model-row.selected {
1054 + background: var(--signal-alpha);
1055 +}
1056 +
1057 +.model-row .prov-glyph {
1058 + font-family: var(--font-mono), monospace;
1059 + font-size: 11px;
1060 + font-weight: 600;
1061 + color: var(--text-dim);
1062 + border: 1px solid var(--border);
1063 + border-radius: 7px;
1064 + width: 30px;
1065 + height: 30px;
1066 + display: inline-flex;
1067 + align-items: center;
1068 + justify-content: center;
1069 + flex: none;
1070 + background: var(--surface);
1071 +}
1072 +
1073 +.model-row .m-main {
1074 + flex: 1;
1075 + min-width: 0;
1076 +}
1077 +
1078 +.model-row .m-name {
1079 + font-size: 14px;
1080 + font-weight: 500;
1081 + color: var(--text);
1082 + white-space: nowrap;
1083 + overflow: hidden;
1084 + text-overflow: ellipsis;
1085 +}
1086 +
1087 +.model-row .m-sub {
1088 + font-family: var(--font-mono), monospace;
1089 + font-size: 11px;
1090 + color: var(--text-dim);
1091 + display: flex;
1092 + gap: 8px;
1093 + white-space: nowrap;
1094 + overflow: hidden;
1095 +}
1096 +
1097 +.model-row .fav-btn {
1098 + width: 40px;
1099 + height: 40px;
1100 + border-radius: 9px;
1101 + color: var(--text-dim);
1102 + display: inline-flex;
1103 + align-items: center;
1104 + justify-content: center;
1105 + flex: none;
1106 +}
1107 +
1108 +.model-row .fav-btn.on {
1109 + color: var(--amber-500);
1110 +}
1111 +
1112 +.model-row .fav-btn:hover {
1113 + background: var(--surface);
1114 +}
1115 +
1116 +.cap-badges {
1117 + display: flex;
1118 + gap: 4px;
1119 + flex: none;
1120 +}
1121 +
1122 +.cap-badge {
1123 + font-family: var(--font-mono), monospace;
1124 + font-size: 9.5px;
1125 + color: var(--text-dim);
1126 + border: 1px solid var(--border);
1127 + border-radius: 4px;
1128 + padding: 1px 4px;
1129 + text-transform: uppercase;
1130 +}
1131 +
1132 +/* ---------- Login ---------- */
1133 +
1134 +.login-page {
1135 + min-height: 100dvh;
1136 + display: flex;
1137 + align-items: center;
1138 + justify-content: center;
1139 + padding: 24px;
1140 + background:
1141 + radial-gradient(1200px 500px at 50% -10%, var(--signal-alpha), transparent 60%),
1142 + var(--surface);
1143 +}
1144 +
1145 +.login-card {
1146 + width: 100%;
1147 + max-width: 380px;
1148 + background: var(--surface-raised);
1149 + border: 1px solid var(--border);
1150 + border-radius: 18px;
1151 + padding: 28px;
1152 + box-shadow: var(--card-shadow);
1153 +}
1154 +
1155 +.login-card .wordmark {
1156 + font-size: 15px;
1157 + display: block;
1158 + margin-bottom: 4px;
1159 +}
1160 +
1161 +.login-card .sub {
1162 + color: var(--text-dim);
1163 + font-size: 13px;
1164 + margin: 0 0 22px;
1165 +}
1166 +
1167 +.login-card label {
1168 + display: block;
1169 + font-size: 12.5px;
1170 + font-weight: 500;
1171 + color: var(--text-dim);
1172 + margin: 14px 0 6px;
1173 +}
1174 +
1175 +.login-card input {
1176 + width: 100%;
1177 + background: var(--surface);
1178 + border: 1px solid var(--border);
1179 + border-radius: 10px;
1180 + padding: 12px 14px;
1181 + font-size: 16px;
1182 + outline: none;
1183 +}
1184 +
1185 +.login-card input:focus {
1186 + border-color: var(--signal-500);
1187 +}
1188 +
1189 +.login-card .login-btn {
1190 + width: 100%;
1191 + margin-top: 22px;
1192 + background: var(--signal-500);
1193 + color: #07110f;
1194 + font-weight: 600;
1195 + font-size: 15px;
1196 + border-radius: 11px;
1197 + padding: 13px;
1198 + transition: background 0.15s;
1199 +}
1200 +
1201 +.login-card .login-btn:hover {
1202 + background: var(--signal-600);
1203 +}
1204 +
1205 +.login-card .login-btn:disabled {
1206 + opacity: 0.6;
1207 +}
1208 +
1209 +.login-error {
1210 + margin-top: 14px;
1211 + color: var(--danger);
1212 + font-size: 13.5px;
1213 +}
1214 +
1215 +/* ---------- Settings / usage ---------- */
1216 +
1217 +.settings-page {
1218 + max-width: 780px;
1219 + margin: 0 auto;
1220 + padding: 20px 16px calc(40px + env(safe-area-inset-bottom));
1221 +}
1222 +
1223 +.settings-page h1 {
1224 + font-size: 20px;
1225 + margin: 8px 0 20px;
1226 +}
1227 +
1228 +.settings-page h2 {
1229 + font-size: 14px;
1230 + font-family: var(--font-mono), monospace;
1231 + text-transform: uppercase;
1232 + letter-spacing: 0.07em;
1233 + color: var(--text-dim);
1234 + margin: 28px 0 12px;
1235 + font-weight: 500;
1236 +}
1237 +
1238 +.stat-grid {
1239 + display: grid;
1240 + grid-template-columns: repeat(2, 1fr);
1241 + gap: 10px;
1242 +}
1243 +
1244 +@media (min-width: 640px) {
1245 + .stat-grid {
1246 + grid-template-columns: repeat(4, 1fr);
1247 + }
1248 +}
1249 +
1250 +.stat-tile {
1251 + background: var(--surface-raised);
1252 + border: 1px solid var(--border);
1253 + border-radius: 12px;
1254 + padding: 14px;
1255 +}
1256 +
1257 +.stat-tile .v {
1258 + font-family: var(--font-mono), monospace;
1259 + font-size: 20px;
1260 + font-weight: 600;
1261 +}
1262 +
1263 +.stat-tile .v.cost {
1264 + color: var(--amber-500);
1265 +}
1266 +
1267 +.stat-tile .k {
1268 + font-size: 12px;
1269 + color: var(--text-dim);
1270 + margin-top: 2px;
1271 +}
1272 +
1273 +.period-tabs {
1274 + display: flex;
1275 + gap: 6px;
1276 + margin-bottom: 14px;
1277 +}
1278 +
1279 +.usage-table {
1280 + width: 100%;
1281 + border-collapse: collapse;
1282 + font-size: 13.5px;
1283 +}
1284 +
1285 +.usage-table th {
1286 + text-align: left;
1287 + font-family: var(--font-mono), monospace;
1288 + font-size: 10.5px;
1289 + text-transform: uppercase;
1290 + letter-spacing: 0.07em;
1291 + color: var(--text-dim);
1292 + font-weight: 500;
1293 + padding: 8px 10px;
1294 + border-bottom: 1px solid var(--border);
1295 +}
1296 +
1297 +.usage-table td {
1298 + padding: 9px 10px;
1299 + border-bottom: 1px solid var(--border);
1300 +}
1301 +
1302 +.usage-table td.num {
1303 + font-family: var(--font-mono), monospace;
1304 + text-align: right;
1305 +}
1306 +
1307 +.usage-table th.num {
1308 + text-align: right;
1309 +}
1310 +
1311 +.settings-row {
1312 + display: flex;
1313 + align-items: center;
1314 + justify-content: space-between;
1315 + gap: 12px;
1316 + background: var(--surface-raised);
1317 + border: 1px solid var(--border);
1318 + border-radius: 12px;
1319 + padding: 14px;
1320 + margin-bottom: 10px;
1321 +}
1322 +
1323 +.settings-row .r-label {
1324 + font-size: 14px;
1325 + font-weight: 500;
1326 +}
1327 +
1328 +.settings-row .r-sub {
1329 + font-size: 12.5px;
1330 + color: var(--text-dim);
1331 + margin-top: 2px;
1332 +}
1333 +
1334 +.btn {
1335 + padding: 9px 16px;
1336 + border-radius: 9px;
1337 + border: 1px solid var(--border);
1338 + background: var(--surface);
1339 + font-size: 13.5px;
1340 + font-weight: 500;
1341 + min-height: 40px;
1342 +}
1343 +
1344 +.btn:hover {
1345 + border-color: var(--signal-500);
1346 +}
1347 +
1348 +.btn.primary {
1349 + background: var(--signal-500);
1350 + border-color: var(--signal-500);
1351 + color: #07110f;
1352 +}
1353 +
1354 +.btn.danger:hover {
1355 + border-color: var(--danger);
1356 + color: var(--danger);
1357 +}
1358 +
1359 +/* thin scrollbars */
1360 +*::-webkit-scrollbar {
1361 + width: 8px;
1362 + height: 8px;
1363 +}
1364 +
1365 +*::-webkit-scrollbar-thumb {
1366 + background: var(--border);
1367 + border-radius: 4px;
1368 +}
1369 +
1370 +*::-webkit-scrollbar-track {
1371 + background: transparent;
1372 +}
modified src/app/layout.tsx +43 −16
@@ -1,26 +1,53 @@
1 import type { Metadata } from "next";
2 import { Geist, Geist_Mono } from "next/font/google";
3 import "./globals.css";
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 4
5 const geistSans = Geist({
6 variable: "--font-geist-sans",
7 subsets: ["latin"],
8 });
5 +import type { Metadata, Viewport } from "next";
6 +import { Inter, JetBrains_Mono } from "next/font/google";
7 +import "./globals.css";
8 +import "katex/dist/katex.min.css";
9 +import "highlight.js/styles/github-dark-dimmed.css";
9 10
10 const geistMono = Geist_Mono({
11 variable: "--font-geist-mono",
12 subsets: ["latin"],
13 });
11 +const inter = Inter({ subsets: ["latin"], variable: "--font-ui" });
12 +const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono", weight: ["400", "500", "600"] });
14 13
15 14 export const metadata: Metadata = {
16 title: "Create Next App",
17 description: "Generated by create next app",
15 + title: "chat.spboucher.ai",
16 + description: "Personal instrument for operating hundreds of AI models.",
17 + manifest: "/manifest.webmanifest",
18 + appleWebApp: {
19 + capable: true,
20 + statusBarStyle: "black-translucent",
21 + title: "spboucher.ai",
22 + },
23 + icons: {
24 + icon: "/icons/icon.svg",
25 + apple: "/icons/apple-touch-icon.png",
26 + },
27 +};
28 +
29 +export const viewport: Viewport = {
30 + width: "device-width",
31 + initialScale: 1,
32 + viewportFit: "cover",
33 + themeColor: [
34 + { media: "(prefers-color-scheme: dark)", color: "#0E1116" },
35 + { media: "(prefers-color-scheme: light)", color: "#F6F8FA" },
36 + ],
18 37 };
19 38
20 export default function RootLayout({ children }: LayoutProps<"/">) {
39 +export default function RootLayout({ children }: { children: React.ReactNode }) {
21 40 return (
22 <html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
23 <body>{children}</body>
41 + <html lang="en" suppressHydrationWarning>
42 + <head>
43 + <script
44 + // Apply persisted theme before first paint to avoid a flash.
45 + dangerouslySetInnerHTML={{
46 + __html: `try{var t=localStorage.getItem('spb-theme');if(t==='light'||t==='dark'){document.documentElement.dataset.theme=t}}catch(e){}`,
47 + }}
48 + />
49 + </head>
50 + <body className={`${inter.variable} ${mono.variable}`}>{children}</body>
24 51 </html>
25 52 );
26 53 }
added src/app/login/page.tsx +72 −0
@@ -0,0 +1,72 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useState } from "react";
8 +import { useRouter } from "next/navigation";
9 +
10 +export default function LoginPage() {
11 + const router = useRouter();
12 + const [username, setUsername] = useState("");
13 + const [password, setPassword] = useState("");
14 + const [error, setError] = useState<string | null>(null);
15 + const [busy, setBusy] = useState(false);
16 +
17 + async function submit(e: React.FormEvent) {
18 + e.preventDefault();
19 + setBusy(true);
20 + setError(null);
21 + try {
22 + const res = await fetch("/api/auth/login", {
23 + method: "POST",
24 + headers: { "Content-Type": "application/json" },
25 + body: JSON.stringify({ username, password }),
26 + });
27 + const json = await res.json().catch(() => ({}));
28 + if (!res.ok) {
29 + setError(json.error ?? "Sign-in failed. Try again.");
30 + setBusy(false);
31 + return;
32 + }
33 + router.replace("/");
34 + router.refresh();
35 + } catch {
36 + setError("Could not reach the server. Check your connection and try again.");
37 + setBusy(false);
38 + }
39 + }
40 +
41 + return (
42 + <div className="login-page">
43 + <form className="login-card" onSubmit={submit}>
44 + <span className="wordmark">
45 + chat.spboucher<span className="tld">.ai</span>
46 + </span>
47 + <p className="sub">Private instrument. Sign in to continue.</p>
48 + <label htmlFor="username">Username</label>
49 + <input
50 + id="username"
51 + autoComplete="username"
52 + autoCapitalize="none"
53 + autoCorrect="off"
54 + value={username}
55 + onChange={(e) => setUsername(e.target.value)}
56 + />
57 + <label htmlFor="password">Password</label>
58 + <input
59 + id="password"
60 + type="password"
61 + autoComplete="current-password"
62 + value={password}
63 + onChange={(e) => setPassword(e.target.value)}
64 + />
65 + <button className="login-btn" type="submit" disabled={busy || !username || !password}>
66 + {busy ? "Signing in…" : "Sign in"}
67 + </button>
68 + {error && <p className="login-error">{error}</p>}
69 + </form>
70 + </div>
71 + );
72 +}
deleted src/app/page.module.css +0 −150
@@ -1,150 +0,0 @@
1 .page {
2 --background: #fafafa;
3 --foreground: #fff;
4
5 --text-primary: #000;
6 --text-secondary: #666;
7
8 --button-primary-hover: #383838;
9 --button-secondary-hover: #f2f2f2;
10 --button-secondary-border: #ebebeb;
11
12 display: flex;
13 flex: 1;
14 flex-direction: column;
15 align-items: center;
16 justify-content: center;
17 font-family: var(--font-geist-sans);
18 background-color: var(--background);
19 }
20
21 .main {
22 display: flex;
23 flex: 1;
24 width: 100%;
25 max-width: 800px;
26 flex-direction: column;
27 align-items: flex-start;
28 justify-content: space-between;
29 background-color: var(--foreground);
30 padding: 120px 60px;
31 }
32
33 .intro {
34 display: flex;
35 flex-direction: column;
36 align-items: flex-start;
37 text-align: left;
38 gap: 24px;
39 }
40
41 .intro h1 {
42 max-width: 320px;
43 font-size: 40px;
44 font-weight: 600;
45 line-height: 48px;
46 letter-spacing: -2.4px;
47 text-wrap: balance;
48 color: var(--text-primary);
49 }
50
51 .intro p {
52 max-width: 440px;
53 font-size: 18px;
54 line-height: 32px;
55 text-wrap: balance;
56 color: var(--text-secondary);
57 }
58
59 .intro a {
60 font-weight: 500;
61 color: var(--text-primary);
62 }
63
64 .ctas {
65 display: flex;
66 flex-direction: row;
67 width: 100%;
68 max-width: 440px;
69 gap: 16px;
70 font-size: 14px;
71 }
72
73 .ctas a {
74 display: flex;
75 justify-content: center;
76 align-items: center;
77 height: 40px;
78 padding: 0 16px;
79 border-radius: 128px;
80 border: 1px solid transparent;
81 transition: 0.2s;
82 cursor: pointer;
83 width: fit-content;
84 font-weight: 500;
85 }
86
87 a.primary {
88 background: var(--text-primary);
89 color: var(--background);
90 gap: 8px;
91 }
92
93 a.secondary {
94 border-color: var(--button-secondary-border);
95 }
96
97 /* Enable hover only on non-touch devices */
98 @media (hover: hover) and (pointer: fine) {
99 a.primary:hover {
100 background: var(--button-primary-hover);
101 border-color: transparent;
102 }
103
104 a.secondary:hover {
105 background: var(--button-secondary-hover);
106 border-color: transparent;
107 }
108 }
109
110 @media (max-width: 600px) {
111 .main {
112 padding: 48px 24px;
113 }
114
115 .intro {
116 gap: 16px;
117 }
118
119 .intro h1 {
120 font-size: 32px;
121 line-height: 40px;
122 letter-spacing: -1.92px;
123 }
124 }
125
126 @media (prefers-color-scheme: dark) {
127 .logo {
128 filter: invert();
129 }
130
131 .page {
132 --background: #000;
133 --foreground: #000;
134
135 --text-primary: #ededed;
136 --text-secondary: #999;
137
138 --button-primary-hover: #ccc;
139 --button-secondary-hover: #1a1a1a;
140 --button-secondary-border: #1a1a1a;
141 }
142 }
143
144 .code {
145 font-family: var(--font-geist-mono);
146 font-size: 0.9em;
147 background: color-mix(in srgb, currentColor 8%, transparent);
148 padding: 0.1em 0.4em;
149 border-radius: 6px;
150 }
modified src/app/page.tsx +17 −65
@@ -1,69 +1,21 @@
1 import Image from "next/image";
2 import styles from "./page.module.css";
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
3 4
4 export default function Home() {
5 +import { redirect } from "next/navigation";
6 +import { currentSession } from "@/lib/auth/guard";
7 +import { ChatApp } from "@/components/ChatApp";
8 +import { RegisterSw } from "@/components/RegisterSw";
9 +
10 +export const dynamic = "force-dynamic";
11 +
12 +export default async function HomePage() {
13 + const session = await currentSession();
14 + if (!session) redirect("/login");
5 15 return (
6 <div className={styles.page}>
7 <main className={styles.main}>
8 <Image
9 className={styles.logo}
10 src="/next.svg"
11 alt="Next.js logo"
12 width={100}
13 height={20}
14 priority
15 />
16 <div className={styles.intro}>
17 <h1>
18 To get started, edit the{" "}
19 <code className={styles.code}>page.tsx</code> file.
20 </h1>
21 <p>
22 Looking for a starting point or more instructions? Head over to{" "}
23 <a
24 href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
25 target="_blank"
26 rel="noopener noreferrer"
27 >
28 Templates
29 </a>{" "}
30 or the{" "}
31 <a
32 href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
33 target="_blank"
34 rel="noopener noreferrer"
35 >
36 Learning
37 </a>{" "}
38 center.
39 </p>
40 </div>
41 <div className={styles.ctas}>
42 <a
43 className={styles.primary}
44 href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
45 target="_blank"
46 rel="noopener noreferrer"
47 >
48 <Image
49 className={styles.logo}
50 src="/vercel.svg"
51 alt="Vercel logomark"
52 width={16}
53 height={14}
54 />
55 Deploy Now
56 </a>
57 <a
58 className={styles.secondary}
59 href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
60 target="_blank"
61 rel="noopener noreferrer"
62 >
63 Documentation
64 </a>
65 </div>
66 </main>
67 </div>
16 + <>
17 + <ChatApp />
18 + <RegisterSw />
19 + </>
68 20 );
69 21 }
added src/app/settings/page.tsx +15 −0
@@ -0,0 +1,15 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { redirect } from "next/navigation";
6 +import { currentSession } from "@/lib/auth/guard";
7 +import { SettingsView } from "@/components/SettingsView";
8 +
9 +export const dynamic = "force-dynamic";
10 +
11 +export default async function SettingsPage() {
12 + const session = await currentSession();
13 + if (!session) redirect("/login");
14 + return <SettingsView />;
15 +}
added src/components/ChatApp.tsx +357 −0
@@ -0,0 +1,357 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8 +import {
9 + type ApiConversation,
10 + type ApiMessage,
11 + type ApiModel,
12 + computeThread,
13 + choicesForLeaf,
14 + estimateTokensClient,
15 +} from "./types";
16 +import { Sidebar } from "./Sidebar";
17 +import { MessageList } from "./MessageList";
18 +import { Composer } from "./Composer";
19 +import { ModelSheet } from "./ModelSheet";
20 +
21 +interface StreamingState {
22 + assistantMessageId: string;
23 + generationId: string;
24 + content: string;
25 + reasoning: string;
26 + error: string | null;
27 +}
28 +
29 +export function ChatApp() {
30 + const [conversations, setConversations] = useState<ApiConversation[]>([]);
31 + const [activeId, setActiveId] = useState<string | null>(null);
32 + const [messages, setMessages] = useState<ApiMessage[]>([]);
33 + const [branchChoice, setBranchChoice] = useState<Map<string, string>>(new Map());
34 + const [models, setModels] = useState<ApiModel[]>([]);
35 + const [modelId, setModelId] = useState<string | null>(null);
36 + const [sheetOpen, setSheetOpen] = useState(false);
37 + const [drawerOpen, setDrawerOpen] = useState(false);
38 + const [streaming, setStreaming] = useState<StreamingState | null>(null);
39 + const [pendingUser, setPendingUser] = useState<ApiMessage | null>(null);
40 + const abortRef = useRef<AbortController | null>(null);
41 + const activeIdRef = useRef<string | null>(null);
42 + activeIdRef.current = activeId;
43 +
44 + const model = useMemo(() => models.find((m) => m.id === modelId) ?? null, [models, modelId]);
45 +
46 + const refreshConversations = useCallback(async () => {
47 + const res = await fetch("/api/conversations");
48 + if (!res.ok) return;
49 + const json = await res.json();
50 + setConversations(json.conversations);
51 + }, []);
52 +
53 + const loadConversation = useCallback(async (id: string) => {
54 + const res = await fetch(`/api/conversations/${id}`);
55 + if (!res.ok) return;
56 + const json = await res.json();
57 + if (activeIdRef.current !== id) return; // user already moved on
58 + setMessages(json.messages);
59 + const leaf = json.conversation.current_leaf_id;
60 + setBranchChoice(leaf ? choicesForLeaf(json.messages, leaf) : new Map());
61 + }, []);
62 +
63 + // Initial load: conversations + model catalog.
64 + useEffect(() => {
65 + refreshConversations();
66 + (async () => {
67 + const res = await fetch("/api/models");
68 + if (!res.ok) return;
69 + const json = await res.json();
70 + const all: ApiModel[] = json.models;
71 + setModels(all);
72 + const saved = localStorage.getItem("spb-model");
73 + const avail = all.filter((m) => m.available);
74 + const pick =
75 + (saved && avail.find((m) => m.id === saved)) ||
76 + avail.find((m) => m.favorite) ||
77 + avail.filter((m) => m.lastUsedAt).sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0))[0] ||
78 + avail.find((m) => m.id === "anthropic/claude-sonnet-4.5") ||
79 + avail.find((m) => m.id === "openai/gpt-4o-mini") ||
80 + avail[0];
81 + if (pick) setModelId(pick.id);
82 + })();
83 + }, [refreshConversations]);
84 +
85 + useEffect(() => {
86 + if (modelId) localStorage.setItem("spb-model", modelId);
87 + }, [modelId]);
88 +
89 + useEffect(() => {
90 + if (activeId) loadConversation(activeId);
91 + else {
92 + setMessages([]);
93 + setBranchChoice(new Map());
94 + }
95 + }, [activeId, loadConversation]);
96 +
97 + const thread = useMemo(() => computeThread(messages, branchChoice), [messages, branchChoice]);
98 +
99 + const contextTokens = useMemo(
100 + () => thread.reduce((acc, m) => acc + estimateTokensClient(m.content), 0),
101 + [thread]
102 + );
103 +
104 + /** Core streaming loop shared by send + regenerate. */
105 + const runStream = useCallback(
106 + async (body: Record<string, unknown>) => {
107 + const ctrl = new AbortController();
108 + abortRef.current = ctrl;
109 + let convId = (body.conversationId as string) ?? null;
110 + try {
111 + const res = await fetch("/api/chat", {
112 + method: "POST",
113 + headers: { "Content-Type": "application/json" },
114 + body: JSON.stringify(body),
115 + signal: ctrl.signal,
116 + });
117 + if (!res.ok || !res.body) {
118 + const json = await res.json().catch(() => ({}));
119 + setStreaming((s) =>
120 + s ? { ...s, error: json.error ?? "The request failed." } : {
121 + assistantMessageId: "",
122 + generationId: "",
123 + content: "",
124 + reasoning: "",
125 + error: json.error ?? "The request failed.",
126 + }
127 + );
128 + return;
129 + }
130 +
131 + convId = res.headers.get("X-Conversation-Id") ?? convId;
132 + if (convId && activeIdRef.current !== convId) {
133 + setActiveId(convId);
134 + refreshConversations();
135 + }
136 +
137 + const reader = res.body.getReader();
138 + const decoder = new TextDecoder();
139 + let buffer = "";
140 + for (;;) {
141 + const { done, value } = await reader.read();
142 + if (done) break;
143 + buffer += decoder.decode(value, { stream: true });
144 + let idx: number;
145 + while ((idx = buffer.indexOf("\n")) !== -1) {
146 + const line = buffer.slice(0, idx).trim();
147 + buffer = buffer.slice(idx + 1);
148 + if (!line.startsWith("data:")) continue;
149 + const payload = line.slice(5).trim();
150 + if (!payload) continue;
151 + let event: Record<string, unknown>;
152 + try {
153 + event = JSON.parse(payload);
154 + } catch {
155 + continue;
156 + }
157 + switch (event.type) {
158 + case "meta":
159 + setStreaming({
160 + assistantMessageId: event.assistantMessageId as string,
161 + generationId: event.generationId as string,
162 + content: "",
163 + reasoning: "",
164 + error: null,
165 + });
166 + break;
167 + case "content.delta":
168 + setStreaming((s) => (s ? { ...s, content: s.content + (event.text as string) } : s));
169 + break;
170 + case "reasoning.delta":
171 + setStreaming((s) => (s ? { ...s, reasoning: s.reasoning + (event.text as string) } : s));
172 + break;
173 + case "generation.error":
174 + setStreaming((s) => (s ? { ...s, error: event.message as string } : s));
175 + break;
176 + }
177 + }
178 + }
179 + } catch {
180 + // network drop or user abort — server state is canonical; resync below
181 + } finally {
182 + abortRef.current = null;
183 + setPendingUser(null);
184 + setStreaming(null);
185 + const target = convId ?? activeIdRef.current;
186 + if (target) {
187 + if (activeIdRef.current === null) setActiveId(target);
188 + await loadConversation(target);
189 + }
190 + refreshConversations();
191 + }
192 + },
193 + [loadConversation, refreshConversations]
194 + );
195 +
196 + const sendMessage = useCallback(
197 + async (content: string) => {
198 + if (!modelId || streaming) return;
199 + const parentId = thread.length ? thread[thread.length - 1].id : null;
200 + // Optimistic: show the user message instantly, marked pending.
201 + setPendingUser({
202 + id: `pending-${Date.now()}`,
203 + conversation_id: activeId ?? "",
204 + parent_id: parentId,
205 + role: "user",
206 + content,
207 + reasoning: null,
208 + model_id: null,
209 + model_name: null,
210 + provider: null,
211 + generation_id: null,
212 + status: "pending",
213 + error_message: null,
214 + created_at: Date.now(),
215 + });
216 + await runStream({
217 + conversationId: activeId ?? undefined,
218 + parentId,
219 + content,
220 + modelId,
221 + });
222 + },
223 + [modelId, streaming, thread, activeId, runStream]
224 + );
225 +
226 + const regenerate = useCallback(
227 + async (assistantMessageId: string, withModelId?: string) => {
228 + if (streaming) return;
229 + await runStream({
230 + regenerateOf: assistantMessageId,
231 + modelId: withModelId ?? modelId,
232 + });
233 + },
234 + [streaming, modelId, runStream]
235 + );
236 +
237 + const stopGeneration = useCallback(async () => {
238 + const genId = streaming?.generationId;
239 + if (genId) {
240 + await fetch(`/api/generations/${genId}/cancel`, { method: "POST" }).catch(() => {});
241 + }
242 + abortRef.current?.abort();
243 + }, [streaming]);
244 +
245 + const selectBranch = useCallback(
246 + (parentKey: string, childId: string) => {
247 + const next = new Map(branchChoice);
248 + next.set(parentKey, childId);
249 + setBranchChoice(next);
250 + // Persist the new active leaf so other devices resume the same branch.
251 + const newThread = computeThread(messages, next);
252 + const leaf = newThread[newThread.length - 1];
253 + if (leaf && activeId) {
254 + fetch(`/api/conversations/${activeId}`, {
255 + method: "PATCH",
256 + headers: { "Content-Type": "application/json" },
257 + body: JSON.stringify({ currentLeafId: leaf.id }),
258 + }).catch(() => {});
259 + }
260 + },
261 + [branchChoice, messages, activeId]
262 + );
263 +
264 + const newConversation = useCallback(() => {
265 + setActiveId(null);
266 + setDrawerOpen(false);
267 + }, []);
268 +
269 + const deleteConversation = useCallback(
270 + async (id: string) => {
271 + await fetch(`/api/conversations/${id}`, { method: "DELETE" });
272 + if (activeIdRef.current === id) setActiveId(null);
273 + refreshConversations();
274 + },
275 + [refreshConversations]
276 + );
277 +
278 + const toggleFavorite = useCallback(async (id: string, favorite: boolean) => {
279 + setModels((ms) => ms.map((m) => (m.id === id ? { ...m, favorite } : m)));
280 + await fetch("/api/models/prefs", {
281 + method: "POST",
282 + headers: { "Content-Type": "application/json" },
283 + body: JSON.stringify({ modelId: id, favorite }),
284 + }).catch(() => {});
285 + }, []);
286 +
287 + const activeConv = conversations.find((c) => c.id === activeId) ?? null;
288 +
289 + return (
290 + <div className="app">
291 + <Sidebar
292 + conversations={conversations}
293 + activeId={activeId}
294 + open={drawerOpen}
295 + onSelect={(id) => {
296 + setActiveId(id);
297 + setDrawerOpen(false);
298 + }}
299 + onNew={newConversation}
300 + onDelete={deleteConversation}
301 + onClose={() => setDrawerOpen(false)}
302 + />
303 + <div className="main">
304 + <header className="topbar">
305 + <button
306 + className="icon-btn menu-btn"
307 + aria-label="Open conversations"
308 + onClick={() => setDrawerOpen(true)}
309 + >
310 + <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6">
311 + <path d="M3 5h14M3 10h14M3 15h14" strokeLinecap="round" />
312 + </svg>
313 + </button>
314 + <span className="topbar-title">{activeConv?.title ?? "New conversation"}</span>
315 + <button className="icon-btn" aria-label="New conversation" onClick={newConversation}>
316 + <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6">
317 + <path d="M10 4v12M4 10h12" strokeLinecap="round" />
318 + </svg>
319 + </button>
320 + </header>
321 +
322 + <MessageList
323 + thread={thread}
324 + allMessages={messages}
325 + pendingUser={pendingUser}
326 + streaming={streaming}
327 + onSelectBranch={selectBranch}
328 + onRegenerate={regenerate}
329 + models={models}
330 + currentModelName={model?.name ?? null}
331 + />
332 +
333 + <Composer
334 + model={model}
335 + contextTokens={contextTokens}
336 + streaming={Boolean(streaming)}
337 + onSend={sendMessage}
338 + onStop={stopGeneration}
339 + onOpenModelSheet={() => setSheetOpen(true)}
340 + />
341 + </div>
342 +
343 + {sheetOpen && (
344 + <ModelSheet
345 + models={models}
346 + selectedId={modelId}
347 + onSelect={(id) => {
348 + setModelId(id);
349 + setSheetOpen(false);
350 + }}
351 + onToggleFavorite={toggleFavorite}
352 + onClose={() => setSheetOpen(false)}
353 + />
354 + )}
355 + </div>
356 + );
357 +}
added src/components/Composer.tsx +115 −0
@@ -0,0 +1,115 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useRef, useState } from "react";
8 +import { estimateTokensClient, formatTokens, perMillion, providerGlyph, type ApiModel } from "./types";
9 +
10 +interface ComposerProps {
11 + model: ApiModel | null;
12 + contextTokens: number;
13 + streaming: boolean;
14 + onSend: (content: string) => void;
15 + onStop: () => void;
16 + onOpenModelSheet: () => void;
17 +}
18 +
19 +export function Composer({ model, contextTokens, streaming, onSend, onStop, onOpenModelSheet }: ComposerProps) {
20 + const [text, setText] = useState("");
21 + const taRef = useRef<HTMLTextAreaElement>(null);
22 +
23 + const totalTokens = contextTokens + estimateTokensClient(text);
24 + const ctxLimit = model?.contextLength;
25 + const ctxPct = ctxLimit ? Math.min((totalTokens / ctxLimit) * 100, 100) : 0;
26 +
27 + const send = () => {
28 + const content = text.trim();
29 + if (!content || streaming || !model) return;
30 + setText("");
31 + if (taRef.current) taRef.current.style.height = "auto";
32 + onSend(content);
33 + };
34 +
35 + const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
36 + // Enter sends on desktop; on touch keyboards Enter makes a newline.
37 + if (e.key === "Enter" && !e.shiftKey && !isTouchDevice()) {
38 + e.preventDefault();
39 + send();
40 + }
41 + };
42 +
43 + const autoGrow = () => {
44 + const ta = taRef.current;
45 + if (!ta) return;
46 + ta.style.height = "auto";
47 + ta.style.height = `${Math.min(ta.scrollHeight, window.innerHeight * 0.4)}px`;
48 + };
49 +
50 + return (
51 + <div className="composer-wrap">
52 + <div className="composer">
53 + {/* Model Rail — the machined cartridge. Tap to open the model sheet. */}
54 + <button
55 + type="button"
56 + className={`model-rail${streaming ? " live" : ""}`}
57 + onClick={onOpenModelSheet}
58 + aria-label="Change model"
59 + >
60 + <span className="rail-glyph">{providerGlyph(model?.provider)}</span>
61 + <span className="rail-main">
62 + <span className="rail-model-id">{model?.id ?? "select a model"}</span>
63 + <span className="rail-meta">
64 + <span>
65 + {formatTokens(totalTokens)}
66 + {ctxLimit ? ` / ${formatTokens(ctxLimit)}` : ""}
67 + </span>
68 + <span className="ctx-meter" aria-hidden>
69 + <span className="fill" style={{ width: `${ctxPct}%` }} />
70 + </span>
71 + <span className="price-chip">{perMillion(model?.pricing?.completion)}</span>
72 + </span>
73 + </span>
74 + <span className="rail-chevron" aria-hidden>
75 + <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
76 + <path d="M4 5.5 7 8.5l3-3" strokeLinecap="round" strokeLinejoin="round" />
77 + </svg>
78 + </span>
79 + </button>
80 +
81 + <div className="composer-box">
82 + <textarea
83 + ref={taRef}
84 + rows={1}
85 + placeholder={model ? "Message…" : "Pick a model first…"}
86 + value={text}
87 + onChange={(e) => {
88 + setText(e.target.value);
89 + autoGrow();
90 + }}
91 + onKeyDown={onKeyDown}
92 + aria-label="Message"
93 + />
94 + {streaming ? (
95 + <button className="send-btn stop" onClick={onStop} aria-label="Stop generation">
96 + <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
97 + <rect x="4" y="4" width="8" height="8" rx="1.5" />
98 + </svg>
99 + </button>
100 + ) : (
101 + <button className="send-btn" onClick={send} disabled={!text.trim() || !model} aria-label="Send message">
102 + <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.8">
103 + <path d="M9 15V3M4 8l5-5 5 5" strokeLinecap="round" strokeLinejoin="round" />
104 + </svg>
105 + </button>
106 + )}
107 + </div>
108 + </div>
109 + </div>
110 + );
111 +}
112 +
113 +function isTouchDevice(): boolean {
114 + return typeof window !== "undefined" && window.matchMedia("(pointer: coarse)").matches;
115 +}
added src/components/Markdown.tsx +72 −0
@@ -0,0 +1,72 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { memo, useState, type ReactNode } from "react";
8 +import ReactMarkdown from "react-markdown";
9 +import remarkGfm from "remark-gfm";
10 +import remarkMath from "remark-math";
11 +import rehypeKatex from "rehype-katex";
12 +import rehypeHighlight from "rehype-highlight";
13 +
14 +function CodeBlock({ className, children }: { className?: string; children?: ReactNode }) {
15 + const [copied, setCopied] = useState(false);
16 + const language = /language-(\w+)/.exec(className ?? "")?.[1] ?? "";
17 +
18 + const extractText = (node: ReactNode): string => {
19 + if (typeof node === "string") return node;
20 + if (Array.isArray(node)) return node.map(extractText).join("");
21 + if (node && typeof node === "object" && "props" in node) {
22 + return extractText((node as { props: { children?: ReactNode } }).props.children);
23 + }
24 + return "";
25 + };
26 +
27 + const copy = async () => {
28 + try {
29 + await navigator.clipboard.writeText(extractText(children));
30 + setCopied(true);
31 + setTimeout(() => setCopied(false), 1500);
32 + } catch {
33 + /* clipboard unavailable */
34 + }
35 + };
36 +
37 + return (
38 + <pre>
39 + <div className="code-block-header">
40 + <span>{language || "text"}</span>
41 + <button type="button" onClick={copy}>
42 + {copied ? "Copied" : "Copy"}
43 + </button>
44 + </div>
45 + <code className={className}>{children}</code>
46 + </pre>
47 + );
48 +}
49 +
50 +export const Markdown = memo(function Markdown({ content }: { content: string }) {
51 + return (
52 + <ReactMarkdown
53 + remarkPlugins={[remarkGfm, remarkMath]}
54 + rehypePlugins={[rehypeKatex, [rehypeHighlight, { ignoreMissing: true, detect: false }]]}
55 + components={{
56 + pre: ({ children }) => <>{children}</>,
57 + code: ({ className, children }) => {
58 + const isBlock = /language-/.test(className ?? "") || String(children).includes("\n");
59 + if (isBlock) return <CodeBlock className={className}>{children}</CodeBlock>;
60 + return <code className={className}>{children}</code>;
61 + },
62 + a: ({ href, children }) => (
63 + <a href={href} target="_blank" rel="noopener noreferrer">
64 + {children}
65 + </a>
66 + ),
67 + }}
68 + >
69 + {content}
70 + </ReactMarkdown>
71 + );
72 +});
added src/components/MessageList.tsx +256 −0
@@ -0,0 +1,256 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useEffect, useRef, useState } from "react";
8 +import { Markdown } from "./Markdown";
9 +import { siblingInfo, type ApiMessage, type ApiModel } from "./types";
10 +
11 +interface StreamingView {
12 + assistantMessageId: string;
13 + generationId: string;
14 + content: string;
15 + reasoning: string;
16 + error: string | null;
17 +}
18 +
19 +interface MessageListProps {
20 + thread: ApiMessage[];
21 + allMessages: ApiMessage[];
22 + pendingUser: ApiMessage | null;
23 + streaming: StreamingView | null;
24 + models: ApiModel[];
25 + currentModelName: string | null;
26 + onSelectBranch: (parentKey: string, childId: string) => void;
27 + onRegenerate: (assistantMessageId: string, withModelId?: string) => void;
28 +}
29 +
30 +export function MessageList({
31 + thread,
32 + allMessages,
33 + pendingUser,
34 + streaming,
35 + models,
36 + currentModelName,
37 + onSelectBranch,
38 + onRegenerate,
39 +}: MessageListProps) {
40 + const scrollRef = useRef<HTMLDivElement>(null);
41 + const [autoFollow, setAutoFollow] = useState(true);
42 +
43 + // Streaming text auto-follows the bottom; scrolling up pauses it.
44 + useEffect(() => {
45 + const el = scrollRef.current;
46 + if (!el) return;
47 + const onScroll = () => {
48 + const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
49 + setAutoFollow(nearBottom);
50 + };
51 + el.addEventListener("scroll", onScroll, { passive: true });
52 + return () => el.removeEventListener("scroll", onScroll);
53 + }, []);
54 +
55 + useEffect(() => {
56 + const el = scrollRef.current;
57 + if (el && autoFollow) el.scrollTop = el.scrollHeight;
58 + }, [thread, pendingUser, streaming?.content, streaming?.reasoning, autoFollow]);
59 +
60 + const jumpToLatest = () => {
61 + const el = scrollRef.current;
62 + if (el) el.scrollTop = el.scrollHeight;
63 + setAutoFollow(true);
64 + };
65 +
66 + const isEmpty = thread.length === 0 && !pendingUser && !streaming;
67 +
68 + return (
69 + <div className="message-scroll" ref={scrollRef}>
70 + {isEmpty ? (
71 + <div className="empty-state" style={{ display: "flex", flexDirection: "column", justifyContent: "center", minHeight: "60dvh" }}>
72 + <div>
73 + <div className="glyph">▮▯▮</div>
74 + <h2>Pick a model and ask something</h2>
75 + <p>{currentModelName ? `${currentModelName} is loaded and ready.` : "Loading the model catalog…"}</p>
76 + </div>
77 + </div>
78 + ) : (
79 + <div className="message-column">
80 + {thread.map((m) => (
81 + <MessageItem
82 + key={m.id}
83 + message={m}
84 + allMessages={allMessages}
85 + streaming={streaming}
86 + models={models}
87 + onSelectBranch={onSelectBranch}
88 + onRegenerate={onRegenerate}
89 + />
90 + ))}
91 + {pendingUser && <div className="msg-user pending">{pendingUser.content}</div>}
92 + {streaming && !thread.some((m) => m.id === streaming.assistantMessageId) && (
93 + <StreamingMessage streaming={streaming} modelName={currentModelName} />
94 + )}
95 + </div>
96 + )}
97 + {!autoFollow && (streaming || thread.length > 0) && (
98 + <button className="jump-pill" onClick={jumpToLatest}>
99 + ↓ jump to latest
100 + </button>
101 + )}
102 + </div>
103 + );
104 +}
105 +
106 +function StreamingMessage({ streaming, modelName }: { streaming: StreamingView; modelName: string | null }) {
107 + return (
108 + <div className="msg-assistant">
109 + <div className="msg-attribution">
110 + <span className="live-dot" aria-hidden />
111 + <span className="model-name">{modelName ?? "model"}</span>
112 + <span>streaming</span>
113 + </div>
114 + {streaming.reasoning && (
115 + <details className="reasoning-block" open>
116 + <summary>reasoning</summary>
117 + <div>{streaming.reasoning}</div>
118 + </details>
119 + )}
120 + <div className={`msg-body${streaming.error ? "" : " streaming-caret"}`}>
121 + <Markdown content={streaming.content} />
122 + </div>
123 + {streaming.error && <div className="msg-error">{streaming.error}</div>}
124 + </div>
125 + );
126 +}
127 +
128 +function MessageItem({
129 + message,
130 + allMessages,
131 + streaming,
132 + models,
133 + onSelectBranch,
134 + onRegenerate,
135 +}: {
136 + message: ApiMessage;
137 + allMessages: ApiMessage[];
138 + streaming: StreamingView | null;
139 + models: ApiModel[];
140 + onSelectBranch: (parentKey: string, childId: string) => void;
141 + onRegenerate: (assistantMessageId: string, withModelId?: string) => void;
142 +}) {
143 + const [copied, setCopied] = useState(false);
144 + const isStreamingThis = streaming?.assistantMessageId === message.id;
145 + const content = isStreamingThis ? streaming.content : message.content;
146 + const reasoning = isStreamingThis ? streaming.reasoning : message.reasoning;
147 + const { index, count, siblings } = siblingInfo(allMessages, message);
148 + const parentKey = message.parent_id ?? "root";
149 + const modelGone = message.model_id !== null && !models.some((m) => m.id === message.model_id && m.available);
150 +
151 + const copy = async () => {
152 + try {
153 + await navigator.clipboard.writeText(content);
154 + setCopied(true);
155 + setTimeout(() => setCopied(false), 1500);
156 + } catch {
157 + /* clipboard unavailable */
158 + }
159 + };
160 +
161 + if (message.role === "user") {
162 + return (
163 + <>
164 + <div className="msg-user">{content}</div>
165 + {count > 1 && (
166 + <BranchNav
167 + index={index}
168 + count={count}
169 + onPrev={() => onSelectBranch(parentKey, siblings[index - 1].id)}
170 + onNext={() => onSelectBranch(parentKey, siblings[index + 1].id)}
171 + />
172 + )}
173 + </>
174 + );
175 + }
176 +
177 + return (
178 + <div className="msg-assistant">
179 + <div className="msg-attribution">
180 + {isStreamingThis && <span className="live-dot" aria-hidden />}
181 + <span className="model-name">{message.model_name ?? message.model_id ?? "model"}</span>
182 + {modelGone && !isStreamingThis && <span title="This model is no longer in the catalog">· Unavailable</span>}
183 + <span>
184 + ·{" "}
185 + {new Date(message.created_at).toLocaleTimeString([], {
186 + hour: "2-digit",
187 + minute: "2-digit",
188 + })}
189 + </span>
190 + {message.status === "cancelled" && <span>· stopped</span>}
191 + </div>
192 +
193 + {reasoning && (
194 + <details className="reasoning-block" open={isStreamingThis && !content}>
195 + <summary>reasoning</summary>
196 + <div>{reasoning}</div>
197 + </details>
198 + )}
199 +
200 + <div className={`msg-body${isStreamingThis && !streaming?.error ? " streaming-caret" : ""}`}>
201 + <Markdown content={content} />
202 + </div>
203 +
204 + {(message.status === "failed" || (isStreamingThis && streaming?.error)) && (
205 + <div className="msg-error">
206 + {isStreamingThis ? streaming?.error : message.error_message ?? "This generation failed."}
207 + </div>
208 + )}
209 +
210 + {!isStreamingThis && (
211 + <div className="msg-actions">
212 + <button className="msg-action-btn" onClick={copy}>
213 + {copied ? "Copied" : "Copy"}
214 + </button>
215 + <button className="msg-action-btn" onClick={() => onRegenerate(message.id)} title="Regenerate with the current model — creates a branch">
216 + ⟳ Regenerate
217 + </button>
218 + {count > 1 && (
219 + <BranchNav
220 + index={index}
221 + count={count}
222 + onPrev={() => onSelectBranch(parentKey, siblings[index - 1].id)}
223 + onNext={() => onSelectBranch(parentKey, siblings[index + 1].id)}
224 + />
225 + )}
226 + </div>
227 + )}
228 + </div>
229 + );
230 +}
231 +
232 +function BranchNav({
233 + index,
234 + count,
235 + onPrev,
236 + onNext,
237 +}: {
238 + index: number;
239 + count: number;
240 + onPrev: () => void;
241 + onNext: () => void;
242 +}) {
243 + return (
244 + <div className="branch-nav" style={{ alignSelf: "flex-end" }}>
245 + <button aria-label="Previous branch" disabled={index <= 0} onClick={onPrev}>
246 +
247 + </button>
248 + <span>
249 + {index + 1}/{count}
250 + </span>
251 + <button aria-label="Next branch" disabled={index >= count - 1} onClick={onNext}>
252 +
253 + </button>
254 + </div>
255 + );
256 +}
added src/components/ModelSheet.tsx +270 −0
@@ -0,0 +1,270 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useEffect, useMemo, useRef, useState } from "react";
8 +import { formatTokens, perMillion, providerGlyph, type ApiModel } from "./types";
9 +
10 +interface ModelSheetProps {
11 + models: ApiModel[];
12 + selectedId: string | null;
13 + onSelect: (id: string) => void;
14 + onToggleFavorite: (id: string, favorite: boolean) => void;
15 + onClose: () => void;
16 +}
17 +
18 +type CapFilter = "reasoning" | "vision" | "tools" | "free";
19 +type SortKey = "name" | "context" | "price" | "newest";
20 +
21 +const PAGE = 80;
22 +
23 +/** Subsequence fuzzy match with a crude score (lower = better). */
24 +function fuzzyScore(query: string, target: string): number | null {
25 + const q = query.toLowerCase();
26 + const t = target.toLowerCase();
27 + const direct = t.indexOf(q);
28 + if (direct !== -1) return direct;
29 + let qi = 0;
30 + let gaps = 0;
31 + let last = -1;
32 + for (let ti = 0; ti < t.length && qi < q.length; ti++) {
33 + if (t[ti] === q[qi]) {
34 + if (last !== -1) gaps += ti - last - 1;
35 + last = ti;
36 + qi++;
37 + }
38 + }
39 + return qi === q.length ? 1000 + gaps : null;
40 +}
41 +
42 +export function ModelSheet({ models, selectedId, onSelect, onToggleFavorite, onClose }: ModelSheetProps) {
43 + const [query, setQuery] = useState("");
44 + const [caps, setCaps] = useState<Set<CapFilter>>(new Set());
45 + const [provider, setProvider] = useState<string | null>(null);
46 + const [sort, setSort] = useState<SortKey>("name");
47 + const [limit, setLimit] = useState(PAGE);
48 + const listRef = useRef<HTMLDivElement>(null);
49 + const inputRef = useRef<HTMLInputElement>(null);
50 +
51 + useEffect(() => {
52 + // Focus search on desktop only — on mobile the keyboard would cover the list.
53 + if (!window.matchMedia("(pointer: coarse)").matches) inputRef.current?.focus();
54 + const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
55 + window.addEventListener("keydown", onKey);
56 + return () => window.removeEventListener("keydown", onKey);
57 + }, [onClose]);
58 +
59 + const providers = useMemo(() => {
60 + const counts = new Map<string, number>();
61 + for (const m of models) {
62 + if (!m.available || !m.provider) continue;
63 + counts.set(m.provider, (counts.get(m.provider) ?? 0) + 1);
64 + }
65 + return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([p]) => p);
66 + }, [models]);
67 +
68 + const filtered = useMemo(() => {
69 + let list = models.filter((m) => m.available);
70 + if (provider) list = list.filter((m) => m.provider === provider);
71 + if (caps.has("reasoning")) list = list.filter((m) => m.capabilities.reasoning);
72 + if (caps.has("vision")) list = list.filter((m) => m.capabilities.vision);
73 + if (caps.has("tools")) list = list.filter((m) => m.capabilities.tools);
74 + if (caps.has("free")) list = list.filter((m) => (m.pricing?.completion ?? 0) === 0 && (m.pricing?.prompt ?? 0) === 0);
75 +
76 + if (query.trim()) {
77 + const scored: Array<{ m: ApiModel; s: number }> = [];
78 + for (const m of list) {
79 + const s = fuzzyScore(query.trim(), `${m.name} ${m.id} ${m.provider ?? ""}`);
80 + if (s !== null) scored.push({ m, s });
81 + }
82 + scored.sort((a, b) => a.s - b.s);
83 + return scored.map((x) => x.m);
84 + }
85 +
86 + const sorted = [...list];
87 + switch (sort) {
88 + case "context":
89 + sorted.sort((a, b) => (b.contextLength ?? 0) - (a.contextLength ?? 0));
90 + break;
91 + case "price":
92 + sorted.sort((a, b) => (a.pricing?.completion ?? 0) - (b.pricing?.completion ?? 0));
93 + break;
94 + case "newest":
95 + sorted.sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0));
96 + break;
97 + default:
98 + sorted.sort((a, b) => a.name.localeCompare(b.name));
99 + }
100 + return sorted;
101 + }, [models, query, caps, provider, sort]);
102 +
103 + const favorites = useMemo(
104 + () => (query ? [] : filtered.filter((m) => m.favorite || m.pinned)),
105 + [filtered, query]
106 + );
107 + const recents = useMemo(
108 + () =>
109 + query
110 + ? []
111 + : filtered
112 + .filter((m) => m.lastUsedAt && !m.favorite && !m.pinned)
113 + .sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0))
114 + .slice(0, 5),
115 + [filtered, query]
116 + );
117 + const rest = useMemo(() => {
118 + const shown = new Set([...favorites, ...recents].map((m) => m.id));
119 + return filtered.filter((m) => !shown.has(m.id));
120 + }, [filtered, favorites, recents]);
121 +
122 + // Incremental rendering keeps the list fast with 300+ entries.
123 + useEffect(() => setLimit(PAGE), [query, caps, provider, sort]);
124 + const onScroll = () => {
125 + const el = listRef.current;
126 + if (el && el.scrollHeight - el.scrollTop - el.clientHeight < 600 && limit < rest.length) {
127 + setLimit((l) => l + PAGE);
128 + }
129 + };
130 +
131 + const toggleCap = (c: CapFilter) => {
132 + const next = new Set(caps);
133 + if (next.has(c)) next.delete(c);
134 + else next.add(c);
135 + setCaps(next);
136 + };
137 +
138 + return (
139 + <>
140 + <div className="sheet-scrim" onClick={onClose} aria-hidden />
141 + <div className="sheet" role="dialog" aria-modal="true" aria-label="Choose a model">
142 + <div className="sheet-handle" aria-hidden />
143 + <div className="sheet-search">
144 + <input
145 + ref={inputRef}
146 + placeholder={`Search ${models.filter((m) => m.available).length} models…`}
147 + value={query}
148 + onChange={(e) => setQuery(e.target.value)}
149 + aria-label="Search models"
150 + />
151 + <button className="icon-btn" onClick={onClose} aria-label="Close">
152 + <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.6">
153 + <path d="m4.5 4.5 9 9m0-9-9 9" strokeLinecap="round" />
154 + </svg>
155 + </button>
156 + </div>
157 +
158 + <div className="filter-row">
159 + {(["reasoning", "vision", "tools", "free"] as CapFilter[]).map((c) => (
160 + <button key={c} className={`filter-chip${caps.has(c) ? " on" : ""}`} onClick={() => toggleCap(c)}>
161 + {c}
162 + </button>
163 + ))}
164 + <span style={{ width: 1, background: "var(--border)", flex: "none", margin: "4px 2px" }} />
165 + {providers.map((p) => (
166 + <button
167 + key={p}
168 + className={`filter-chip${provider === p ? " on" : ""}`}
169 + onClick={() => setProvider(provider === p ? null : p)}
170 + >
171 + {p}
172 + </button>
173 + ))}
174 + </div>
175 +
176 + <div className="filter-row" style={{ paddingTop: 0 }}>
177 + {(
178 + [
179 + ["name", "A–Z"],
180 + ["context", "context ↓"],
181 + ["price", "price ↑"],
182 + ["newest", "recent"],
183 + ] as [SortKey, string][]
184 + ).map(([k, label]) => (
185 + <button key={k} className={`filter-chip${sort === k ? " on" : ""}`} onClick={() => setSort(k)}>
186 + {label}
187 + </button>
188 + ))}
189 + </div>
190 +
191 + <div className="model-list" ref={listRef} onScroll={onScroll}>
192 + {favorites.length > 0 && (
193 + <>
194 + <div className="model-section-label">Favorites</div>
195 + {favorites.map((m) => (
196 + <ModelRow key={m.id} m={m} selected={m.id === selectedId} onSelect={onSelect} onToggleFavorite={onToggleFavorite} />
197 + ))}
198 + </>
199 + )}
200 + {recents.length > 0 && (
201 + <>
202 + <div className="model-section-label">Recent</div>
203 + {recents.map((m) => (
204 + <ModelRow key={m.id} m={m} selected={m.id === selectedId} onSelect={onSelect} onToggleFavorite={onToggleFavorite} />
205 + ))}
206 + </>
207 + )}
208 + <div className="model-section-label">{query ? "Results" : "All models"}</div>
209 + {rest.slice(0, limit).map((m) => (
210 + <ModelRow key={m.id} m={m} selected={m.id === selectedId} onSelect={onSelect} onToggleFavorite={onToggleFavorite} />
211 + ))}
212 + {rest.length === 0 && favorites.length === 0 && recents.length === 0 && (
213 + <p style={{ padding: 16, color: "var(--text-dim)", fontSize: 14 }}>
214 + No models match. Clear a filter or try another search.
215 + </p>
216 + )}
217 + </div>
218 + </div>
219 + </>
220 + );
221 +}
222 +
223 +function ModelRow({
224 + m,
225 + selected,
226 + onSelect,
227 + onToggleFavorite,
228 +}: {
229 + m: ApiModel;
230 + selected: boolean;
231 + onSelect: (id: string) => void;
232 + onToggleFavorite: (id: string, favorite: boolean) => void;
233 +}) {
234 + return (
235 + <div
236 + className={`model-row${selected ? " selected" : ""}`}
237 + role="button"
238 + tabIndex={0}
239 + onClick={() => onSelect(m.id)}
240 + onKeyDown={(e) => e.key === "Enter" && onSelect(m.id)}
241 + >
242 + <span className="prov-glyph">{providerGlyph(m.provider)}</span>
243 + <span className="m-main">
244 + <div className="m-name">{m.name}</div>
245 + <div className="m-sub">
246 + <span>{m.id}</span>
247 + <span>{m.contextLength ? formatTokens(m.contextLength) : "—"}</span>
248 + <span style={{ color: "var(--amber-500)" }}>{perMillion(m.pricing?.completion)}</span>
249 + </div>
250 + </span>
251 + <span className="cap-badges" aria-hidden>
252 + {m.capabilities.reasoning && <span className="cap-badge">R</span>}
253 + {m.capabilities.vision && <span className="cap-badge">V</span>}
254 + {m.capabilities.tools && <span className="cap-badge">T</span>}
255 + </span>
256 + <button
257 + className={`fav-btn${m.favorite ? " on" : ""}`}
258 + aria-label={m.favorite ? "Remove from favorites" : "Add to favorites"}
259 + onClick={(e) => {
260 + e.stopPropagation();
261 + onToggleFavorite(m.id, !m.favorite);
262 + }}
263 + >
264 + <svg width="16" height="16" viewBox="0 0 16 16" fill={m.favorite ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.3">
265 + <path d="M8 1.8l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.6l-3.8 2 .7-4.3-3.1-3 4.3-.6z" strokeLinejoin="round" />
266 + </svg>
267 + </button>
268 + </div>
269 + );
270 +}
added src/components/RegisterSw.tsx +16 −0
@@ -0,0 +1,16 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useEffect } from "react";
8 +
9 +export function RegisterSw() {
10 + useEffect(() => {
11 + if ("serviceWorker" in navigator) {
12 + navigator.serviceWorker.register("/sw.js").catch(() => {});
13 + }
14 + }, []);
15 + return null;
16 +}
added src/components/SettingsView.tsx +172 −0
@@ -0,0 +1,172 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useCallback, useEffect, useState } from "react";
8 +import { formatTokens, formatUsd } from "./types";
9 +
10 +interface UsageTotals {
11 + requests: number;
12 + prompt_tokens: number;
13 + completion_tokens: number;
14 + reasoning_tokens: number;
15 + total_tokens: number;
16 + cost_usd: number;
17 +}
18 +
19 +interface UsageByModel {
20 + model_id: string;
21 + model_name: string;
22 + provider: string | null;
23 + requests: number;
24 + total_tokens: number;
25 + cost_usd: number;
26 +}
27 +
28 +const PERIODS = [
29 + ["today", "Today"],
30 + ["7d", "7 days"],
31 + ["30d", "30 days"],
32 + ["all", "All time"],
33 +] as const;
34 +
35 +export function SettingsView() {
36 + const [period, setPeriod] = useState<string>("7d");
37 + const [totals, setTotals] = useState<UsageTotals | null>(null);
38 + const [byModel, setByModel] = useState<UsageByModel[]>([]);
39 + const [syncing, setSyncing] = useState(false);
40 + const [syncMsg, setSyncMsg] = useState<string | null>(null);
41 + const [theme, setTheme] = useState<string>("auto");
42 +
43 + useEffect(() => {
44 + setTheme(localStorage.getItem("spb-theme") ?? "auto");
45 + }, []);
46 +
47 + const loadUsage = useCallback(async (p: string) => {
48 + const res = await fetch(`/api/usage?period=${p}`);
49 + if (!res.ok) return;
50 + const json = await res.json();
51 + setTotals(json.totals);
52 + setByModel(json.byModel);
53 + }, []);
54 +
55 + useEffect(() => {
56 + loadUsage(period);
57 + }, [period, loadUsage]);
58 +
59 + async function syncCatalog() {
60 + setSyncing(true);
61 + setSyncMsg(null);
62 + try {
63 + const res = await fetch("/api/models/sync", { method: "POST" });
64 + const json = await res.json();
65 + setSyncMsg(res.ok ? `Synced ${json.total} models (${json.added} new).` : json.error ?? "Sync failed.");
66 + } catch {
67 + setSyncMsg("Sync failed. Check connectivity.");
68 + }
69 + setSyncing(false);
70 + }
71 +
72 + function applyTheme(next: string) {
73 + setTheme(next);
74 + if (next === "auto") {
75 + localStorage.removeItem("spb-theme");
76 + delete document.documentElement.dataset.theme;
77 + } else {
78 + localStorage.setItem("spb-theme", next);
79 + document.documentElement.dataset.theme = next;
80 + }
81 + }
82 +
83 + return (
84 + <div className="settings-page">
85 + <a href="/" style={{ fontSize: 14, textDecoration: "none" }}>
86 + ← Back to chat
87 + </a>
88 + <h1>Settings</h1>
89 +
90 + <h2>Usage</h2>
91 + <div className="period-tabs">
92 + {PERIODS.map(([k, label]) => (
93 + <button key={k} className={`filter-chip${period === k ? " on" : ""}`} onClick={() => setPeriod(k)}>
94 + {label}
95 + </button>
96 + ))}
97 + </div>
98 + <div className="stat-grid">
99 + <div className="stat-tile">
100 + <div className="v">{totals?.requests ?? "—"}</div>
101 + <div className="k">Requests</div>
102 + </div>
103 + <div className="stat-tile">
104 + <div className="v">{formatTokens(totals?.prompt_tokens)}</div>
105 + <div className="k">Input tokens</div>
106 + </div>
107 + <div className="stat-tile">
108 + <div className="v">{formatTokens(totals?.completion_tokens)}</div>
109 + <div className="k">Output tokens</div>
110 + </div>
111 + <div className="stat-tile">
112 + <div className="v cost">{formatUsd(totals?.cost_usd)}</div>
113 + <div className="k">Total cost</div>
114 + </div>
115 + </div>
116 +
117 + {byModel.length > 0 && (
118 + <>
119 + <h2>By model</h2>
120 + <table className="usage-table">
121 + <thead>
122 + <tr>
123 + <th>Model</th>
124 + <th className="num">Requests</th>
125 + <th className="num">Tokens</th>
126 + <th className="num">Cost</th>
127 + </tr>
128 + </thead>
129 + <tbody>
130 + {byModel.map((m) => (
131 + <tr key={m.model_id}>
132 + <td>{m.model_name}</td>
133 + <td className="num">{m.requests}</td>
134 + <td className="num">{formatTokens(m.total_tokens)}</td>
135 + <td className="num" style={{ color: "var(--amber-500)" }}>
136 + {formatUsd(m.cost_usd)}
137 + </td>
138 + </tr>
139 + ))}
140 + </tbody>
141 + </table>
142 + </>
143 + )}
144 +
145 + <h2>Appearance</h2>
146 + <div className="settings-row">
147 + <div>
148 + <div className="r-label">Theme</div>
149 + <div className="r-sub">Dark is the native face of the instrument.</div>
150 + </div>
151 + <div style={{ display: "flex", gap: 6 }}>
152 + {["auto", "dark", "light"].map((t) => (
153 + <button key={t} className={`filter-chip${theme === t ? " on" : ""}`} onClick={() => applyTheme(t)}>
154 + {t}
155 + </button>
156 + ))}
157 + </div>
158 + </div>
159 +
160 + <h2>Model catalog</h2>
161 + <div className="settings-row">
162 + <div>
163 + <div className="r-label">Refresh catalog</div>
164 + <div className="r-sub">{syncMsg ?? "Pull the latest models from OpenRouter."}</div>
165 + </div>
166 + <button className="btn" onClick={syncCatalog} disabled={syncing}>
167 + {syncing ? "Syncing…" : "Sync now"}
168 + </button>
169 + </div>
170 + </div>
171 + );
172 +}
added src/components/Sidebar.tsx +93 −0
@@ -0,0 +1,93 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +"use client";
6 +
7 +import { useRouter } from "next/navigation";
8 +import type { ApiConversation } from "./types";
9 +
10 +interface SidebarProps {
11 + conversations: ApiConversation[];
12 + activeId: string | null;
13 + open: boolean;
14 + onSelect: (id: string) => void;
15 + onNew: () => void;
16 + onDelete: (id: string) => void;
17 + onClose: () => void;
18 +}
19 +
20 +export function Sidebar({ conversations, activeId, open, onSelect, onNew, onDelete, onClose }: SidebarProps) {
21 + const router = useRouter();
22 +
23 + async function logout() {
24 + await fetch("/api/auth/logout", { method: "POST" });
25 + router.replace("/login");
26 + router.refresh();
27 + }
28 +
29 + return (
30 + <>
31 + <div className={`drawer-scrim${open ? " open" : ""}`} onClick={onClose} aria-hidden />
32 + <aside className={`sidebar${open ? " open" : ""}`}>
33 + <div className="sidebar-header">
34 + <span className="wordmark">
35 + chat.spboucher<span className="tld">.ai</span>
36 + </span>
37 + </div>
38 + <button className="new-chat-btn" onClick={onNew}>
39 + <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6">
40 + <path d="M8 3v10M3 8h10" strokeLinecap="round" />
41 + </svg>
42 + New conversation
43 + </button>
44 + <nav className="conv-list">
45 + {conversations.map((c) => (
46 + <div
47 + key={c.id}
48 + className={`conv-item${c.id === activeId ? " active" : ""}`}
49 + role="button"
50 + tabIndex={0}
51 + onClick={() => onSelect(c.id)}
52 + onKeyDown={(e) => e.key === "Enter" && onSelect(c.id)}
53 + >
54 + <span className="conv-title">{c.title}</span>
55 + <button
56 + className="conv-del"
57 + aria-label={`Delete "${c.title}"`}
58 + onClick={(e) => {
59 + e.stopPropagation();
60 + if (confirm(`Delete "${c.title}"? This cannot be undone.`)) onDelete(c.id);
61 + }}
62 + >
63 + <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4">
64 + <path d="M2 3.5h10M5.5 3.5V2h3v1.5M3.5 3.5l.7 8a1 1 0 0 0 1 .9h3.6a1 1 0 0 0 1-.9l.7-8" strokeLinecap="round" />
65 + </svg>
66 + </button>
67 + </div>
68 + ))}
69 + {conversations.length === 0 && (
70 + <p style={{ padding: "10px", fontSize: 13, color: "var(--text-dim)" }}>
71 + No conversations yet.
72 + </p>
73 + )}
74 + </nav>
75 + <div className="sidebar-footer">
76 + <a className="foot-btn" href="/settings" style={{ textDecoration: "none" }}>
77 + <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.4">
78 + <circle cx="7.5" cy="7.5" r="2.2" />
79 + <path d="M7.5 1.5v2M7.5 11.5v2M1.5 7.5h2M11.5 7.5h2M3.3 3.3l1.4 1.4M10.3 10.3l1.4 1.4M11.7 3.3l-1.4 1.4M4.7 10.3l-1.4 1.4" strokeLinecap="round" />
80 + </svg>
81 + Settings
82 + </a>
83 + <button className="foot-btn" onClick={logout}>
84 + <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.4">
85 + <path d="M9.5 2H4a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h5.5M12.5 7.5H6.5M10.5 5l2.5 2.5L10.5 10" strokeLinecap="round" />
86 + </svg>
87 + Sign out
88 + </button>
89 + </div>
90 + </aside>
91 + </>
92 + );
93 +}
added src/components/types.ts +134 −0
@@ -0,0 +1,134 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Client-side view types mirroring the API payloads.
6 +
7 +export interface ApiConversation {
8 + id: string;
9 + title: string;
10 + pinned: number;
11 + current_leaf_id: string | null;
12 + created_at: number;
13 + updated_at: number;
14 +}
15 +
16 +export interface ApiMessage {
17 + id: string;
18 + conversation_id: string;
19 + parent_id: string | null;
20 + role: "user" | "assistant" | "system";
21 + content: string;
22 + reasoning: string | null;
23 + model_id: string | null;
24 + model_name: string | null;
25 + provider: string | null;
26 + generation_id: string | null;
27 + status: "pending" | "streaming" | "completed" | "cancelled" | "failed";
28 + error_message: string | null;
29 + created_at: number;
30 +}
31 +
32 +export interface ApiModel {
33 + id: string;
34 + name: string;
35 + provider?: string;
36 + description?: string;
37 + contextLength?: number;
38 + pricing?: { prompt?: number; completion?: number; image?: number; request?: number };
39 + capabilities: {
40 + text: boolean;
41 + vision: boolean;
42 + reasoning: boolean;
43 + tools: boolean;
44 + structuredOutput: boolean;
45 + };
46 + available: boolean;
47 + favorite: boolean;
48 + pinned: boolean;
49 + lastUsedAt: number | null;
50 + useCount: number;
51 +}
52 +
53 +/** Compute the active thread through the message tree, honoring branch choices. */
54 +export function computeThread(
55 + messages: ApiMessage[],
56 + branchChoice: Map<string, string> // parentId ("root" for roots) → chosen child id
57 +): ApiMessage[] {
58 + const children = new Map<string, ApiMessage[]>();
59 + for (const m of messages) {
60 + const key = m.parent_id ?? "root";
61 + const arr = children.get(key) ?? [];
62 + arr.push(m);
63 + children.set(key, arr);
64 + }
65 + for (const arr of children.values()) arr.sort((a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id));
66 +
67 + const thread: ApiMessage[] = [];
68 + let key = "root";
69 + for (;;) {
70 + const siblings = children.get(key);
71 + if (!siblings || siblings.length === 0) break;
72 + const chosenId = branchChoice.get(key);
73 + const chosen = siblings.find((s) => s.id === chosenId) ?? siblings[siblings.length - 1];
74 + thread.push(chosen);
75 + key = chosen.id;
76 + }
77 + return thread;
78 +}
79 +
80 +/** Sibling info for branch navigation on a message. */
81 +export function siblingInfo(
82 + messages: ApiMessage[],
83 + message: ApiMessage
84 +): { index: number; count: number; siblings: ApiMessage[] } {
85 + const siblings = messages
86 + .filter((m) => (m.parent_id ?? "root") === (message.parent_id ?? "root"))
87 + .sort((a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id));
88 + return { index: siblings.findIndex((s) => s.id === message.id), count: siblings.length, siblings };
89 +}
90 +
91 +/** Branch choices that select the path from root to the given leaf. */
92 +export function choicesForLeaf(messages: ApiMessage[], leafId: string): Map<string, string> {
93 + const byId = new Map(messages.map((m) => [m.id, m]));
94 + const choices = new Map<string, string>();
95 + let cursor = byId.get(leafId);
96 + while (cursor) {
97 + choices.set(cursor.parent_id ?? "root", cursor.id);
98 + cursor = cursor.parent_id ? byId.get(cursor.parent_id) : undefined;
99 + }
100 + return choices;
101 +}
102 +
103 +export function formatTokens(n: number | undefined | null): string {
104 + if (n === undefined || n === null) return "—";
105 + if (n < 1000) return String(n);
106 + if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`;
107 + return `${(n / 1_000_000).toFixed(1)}M`;
108 +}
109 +
110 +export function formatUsd(v: number | undefined | null): string {
111 + if (v === undefined || v === null) return "—";
112 + if (v === 0) return "$0";
113 + if (v < 0.01) return `$${v.toFixed(4)}`;
114 + if (v < 1) return `$${v.toFixed(3)}`;
115 + return `$${v.toFixed(2)}`;
116 +}
117 +
118 +export function perMillion(perToken: number | undefined): string {
119 + if (perToken === undefined || perToken === null) return "—";
120 + const v = perToken * 1_000_000;
121 + if (v === 0) return "free";
122 + if (v < 1) return `$${v.toFixed(2)}/M`;
123 + return `$${v.toFixed(v < 10 ? 2 : 0)}/M`;
124 +}
125 +
126 +/** Provider glyph: first two letters, monochrome. Never brand colors. */
127 +export function providerGlyph(provider: string | undefined | null): string {
128 + if (!provider) return "··";
129 + return provider.slice(0, 2).toUpperCase();
130 +}
131 +
132 +export function estimateTokensClient(text: string): number {
133 + return Math.ceil(text.length / 3.6);
134 +}
added src/lib/auth/auth.ts +121 −0
@@ -0,0 +1,121 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { hash as argon2Hash, verify as argon2Verify } from "@node-rs/argon2";
6 +import crypto from "node:crypto";
7 +import { getDb } from "@/lib/db/database";
8 +
9 +export const SESSION_COOKIE = "spb_session";
10 +const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
11 +const LOGIN_WINDOW_MS = 1000 * 60 * 15;
12 +const MAX_ATTEMPTS_PER_WINDOW = 8;
13 +
14 +// argon2id with sane defaults for a single-user server
15 +const ARGON2_OPTS = { memoryCost: 65536, timeCost: 3, parallelism: 2 };
16 +
17 +export interface SessionInfo {
18 + userId: string;
19 + username: string;
20 + sessionId: string;
21 +}
22 +
23 +function sha256(input: string): string {
24 + return crypto.createHash("sha256").update(input).digest("hex");
25 +}
26 +
27 +export async function hashPassword(password: string): Promise<string> {
28 + return argon2Hash(password, ARGON2_OPTS);
29 +}
30 +
31 +export function loginRateLimited(ip: string): boolean {
32 + const db = getDb();
33 + const since = Date.now() - LOGIN_WINDOW_MS;
34 + const row = db
35 + .prepare(
36 + "SELECT COUNT(*) AS n FROM login_attempts WHERE ip = ? AND created_at > ? AND success = 0"
37 + )
38 + .get(ip, since) as { n: number };
39 + return row.n >= MAX_ATTEMPTS_PER_WINDOW;
40 +}
41 +
42 +export function recordLoginAttempt(ip: string, success: boolean): void {
43 + const db = getDb();
44 + db.prepare("INSERT INTO login_attempts (ip, success, created_at) VALUES (?, ?, ?)").run(
45 + ip,
46 + success ? 1 : 0,
47 + Date.now()
48 + );
49 + // opportunistic pruning
50 + db.prepare("DELETE FROM login_attempts WHERE created_at < ?").run(Date.now() - LOGIN_WINDOW_MS * 8);
51 +}
52 +
53 +export async function verifyCredentials(
54 + username: string,
55 + password: string
56 +): Promise<{ id: string; username: string } | null> {
57 + const db = getDb();
58 + const user = db
59 + .prepare("SELECT id, username, password_hash FROM users WHERE username = ?")
60 + .get(username) as { id: string; username: string; password_hash: string } | undefined;
61 + if (!user) {
62 + // constant-ish time: still run a hash verification against a dummy
63 + await argon2Verify(
64 + "$argon2id$v=19$m=65536,t=3,p=2$AAAAAAAAAAAAAAAAAAAAAA$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
65 + password
66 + ).catch(() => false);
67 + return null;
68 + }
69 + const ok = await argon2Verify(user.password_hash, password).catch(() => false);
70 + return ok ? { id: user.id, username: user.username } : null;
71 +}
72 +
73 +export function createSession(userId: string, userAgent?: string, ip?: string): string {
74 + const db = getDb();
75 + const rawToken = crypto.randomBytes(32).toString("base64url");
76 + const now = Date.now();
77 + db.prepare(
78 + `INSERT INTO sessions (id, user_id, created_at, expires_at, last_seen_at, user_agent, ip)
79 + VALUES (?, ?, ?, ?, ?, ?, ?)`
80 + ).run(sha256(rawToken), userId, now, now + SESSION_TTL_MS, now, userAgent ?? null, ip ?? null);
81 + db.prepare("DELETE FROM sessions WHERE expires_at < ?").run(now);
82 + return rawToken;
83 +}
84 +
85 +export function getSession(rawToken: string | undefined): SessionInfo | null {
86 + if (!rawToken) return null;
87 + const db = getDb();
88 + const now = Date.now();
89 + const row = db
90 + .prepare(
91 + `SELECT s.id AS session_id, u.id AS user_id, u.username
92 + FROM sessions s JOIN users u ON u.id = s.user_id
93 + WHERE s.id = ? AND s.expires_at > ?`
94 + )
95 + .get(sha256(rawToken), now) as
96 + | { session_id: string; user_id: string; username: string }
97 + | undefined;
98 + if (!row) return null;
99 + db.prepare("UPDATE sessions SET last_seen_at = ?, expires_at = ? WHERE id = ?").run(
100 + now,
101 + now + SESSION_TTL_MS,
102 + row.session_id
103 + );
104 + return { userId: row.user_id, username: row.username, sessionId: row.session_id };
105 +}
106 +
107 +export function destroySession(rawToken: string | undefined): void {
108 + if (!rawToken) return;
109 + getDb().prepare("DELETE FROM sessions WHERE id = ?").run(sha256(rawToken));
110 +}
111 +
112 +export function sessionCookieHeader(rawToken: string, maxAgeSeconds: number): string {
113 + return [
114 + `${SESSION_COOKIE}=${rawToken}`,
115 + "Path=/",
116 + "HttpOnly",
117 + "Secure",
118 + "SameSite=Lax",
119 + `Max-Age=${maxAgeSeconds}`,
120 + ].join("; ");
121 +}
added src/lib/auth/guard.ts +35 −0
@@ -0,0 +1,35 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { cookies, headers } from "next/headers";
6 +import { NextResponse } from "next/server";
7 +import { getSession, SESSION_COOKIE, type SessionInfo } from "./auth";
8 +
9 +/** DB-backed session check for route handlers and server components. */
10 +export async function currentSession(): Promise<SessionInfo | null> {
11 + const jar = await cookies();
12 + return getSession(jar.get(SESSION_COOKIE)?.value);
13 +}
14 +
15 +/** Returns the session or a ready-to-return 401 response. Every /api route uses this. */
16 +export async function requireSession(): Promise<
17 + { session: SessionInfo; unauthorized: null } | { session: null; unauthorized: NextResponse }
18 +> {
19 + const session = await currentSession();
20 + if (!session) {
21 + return {
22 + session: null,
23 + unauthorized: NextResponse.json({ error: "Unauthorized" }, { status: 401 }),
24 + };
25 + }
26 + return { session, unauthorized: null };
27 +}
28 +
29 +/** Client IP, trusting X-Forwarded-For from the ngrok tunnel. */
30 +export async function clientIp(): Promise<string> {
31 + const h = await headers();
32 + const xff = h.get("x-forwarded-for");
33 + if (xff) return xff.split(",")[0].trim();
34 + return h.get("x-real-ip") ?? "unknown";
35 +}
added src/lib/catalog.ts +212 −0
@@ -0,0 +1,212 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Model catalog: OpenRouter → sync job → SQLite cache → application.
6 +// The catalog is never hand-maintained; removed models are marked unavailable, never deleted.
7 +
8 +import { getDb } from "@/lib/db/database";
9 +import { fetchModels } from "@/lib/openrouter";
10 +import type { ModelDefinition } from "@/lib/openrouter";
11 +
12 +const SYNC_INTERVAL_MS = 1000 * 60 * 60 * 4; // refresh every 4 hours
13 +
14 +export interface CatalogModel extends ModelDefinition {
15 + available: boolean;
16 + favorite: boolean;
17 + pinned: boolean;
18 + lastUsedAt: number | null;
19 + useCount: number;
20 +}
21 +
22 +interface ModelRow {
23 + openrouter_model_id: string;
24 + name: string;
25 + provider: string | null;
26 + description: string | null;
27 + context_length: number | null;
28 + pricing_prompt: number | null;
29 + pricing_completion: number | null;
30 + pricing_image: number | null;
31 + pricing_request: number | null;
32 + cap_text: number;
33 + cap_vision: number;
34 + cap_reasoning: number;
35 + cap_tools: number;
36 + cap_structured: number;
37 + architecture: string | null;
38 + tokenizer: string | null;
39 + available: number;
40 + or_created_at: number | null;
41 + favorite: number | null;
42 + pinned: number | null;
43 + last_used_at: number | null;
44 + use_count: number | null;
45 +}
46 +
47 +function rowToModel(r: ModelRow): CatalogModel {
48 + return {
49 + id: r.openrouter_model_id,
50 + name: r.name,
51 + provider: r.provider ?? undefined,
52 + description: r.description ?? undefined,
53 + contextLength: r.context_length ?? undefined,
54 + pricing: {
55 + prompt: r.pricing_prompt ?? undefined,
56 + completion: r.pricing_completion ?? undefined,
57 + image: r.pricing_image ?? undefined,
58 + request: r.pricing_request ?? undefined,
59 + },
60 + capabilities: {
61 + text: Boolean(r.cap_text),
62 + vision: Boolean(r.cap_vision),
63 + reasoning: Boolean(r.cap_reasoning),
64 + tools: Boolean(r.cap_tools),
65 + structuredOutput: Boolean(r.cap_structured),
66 + },
67 + metadata: {
68 + architecture: r.architecture ?? undefined,
69 + tokenizer: r.tokenizer ?? undefined,
70 + createdAt: r.or_created_at ?? undefined,
71 + },
72 + available: Boolean(r.available),
73 + favorite: Boolean(r.favorite),
74 + pinned: Boolean(r.pinned),
75 + lastUsedAt: r.last_used_at,
76 + useCount: r.use_count ?? 0,
77 + };
78 +}
79 +
80 +export async function syncCatalog(): Promise<{ total: number; added: number; removed: number }> {
81 + const models = await fetchModels();
82 + const db = getDb();
83 + const now = Date.now();
84 +
85 + const upsert = db.prepare(`
86 + INSERT INTO models (
87 + openrouter_model_id, name, provider, description, context_length,
88 + pricing_prompt, pricing_completion, pricing_image, pricing_request,
89 + cap_text, cap_vision, cap_reasoning, cap_tools, cap_structured,
90 + architecture, tokenizer, raw_json, available, or_created_at, first_seen_at, updated_at
91 + ) VALUES (
92 + @id, @name, @provider, @description, @contextLength,
93 + @pPrompt, @pCompletion, @pImage, @pRequest,
94 + @cText, @cVision, @cReasoning, @cTools, @cStructured,
95 + @architecture, @tokenizer, @raw, 1, @orCreatedAt, @now, @now
96 + )
97 + ON CONFLICT(openrouter_model_id) DO UPDATE SET
98 + name=@name, provider=@provider, description=@description, context_length=@contextLength,
99 + pricing_prompt=@pPrompt, pricing_completion=@pCompletion, pricing_image=@pImage, pricing_request=@pRequest,
100 + cap_text=@cText, cap_vision=@cVision, cap_reasoning=@cReasoning, cap_tools=@cTools, cap_structured=@cStructured,
101 + architecture=@architecture, tokenizer=@tokenizer, raw_json=@raw, available=1, or_created_at=@orCreatedAt, updated_at=@now
102 + `);
103 +
104 + const existing = new Set(
105 + (db.prepare("SELECT openrouter_model_id FROM models").all() as { openrouter_model_id: string }[]).map(
106 + (r) => r.openrouter_model_id
107 + )
108 + );
109 + const seen = new Set<string>();
110 + let added = 0;
111 +
112 + const tx = db.transaction(() => {
113 + for (const m of models) {
114 + seen.add(m.id);
115 + if (!existing.has(m.id)) added++;
116 + upsert.run({
117 + id: m.id,
118 + name: m.name,
119 + provider: m.provider ?? null,
120 + description: m.description ?? null,
121 + contextLength: m.contextLength ?? null,
122 + pPrompt: m.pricing?.prompt ?? null,
123 + pCompletion: m.pricing?.completion ?? null,
124 + pImage: m.pricing?.image ?? null,
125 + pRequest: m.pricing?.request ?? null,
126 + cText: m.capabilities.text ? 1 : 0,
127 + cVision: m.capabilities.vision ? 1 : 0,
128 + cReasoning: m.capabilities.reasoning ? 1 : 0,
129 + cTools: m.capabilities.tools ? 1 : 0,
130 + cStructured: m.capabilities.structuredOutput ? 1 : 0,
131 + architecture: m.metadata.architecture ?? null,
132 + tokenizer: m.metadata.tokenizer ?? null,
133 + raw: JSON.stringify(m.metadata.raw ?? null),
134 + orCreatedAt: m.metadata.createdAt ?? null,
135 + now,
136 + });
137 + }
138 + // Models gone from the catalog stay in the DB (history integrity) but are marked unavailable.
139 + const markUnavailable = db.prepare(
140 + "UPDATE models SET available = 0, updated_at = ? WHERE openrouter_model_id = ?"
141 + );
142 + for (const id of existing) {
143 + if (!seen.has(id)) markUnavailable.run(now, id);
144 + }
145 + db.prepare("INSERT INTO settings (key, value) VALUES ('catalog_synced_at', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(now));
146 + });
147 + tx();
148 +
149 + return { total: models.length, added, removed: existing.size - [...existing].filter((id) => seen.has(id)).length };
150 +}
151 +
152 +export function catalogSyncedAt(): number | null {
153 + const row = getDb().prepare("SELECT value FROM settings WHERE key = 'catalog_synced_at'").get() as
154 + | { value: string }
155 + | undefined;
156 + return row ? Number(row.value) : null;
157 +}
158 +
159 +/** Sync if the cache is empty or stale. Safe to call on any request path. */
160 +export async function ensureCatalogFresh(): Promise<void> {
161 + const syncedAt = catalogSyncedAt();
162 + if (syncedAt && Date.now() - syncedAt < SYNC_INTERVAL_MS) return;
163 + try {
164 + await syncCatalog();
165 + } catch (e) {
166 + if (!syncedAt) throw e; // no cache at all — surface the failure
167 + // stale cache is still usable; sync will retry on a later request
168 + console.error("catalog refresh failed:", e);
169 + }
170 +}
171 +
172 +export function listCatalog(): CatalogModel[] {
173 + const rows = getDb()
174 + .prepare(
175 + `SELECT m.*, p.favorite, p.pinned, p.last_used_at, p.use_count
176 + FROM models m LEFT JOIN model_prefs p ON p.model_id = m.openrouter_model_id
177 + ORDER BY m.name COLLATE NOCASE`
178 + )
179 + .all() as ModelRow[];
180 + return rows.map(rowToModel);
181 +}
182 +
183 +export function getModel(id: string): CatalogModel | null {
184 + const row = getDb()
185 + .prepare(
186 + `SELECT m.*, p.favorite, p.pinned, p.last_used_at, p.use_count
187 + FROM models m LEFT JOIN model_prefs p ON p.model_id = m.openrouter_model_id
188 + WHERE m.openrouter_model_id = ?`
189 + )
190 + .get(id) as ModelRow | undefined;
191 + return row ? rowToModel(row) : null;
192 +}
193 +
194 +export function touchModelUsage(id: string): void {
195 + getDb()
196 + .prepare(
197 + `INSERT INTO model_prefs (model_id, last_used_at, use_count) VALUES (?, ?, 1)
198 + ON CONFLICT(model_id) DO UPDATE SET last_used_at = excluded.last_used_at, use_count = use_count + 1`
199 + )
200 + .run(id, Date.now());
201 +}
202 +
203 +export function setModelPref(id: string, pref: { favorite?: boolean; pinned?: boolean }): void {
204 + const db = getDb();
205 + db.prepare("INSERT OR IGNORE INTO model_prefs (model_id) VALUES (?)").run(id);
206 + if (pref.favorite !== undefined) {
207 + db.prepare("UPDATE model_prefs SET favorite = ? WHERE model_id = ?").run(pref.favorite ? 1 : 0, id);
208 + }
209 + if (pref.pinned !== undefined) {
210 + db.prepare("UPDATE model_prefs SET pinned = ? WHERE model_id = ?").run(pref.pinned ? 1 : 0, id);
211 + }
212 +}
added src/lib/context.ts +64 −0
@@ -0,0 +1,64 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Context compiler: DB history and model context are separate concepts.
6 +// Compiles the active thread into a request that fits the model's window.
7 +
8 +import type { ChatMessageInput } from "@/lib/openrouter";
9 +import type { MessageRow } from "@/lib/conversations";
10 +
11 +/** Cheap token estimate: ~4 characters per token. Deliberately conservative. */
12 +export function estimateTokens(text: string): number {
13 + return Math.ceil(text.length / 3.6);
14 +}
15 +
16 +export interface CompiledContext {
17 + messages: ChatMessageInput[];
18 + estimatedPromptTokens: number;
19 + trimmedCount: number;
20 +}
21 +
22 +const RESERVED_OUTPUT_TOKENS = 4096;
23 +
24 +/**
25 + * Compile the thread for a request: keep system instructions, keep the most
26 + * recent messages, trim oldest low-value context when over budget.
27 + */
28 +export function compileContext(
29 + thread: MessageRow[],
30 + contextLength: number | undefined,
31 + systemPrompt?: string
32 +): CompiledContext {
33 + const budget = Math.max((contextLength ?? 128_000) - RESERVED_OUTPUT_TOKENS, 8_000);
34 +
35 + const system: ChatMessageInput[] = systemPrompt
36 + ? [{ role: "system", content: systemPrompt }]
37 + : [];
38 + const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
39 +
40 + // Only completed / meaningful messages reach the model.
41 + const usable = thread.filter(
42 + (m) => (m.role === "user" || m.role === "assistant") && m.content.length > 0
43 + );
44 +
45 + // Walk from newest to oldest, accumulating until the budget is spent.
46 + const kept: MessageRow[] = [];
47 + let used = systemTokens;
48 + for (let i = usable.length - 1; i >= 0; i--) {
49 + const t = estimateTokens(usable[i].content) + 6; // per-message overhead
50 + if (used + t > budget && kept.length > 0) break;
51 + kept.push(usable[i]);
52 + used += t;
53 + }
54 + kept.reverse();
55 +
56 + return {
57 + messages: [
58 + ...system,
59 + ...kept.map((m): ChatMessageInput => ({ role: m.role as "user" | "assistant", content: m.content })),
60 + ],
61 + estimatedPromptTokens: used,
62 + trimmedCount: usable.length - kept.length,
63 + };
64 +}
added src/lib/conversations.ts +134 −0
@@ -0,0 +1,134 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import crypto from "node:crypto";
6 +import { getDb } from "@/lib/db/database";
7 +
8 +export interface ConversationRow {
9 + id: string;
10 + title: string;
11 + pinned: number;
12 + current_leaf_id: string | null;
13 + created_at: number;
14 + updated_at: number;
15 +}
16 +
17 +export interface MessageRow {
18 + id: string;
19 + conversation_id: string;
20 + parent_id: string | null;
21 + role: "user" | "assistant" | "system";
22 + content: string;
23 + reasoning: string | null;
24 + model_id: string | null;
25 + model_name: string | null;
26 + provider: string | null;
27 + generation_id: string | null;
28 + status: "pending" | "streaming" | "completed" | "cancelled" | "failed";
29 + error_message: string | null;
30 + created_at: number;
31 +}
32 +
33 +export function listConversations(): ConversationRow[] {
34 + return getDb()
35 + .prepare("SELECT * FROM conversations ORDER BY pinned DESC, updated_at DESC")
36 + .all() as ConversationRow[];
37 +}
38 +
39 +export function getConversation(id: string): ConversationRow | null {
40 + return (getDb().prepare("SELECT * FROM conversations WHERE id = ?").get(id) as ConversationRow) ?? null;
41 +}
42 +
43 +export function createConversation(title?: string): ConversationRow {
44 + const db = getDb();
45 + const id = crypto.randomUUID();
46 + const now = Date.now();
47 + db.prepare(
48 + "INSERT INTO conversations (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)"
49 + ).run(id, title?.slice(0, 80) || "New conversation", now, now);
50 + return getConversation(id)!;
51 +}
52 +
53 +export function updateConversation(
54 + id: string,
55 + patch: { title?: string; pinned?: boolean; currentLeafId?: string | null }
56 +): void {
57 + const db = getDb();
58 + if (patch.title !== undefined) {
59 + db.prepare("UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?").run(
60 + patch.title.slice(0, 120),
61 + Date.now(),
62 + id
63 + );
64 + }
65 + if (patch.pinned !== undefined) {
66 + db.prepare("UPDATE conversations SET pinned = ? WHERE id = ?").run(patch.pinned ? 1 : 0, id);
67 + }
68 + if (patch.currentLeafId !== undefined) {
69 + db.prepare("UPDATE conversations SET current_leaf_id = ? WHERE id = ?").run(patch.currentLeafId, id);
70 + }
71 +}
72 +
73 +export function deleteConversation(id: string): void {
74 + getDb().prepare("DELETE FROM conversations WHERE id = ?").run(id);
75 +}
76 +
77 +export function touchConversation(id: string): void {
78 + getDb().prepare("UPDATE conversations SET updated_at = ? WHERE id = ?").run(Date.now(), id);
79 +}
80 +
81 +export function listMessages(conversationId: string): MessageRow[] {
82 + return getDb()
83 + .prepare("SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at, id")
84 + .all(conversationId) as MessageRow[];
85 +}
86 +
87 +export function getMessage(id: string): MessageRow | null {
88 + return (getDb().prepare("SELECT * FROM messages WHERE id = ?").get(id) as MessageRow) ?? null;
89 +}
90 +
91 +export function insertMessage(m: {
92 + conversationId: string;
93 + parentId: string | null;
94 + role: "user" | "assistant" | "system";
95 + content?: string;
96 + modelId?: string;
97 + modelName?: string;
98 + provider?: string;
99 + generationId?: string;
100 + status?: MessageRow["status"];
101 +}): MessageRow {
102 + const db = getDb();
103 + const id = crypto.randomUUID();
104 + db.prepare(
105 + `INSERT INTO messages (id, conversation_id, parent_id, role, content, model_id, model_name, provider, generation_id, status, created_at)
106 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
107 + ).run(
108 + id,
109 + m.conversationId,
110 + m.parentId,
111 + m.role,
112 + m.content ?? "",
113 + m.modelId ?? null,
114 + m.modelName ?? null,
115 + m.provider ?? null,
116 + m.generationId ?? null,
117 + m.status ?? "completed",
118 + Date.now()
119 + );
120 + return getMessage(id)!;
121 +}
122 +
123 +/** Walk parent pointers from a leaf to the root: the active thread, oldest first. */
124 +export function threadToLeaf(conversationId: string, leafId: string | null): MessageRow[] {
125 + if (!leafId) return [];
126 + const byId = new Map(listMessages(conversationId).map((m) => [m.id, m]));
127 + const thread: MessageRow[] = [];
128 + let cursor = byId.get(leafId);
129 + while (cursor) {
130 + thread.push(cursor);
131 + cursor = cursor.parent_id ? byId.get(cursor.parent_id) : undefined;
132 + }
133 + return thread.reverse();
134 +}
added src/lib/db/database.ts +28 −0
@@ -0,0 +1,28 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import Database from "better-sqlite3";
6 +import fs from "node:fs";
7 +import path from "node:path";
8 +import { migrate } from "./schema";
9 +
10 +const DATA_DIR = path.resolve(/* turbopackIgnore: true */ process.env.CHAT_DATA_DIR || "./data");
11 +
12 +let db: Database.Database | null = null;
13 +
14 +export function dataDir(): string {
15 + return DATA_DIR;
16 +}
17 +
18 +export function getDb(): Database.Database {
19 + if (db) return db;
20 + fs.mkdirSync(DATA_DIR, { recursive: true });
21 + fs.mkdirSync(path.join(DATA_DIR, "uploads"), { recursive: true });
22 + db = new Database(path.join(DATA_DIR, "chat.db"));
23 + db.pragma("journal_mode = WAL");
24 + db.pragma("foreign_keys = ON");
25 + db.pragma("busy_timeout = 5000");
26 + migrate(db);
27 + return db;
28 +}
added src/lib/db/schema.ts +169 −0
@@ -0,0 +1,169 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import type { Database } from "better-sqlite3";
6 +
7 +interface Migration {
8 + version: number;
9 + up: string;
10 +}
11 +
12 +const migrations: Migration[] = [
13 + {
14 + version: 1,
15 + up: `
16 + CREATE TABLE IF NOT EXISTS users (
17 + id TEXT PRIMARY KEY,
18 + username TEXT NOT NULL UNIQUE,
19 + password_hash TEXT NOT NULL,
20 + totp_secret TEXT,
21 + created_at INTEGER NOT NULL,
22 + updated_at INTEGER NOT NULL
23 + );
24 +
25 + CREATE TABLE IF NOT EXISTS sessions (
26 + id TEXT PRIMARY KEY, -- sha256 of the raw token; raw token lives only in the cookie
27 + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
28 + created_at INTEGER NOT NULL,
29 + expires_at INTEGER NOT NULL,
30 + last_seen_at INTEGER NOT NULL,
31 + user_agent TEXT,
32 + ip TEXT
33 + );
34 + CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
35 + CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
36 +
37 + CREATE TABLE IF NOT EXISTS login_attempts (
38 + id INTEGER PRIMARY KEY AUTOINCREMENT,
39 + ip TEXT NOT NULL,
40 + success INTEGER NOT NULL DEFAULT 0,
41 + created_at INTEGER NOT NULL
42 + );
43 + CREATE INDEX IF NOT EXISTS idx_login_attempts_ip ON login_attempts(ip, created_at);
44 +
45 + CREATE TABLE IF NOT EXISTS conversations (
46 + id TEXT PRIMARY KEY,
47 + title TEXT NOT NULL DEFAULT 'New conversation',
48 + pinned INTEGER NOT NULL DEFAULT 0,
49 + current_leaf_id TEXT,
50 + created_at INTEGER NOT NULL,
51 + updated_at INTEGER NOT NULL
52 + );
53 + CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at DESC);
54 +
55 + -- Messages form a tree: branching = multiple children of the same parent.
56 + CREATE TABLE IF NOT EXISTS messages (
57 + id TEXT PRIMARY KEY,
58 + conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
59 + parent_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
60 + role TEXT NOT NULL CHECK (role IN ('user','assistant','system')),
61 + content TEXT NOT NULL DEFAULT '',
62 + reasoning TEXT,
63 + -- model attribution, stored denormalized so history survives catalog changes
64 + model_id TEXT,
65 + model_name TEXT,
66 + provider TEXT,
67 + generation_id TEXT,
68 + status TEXT NOT NULL DEFAULT 'completed' CHECK (status IN ('pending','streaming','completed','cancelled','failed')),
69 + error_message TEXT,
70 + created_at INTEGER NOT NULL
71 + );
72 + CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id, created_at);
73 + CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id);
74 +
75 + -- Explicit generation state machine: queued → starting → streaming → completed | cancelled | failed
76 + CREATE TABLE IF NOT EXISTS generations (
77 + id TEXT PRIMARY KEY,
78 + message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
79 + conversation_id TEXT NOT NULL,
80 + model_id TEXT NOT NULL,
81 + state TEXT NOT NULL DEFAULT 'queued' CHECK (state IN ('queued','starting','streaming','completed','cancelled','failed')),
82 + error_code TEXT,
83 + error_message TEXT,
84 + openrouter_generation_id TEXT,
85 + created_at INTEGER NOT NULL,
86 + finished_at INTEGER
87 + );
88 + CREATE INDEX IF NOT EXISTS idx_generations_message ON generations(message_id);
89 +
90 + CREATE TABLE IF NOT EXISTS generation_usage (
91 + id INTEGER PRIMARY KEY AUTOINCREMENT,
92 + generation_id TEXT NOT NULL REFERENCES generations(id) ON DELETE CASCADE,
93 + model_id TEXT NOT NULL,
94 + prompt_tokens INTEGER,
95 + completion_tokens INTEGER,
96 + reasoning_tokens INTEGER,
97 + cached_tokens INTEGER,
98 + total_tokens INTEGER,
99 + estimated_cost_usd REAL,
100 + reported_cost_usd REAL,
101 + created_at INTEGER NOT NULL
102 + );
103 + CREATE INDEX IF NOT EXISTS idx_usage_created ON generation_usage(created_at);
104 + CREATE INDEX IF NOT EXISTS idx_usage_model ON generation_usage(model_id);
105 +
106 + -- Dynamic model catalog cache, synced from OpenRouter. Never hand-maintained.
107 + CREATE TABLE IF NOT EXISTS models (
108 + openrouter_model_id TEXT PRIMARY KEY, -- opaque, sent back verbatim
109 + name TEXT NOT NULL,
110 + provider TEXT,
111 + description TEXT,
112 + context_length INTEGER,
113 + pricing_prompt REAL,
114 + pricing_completion REAL,
115 + pricing_image REAL,
116 + pricing_request REAL,
117 + cap_text INTEGER NOT NULL DEFAULT 1,
118 + cap_vision INTEGER NOT NULL DEFAULT 0,
119 + cap_reasoning INTEGER NOT NULL DEFAULT 0,
120 + cap_tools INTEGER NOT NULL DEFAULT 0,
121 + cap_structured INTEGER NOT NULL DEFAULT 0,
122 + architecture TEXT,
123 + tokenizer TEXT,
124 + raw_json TEXT,
125 + available INTEGER NOT NULL DEFAULT 1,
126 + or_created_at INTEGER,
127 + first_seen_at INTEGER NOT NULL,
128 + updated_at INTEGER NOT NULL
129 + );
130 + CREATE INDEX IF NOT EXISTS idx_models_available ON models(available);
131 +
132 + CREATE TABLE IF NOT EXISTS model_prefs (
133 + model_id TEXT PRIMARY KEY,
134 + favorite INTEGER NOT NULL DEFAULT 0,
135 + pinned INTEGER NOT NULL DEFAULT 0,
136 + last_used_at INTEGER,
137 + use_count INTEGER NOT NULL DEFAULT 0
138 + );
139 +
140 + CREATE TABLE IF NOT EXISTS settings (
141 + key TEXT PRIMARY KEY,
142 + value TEXT NOT NULL
143 + );
144 + `,
145 + },
146 +];
147 +
148 +export function migrate(db: Database): void {
149 + db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
150 + version INTEGER PRIMARY KEY,
151 + applied_at INTEGER NOT NULL
152 + )`);
153 + const applied = new Set(
154 + (db.prepare("SELECT version FROM schema_migrations").all() as { version: number }[]).map(
155 + (r) => r.version
156 + )
157 + );
158 + for (const m of migrations) {
159 + if (applied.has(m.version)) continue;
160 + const tx = db.transaction(() => {
161 + db.exec(m.up);
162 + db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(
163 + m.version,
164 + Date.now()
165 + );
166 + });
167 + tx();
168 + }
169 +}
added src/lib/generate.ts +263 −0
@@ -0,0 +1,263 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Generation runner: explicit state machine per generation
6 +// (queued → starting → streaming → completed | cancelled | failed),
7 +// SSE emission with keep-alives, incremental persistence, cancellation registry.
8 +
9 +import crypto from "node:crypto";
10 +import { getDb } from "@/lib/db/database";
11 +import { streamGeneration, estimateCost } from "@/lib/openrouter";
12 +import type { ChatStreamEvent } from "@/lib/openrouter";
13 +import { compileContext } from "@/lib/context";
14 +import {
15 + getConversation,
16 + insertMessage,
17 + threadToLeaf,
18 + touchConversation,
19 + updateConversation,
20 + type MessageRow,
21 +} from "@/lib/conversations";
22 +import { getModel, touchModelUsage, type CatalogModel } from "@/lib/catalog";
23 +
24 +const KEEPALIVE_MS = 15_000;
25 +const FLUSH_MS = 700;
26 +
27 +// Registry of live generations for cancellation; on globalThis to survive dev HMR.
28 +const registry: Map<string, AbortController> = ((globalThis as Record<string, unknown>).__genRegistry ??=
29 + new Map()) as Map<string, AbortController>;
30 +
31 +export function cancelGeneration(generationId: string): boolean {
32 + const ctrl = registry.get(generationId);
33 + if (!ctrl) return false;
34 + ctrl.abort();
35 + return true;
36 +}
37 +
38 +export function generationState(generationId: string) {
39 + return getDb()
40 + .prepare("SELECT id, message_id, state, error_code, error_message, finished_at FROM generations WHERE id = ?")
41 + .get(generationId) as
42 + | { id: string; message_id: string; state: string; error_code: string | null; error_message: string | null; finished_at: number | null }
43 + | undefined;
44 +}
45 +
46 +function setGenState(id: string, state: string, errorCode?: string, errorMessage?: string): void {
47 + const finished = ["completed", "cancelled", "failed"].includes(state) ? Date.now() : null;
48 + getDb()
49 + .prepare(
50 + "UPDATE generations SET state = ?, error_code = COALESCE(?, error_code), error_message = COALESCE(?, error_message), finished_at = COALESCE(?, finished_at) WHERE id = ?"
51 + )
52 + .run(state, errorCode ?? null, errorMessage ?? null, finished, id);
53 +}
54 +
55 +export interface StartGenerationArgs {
56 + conversationId: string;
57 + parentMessage: MessageRow | null; // the user message the assistant answers
58 + model: CatalogModel;
59 +}
60 +
61 +export interface StartedGeneration {
62 + generationId: string;
63 + assistantMessage: MessageRow;
64 + stream: ReadableStream<Uint8Array>;
65 +}
66 +
67 +function sseFrame(event: object): string {
68 + return `data: ${JSON.stringify(event)}\n\n`;
69 +}
70 +
71 +/**
72 + * Create the assistant placeholder + generation row, then return an SSE stream
73 + * that runs the generation, persists deltas incrementally, and finalizes state.
74 + */
75 +export function startGeneration(args: StartGenerationArgs): StartedGeneration {
76 + const db = getDb();
77 + const generationId = crypto.randomUUID();
78 + const { conversationId, parentMessage, model } = args;
79 +
80 + const assistantMessage = insertMessage({
81 + conversationId,
82 + parentId: parentMessage?.id ?? null,
83 + role: "assistant",
84 + modelId: model.id,
85 + modelName: model.name,
86 + provider: model.provider,
87 + generationId,
88 + status: "pending",
89 + });
90 +
91 + db.prepare(
92 + "INSERT INTO generations (id, message_id, conversation_id, model_id, state, created_at) VALUES (?, ?, ?, ?, 'queued', ?)"
93 + ).run(generationId, assistantMessage.id, conversationId, model.id, Date.now());
94 +
95 + updateConversation(conversationId, { currentLeafId: assistantMessage.id });
96 + touchConversation(conversationId);
97 + touchModelUsage(model.id);
98 +
99 + const ctrl = new AbortController();
100 + registry.set(generationId, ctrl);
101 +
102 + const thread = threadToLeaf(conversationId, parentMessage?.id ?? null);
103 + const compiled = compileContext(thread, model.contextLength);
104 +
105 + const encoder = new TextEncoder();
106 +
107 + const stream = new ReadableStream<Uint8Array>({
108 + async start(controller) {
109 + const send = (e: object) => {
110 + try {
111 + controller.enqueue(encoder.encode(sseFrame(e)));
112 + } catch {
113 + /* client went away; generation continues until aborted or done */
114 + }
115 + };
116 +
117 + // keep-alive comments so ngrok and mobile radios don't kill idle streams
118 + const keepAlive = setInterval(() => {
119 + try {
120 + controller.enqueue(encoder.encode(`: ping\n\n`));
121 + } catch {
122 + /* ignore */
123 + }
124 + }, KEEPALIVE_MS);
125 +
126 + let content = "";
127 + let reasoning = "";
128 + let dirty = false;
129 + const flush = () => {
130 + if (!dirty) return;
131 + db.prepare("UPDATE messages SET content = ?, reasoning = ?, status = 'streaming' WHERE id = ?").run(
132 + content,
133 + reasoning || null,
134 + assistantMessage.id
135 + );
136 + dirty = false;
137 + };
138 + const flusher = setInterval(flush, FLUSH_MS);
139 +
140 + send({
141 + type: "meta",
142 + conversationId,
143 + assistantMessageId: assistantMessage.id,
144 + generationId,
145 + model: model.id,
146 + estimatedPromptTokens: compiled.estimatedPromptTokens,
147 + });
148 +
149 + setGenState(generationId, "starting");
150 +
151 + let finalState: "completed" | "cancelled" | "failed" = "completed";
152 + let errorMessage: string | null = null;
153 + let sawUsage = false;
154 +
155 + try {
156 + const events = streamGeneration(
157 + { model: model.id, messages: compiled.messages, signal: ctrl.signal },
158 + generationId
159 + );
160 + for await (const event of events) {
161 + switch (event.type) {
162 + case "generation.start":
163 + setGenState(generationId, "streaming");
164 + break;
165 + case "content.delta":
166 + content += event.text;
167 + dirty = true;
168 + break;
169 + case "reasoning.delta":
170 + reasoning += event.text;
171 + dirty = true;
172 + break;
173 + case "usage":
174 + sawUsage = true;
175 + recordUsage(generationId, model, event);
176 + break;
177 + case "generation.error":
178 + finalState = "failed";
179 + errorMessage = event.message;
180 + break;
181 + case "generation.end":
182 + case "tool.start":
183 + case "tool.delta":
184 + break;
185 + }
186 + send(event);
187 + }
188 + if (ctrl.signal.aborted) finalState = "cancelled";
189 + } catch {
190 + finalState = ctrl.signal.aborted ? "cancelled" : "failed";
191 + if (finalState === "failed") errorMessage = "The stream failed unexpectedly.";
192 + send({ type: "generation.error", message: errorMessage ?? "Cancelled", retryable: finalState === "failed" });
193 + } finally {
194 + clearInterval(keepAlive);
195 + clearInterval(flusher);
196 + registry.delete(generationId);
197 +
198 + flush();
199 + db.prepare(
200 + "UPDATE messages SET content = ?, reasoning = ?, status = ?, error_message = ? WHERE id = ?"
201 + ).run(content, reasoning || null, finalState, errorMessage, assistantMessage.id);
202 + setGenState(generationId, finalState, undefined, errorMessage ?? undefined);
203 + touchConversation(conversationId);
204 +
205 + // If the provider never reported usage, store an estimate for cost tracking.
206 + if (!sawUsage && finalState === "completed") {
207 + recordUsage(generationId, model, {
208 + type: "usage",
209 + promptTokens: compiled.estimatedPromptTokens,
210 + completionTokens: Math.ceil(content.length / 3.6),
211 + totalTokens: compiled.estimatedPromptTokens + Math.ceil(content.length / 3.6),
212 + });
213 + }
214 +
215 + send({ type: "state.final", state: finalState, messageId: assistantMessage.id });
216 + try {
217 + controller.close();
218 + } catch {
219 + /* already closed */
220 + }
221 + }
222 + },
223 + cancel() {
224 + // Client disconnected (tab closed, network drop). Abort upstream — never keep paying.
225 + ctrl.abort();
226 + },
227 + });
228 +
229 + return { generationId, assistantMessage, stream };
230 +}
231 +
232 +function recordUsage(
233 + generationId: string,
234 + model: CatalogModel,
235 + usage: Extract<ChatStreamEvent, { type: "usage" }>
236 +): void {
237 + const estimated = estimateCost(model.pricing, usage.promptTokens, usage.completionTokens);
238 + getDb()
239 + .prepare(
240 + `INSERT INTO generation_usage
241 + (generation_id, model_id, prompt_tokens, completion_tokens, reasoning_tokens, cached_tokens, total_tokens, estimated_cost_usd, reported_cost_usd, created_at)
242 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
243 + )
244 + .run(
245 + generationId,
246 + model.id,
247 + usage.promptTokens ?? null,
248 + usage.completionTokens ?? null,
249 + usage.reasoningTokens ?? null,
250 + usage.cachedTokens ?? null,
251 + usage.totalTokens ?? null,
252 + estimated ?? null,
253 + usage.cost ?? null,
254 + Date.now()
255 + );
256 +}
257 +
258 +/** Guard used by the chat route: conversation must exist. */
259 +export function assertConversation(id: string) {
260 + const conv = getConversation(id);
261 + if (!conv) throw new Error("Conversation not found");
262 + return conv;
263 +}
added src/lib/openrouter/capabilities.ts +20 −0
@@ -0,0 +1,20 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import type { ORModelRaw } from "./schemas";
6 +import type { ModelDefinition } from "./types";
7 +
8 +/** Derive normalized capabilities from OpenRouter metadata — never from string-parsing model IDs. */
9 +export function deriveCapabilities(raw: ORModelRaw): ModelDefinition["capabilities"] {
10 + const inputs = raw.architecture?.input_modalities ?? [];
11 + const outputs = raw.architecture?.output_modalities ?? [];
12 + const params = raw.supported_parameters ?? [];
13 + return {
14 + text: outputs.length === 0 || outputs.includes("text"),
15 + vision: inputs.includes("image"),
16 + reasoning: params.includes("reasoning") || params.includes("include_reasoning"),
17 + tools: params.includes("tools") || params.includes("tool_choice"),
18 + structuredOutput: params.includes("structured_outputs") || params.includes("response_format"),
19 + };
20 +}
added src/lib/openrouter/client.ts +100 −0
@@ -0,0 +1,100 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// The ONLY place that talks to the OpenRouter HTTP API. No scattered fetch() calls.
6 +
7 +import { normalizeHttpError, normalizeNetworkError, AIError } from "./errors";
8 +
9 +const BASE_URL = "https://openrouter.ai/api/v1";
10 +const MAX_RETRIES = 2;
11 +const RETRY_BASE_DELAY_MS = 750;
12 +
13 +function apiKey(): string {
14 + const key = process.env.OPENROUTER_API_KEY;
15 + if (!key) {
16 + throw new AIError({
17 + code: "AUTHENTICATION_ERROR",
18 + message: "Server is missing its OpenRouter credentials.",
19 + retryable: false,
20 + });
21 + }
22 + return key;
23 +}
24 +
25 +function baseHeaders(): Record<string, string> {
26 + return {
27 + Authorization: `Bearer ${apiKey()}`,
28 + "Content-Type": "application/json",
29 + "HTTP-Referer": process.env.APP_ORIGIN ?? "https://chat.spboucher.ai",
30 + "X-Title": "chat.spboucher.ai",
31 + };
32 +}
33 +
34 +async function readErrorMessage(res: Response): Promise<string> {
35 + try {
36 + const json = await res.json();
37 + return json?.error?.message ?? json?.message ?? "";
38 + } catch {
39 + return "";
40 + }
41 +}
42 +
43 +/**
44 + * GET with conservative bounded retries for transient failures only.
45 + * Auth failures and client errors are never retried.
46 + */
47 +export async function orGet(path: string, signal?: AbortSignal): Promise<unknown> {
48 + let lastError: AIError | null = null;
49 + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
50 + try {
51 + const res = await fetch(`${BASE_URL}${path}`, { headers: baseHeaders(), signal });
52 + if (res.ok) return await res.json();
53 + const err = normalizeHttpError(res.status, await readErrorMessage(res));
54 + if (!err.retryable) throw err;
55 + lastError = err;
56 + } catch (e) {
57 + if (signal?.aborted) throw normalizeNetworkError(e);
58 + const err = e instanceof AIError ? e : normalizeNetworkError(e);
59 + if (!err.retryable) throw err;
60 + lastError = err;
61 + }
62 + if (attempt < MAX_RETRIES) {
63 + await new Promise((r) => setTimeout(r, RETRY_BASE_DELAY_MS * 2 ** attempt));
64 + }
65 + }
66 + throw lastError ?? new AIError({ code: "UNKNOWN", message: "Request failed.", retryable: false });
67 +}
68 +
69 +/**
70 + * Open a streaming chat completion. Returns the raw Response whose body is an SSE stream.
71 + * No retries once the stream is open; connection-phase transient errors retry once.
72 + */
73 +export async function orChatStream(
74 + body: Record<string, unknown>,
75 + signal: AbortSignal
76 +): Promise<Response> {
77 + const modelId = typeof body.model === "string" ? body.model : undefined;
78 + let lastError: AIError | null = null;
79 + for (let attempt = 0; attempt <= 1; attempt++) {
80 + try {
81 + const res = await fetch(`${BASE_URL}/chat/completions`, {
82 + method: "POST",
83 + headers: baseHeaders(),
84 + body: JSON.stringify({ ...body, stream: true, usage: { include: true } }),
85 + signal,
86 + });
87 + if (res.ok && res.body) return res;
88 + const err = normalizeHttpError(res.status, await readErrorMessage(res), modelId);
89 + if (!err.retryable) throw err;
90 + lastError = err;
91 + } catch (e) {
92 + if (signal.aborted) throw normalizeNetworkError(e, modelId);
93 + const err = e instanceof AIError ? e : normalizeNetworkError(e, modelId);
94 + if (!err.retryable) throw err;
95 + lastError = err;
96 + }
97 + if (attempt < 1) await new Promise((r) => setTimeout(r, RETRY_BASE_DELAY_MS));
98 + }
99 + throw lastError ?? new AIError({ code: "UNKNOWN", message: "Stream failed to open.", retryable: false, modelId });
100 +}
added src/lib/openrouter/errors.ts +146 −0
@@ -0,0 +1,146 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +export type AIErrorCode =
6 + | "AUTHENTICATION_ERROR"
7 + | "RATE_LIMIT"
8 + | "MODEL_UNAVAILABLE"
9 + | "PROVIDER_UNAVAILABLE"
10 + | "CONTEXT_TOO_LARGE"
11 + | "INVALID_REQUEST"
12 + | "CONTENT_REJECTED"
13 + | "TIMEOUT"
14 + | "NETWORK_ERROR"
15 + | "UNKNOWN";
16 +
17 +export interface NormalizedAIError {
18 + code: AIErrorCode;
19 + message: string;
20 + retryable: boolean;
21 + modelId?: string;
22 + status?: number;
23 +}
24 +
25 +export class AIError extends Error implements NormalizedAIError {
26 + code: AIErrorCode;
27 + retryable: boolean;
28 + modelId?: string;
29 + status?: number;
30 +
31 + constructor(err: NormalizedAIError) {
32 + super(err.message);
33 + this.name = "AIError";
34 + this.code = err.code;
35 + this.retryable = err.retryable;
36 + this.modelId = err.modelId;
37 + this.status = err.status;
38 + }
39 +}
40 +
41 +/** Map an HTTP status + upstream message to a normalized error. Never leaks internals. */
42 +export function normalizeHttpError(status: number, upstreamMessage: string, modelId?: string): AIError {
43 + const msg = (upstreamMessage || "").slice(0, 500);
44 + switch (status) {
45 + case 401:
46 + case 403:
47 + return new AIError({
48 + code: "AUTHENTICATION_ERROR",
49 + message: "The gateway rejected the server credentials. Check the OpenRouter key.",
50 + retryable: false,
51 + modelId,
52 + status,
53 + });
54 + case 402:
55 + return new AIError({
56 + code: "AUTHENTICATION_ERROR",
57 + message: "OpenRouter account has insufficient credits.",
58 + retryable: false,
59 + modelId,
60 + status,
61 + });
62 + case 404:
63 + return new AIError({
64 + code: "MODEL_UNAVAILABLE",
65 + message: "This model is not available right now. Pick another model.",
66 + retryable: false,
67 + modelId,
68 + status,
69 + });
70 + case 408:
71 + return new AIError({ code: "TIMEOUT", message: "The model timed out. Try again.", retryable: true, modelId, status });
72 + case 413:
73 + return new AIError({
74 + code: "CONTEXT_TOO_LARGE",
75 + message: "The conversation exceeds this model's context window. Trim it or switch to a larger-context model.",
76 + retryable: false,
77 + modelId,
78 + status,
79 + });
80 + case 429:
81 + return new AIError({
82 + code: "RATE_LIMIT",
83 + message: "Rate limited upstream. Wait a moment and retry.",
84 + retryable: true,
85 + modelId,
86 + status,
87 + });
88 + case 502:
89 + case 503:
90 + return new AIError({
91 + code: "PROVIDER_UNAVAILABLE",
92 + message: "The upstream provider is unavailable. Retry, or switch models.",
93 + retryable: true,
94 + modelId,
95 + status,
96 + });
97 + default:
98 + if (status >= 500) {
99 + return new AIError({
100 + code: "PROVIDER_UNAVAILABLE",
101 + message: "Upstream failure. Retry, or switch models.",
102 + retryable: true,
103 + modelId,
104 + status,
105 + });
106 + }
107 + if (/moderation|flagged|content policy/i.test(msg)) {
108 + return new AIError({
109 + code: "CONTENT_REJECTED",
110 + message: "The provider rejected this content.",
111 + retryable: false,
112 + modelId,
113 + status,
114 + });
115 + }
116 + if (/context length|maximum context|too many tokens/i.test(msg)) {
117 + return new AIError({
118 + code: "CONTEXT_TOO_LARGE",
119 + message: "The conversation exceeds this model's context window. Trim it or switch to a larger-context model.",
120 + retryable: false,
121 + modelId,
122 + status,
123 + });
124 + }
125 + return new AIError({
126 + code: "INVALID_REQUEST",
127 + message: msg || "The request was rejected.",
128 + retryable: false,
129 + modelId,
130 + status,
131 + });
132 + }
133 +}
134 +
135 +export function normalizeNetworkError(err: unknown, modelId?: string): AIError {
136 + if (err instanceof AIError) return err;
137 + if (err instanceof Error && err.name === "AbortError") {
138 + return new AIError({ code: "TIMEOUT", message: "The request was cancelled.", retryable: false, modelId });
139 + }
140 + return new AIError({
141 + code: "NETWORK_ERROR",
142 + message: "Could not reach the model gateway. Check connectivity and retry.",
143 + retryable: true,
144 + modelId,
145 + });
146 +}
added src/lib/openrouter/index.ts +9 −0
@@ -0,0 +1,9 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +export * from "./types";
6 +export * from "./errors";
7 +export { fetchModels, normalizeModel } from "./models";
8 +export { streamGeneration, OpenRouterGateway } from "./stream";
9 +export { estimateCost, formatUsd, perMillion } from "./pricing";
added src/lib/openrouter/models.ts +49 −0
@@ -0,0 +1,49 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import { orGet } from "./client";
6 +import { parseModelsResponse, type ORModelRaw } from "./schemas";
7 +import { deriveCapabilities } from "./capabilities";
8 +import { derivePricing } from "./pricing";
9 +import type { ModelDefinition } from "./types";
10 +
11 +/** Provider is the human-readable prefix of the display name ("Anthropic: Claude ..."), never parsed from the ID for behavior. */
12 +function deriveProvider(raw: ORModelRaw): string | undefined {
13 + const name = raw.name ?? "";
14 + const colon = name.indexOf(":");
15 + if (colon > 0) return name.slice(0, colon).trim();
16 + const slash = raw.id.indexOf("/");
17 + if (slash > 0) return raw.id.slice(0, slash);
18 + return undefined;
19 +}
20 +
21 +function displayName(raw: ORModelRaw): string {
22 + const name = raw.name ?? raw.id;
23 + const colon = name.indexOf(":");
24 + return colon > 0 ? name.slice(colon + 1).trim() : name;
25 +}
26 +
27 +export function normalizeModel(raw: ORModelRaw): ModelDefinition {
28 + return {
29 + id: raw.id,
30 + name: displayName(raw),
31 + provider: deriveProvider(raw),
32 + description: raw.description,
33 + contextLength: raw.context_length ?? raw.top_provider?.context_length,
34 + pricing: derivePricing(raw),
35 + capabilities: deriveCapabilities(raw),
36 + metadata: {
37 + architecture: raw.architecture?.modality,
38 + tokenizer: raw.architecture?.tokenizer,
39 + createdAt: raw.created,
40 + raw,
41 + },
42 + };
43 +}
44 +
45 +/** Fetch and normalize the full OpenRouter model catalog. */
46 +export async function fetchModels(signal?: AbortSignal): Promise<ModelDefinition[]> {
47 + const json = await orGet("/models", signal);
48 + return parseModelsResponse(json).map(normalizeModel);
49 +}
added src/lib/openrouter/pricing.ts +57 −0
@@ -0,0 +1,57 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +import type { ORModelRaw } from "./schemas";
6 +import type { ModelDefinition } from "./types";
7 +
8 +function toNumber(v: string | number | null | undefined): number | undefined {
9 + if (v === null || v === undefined) return undefined;
10 + const n = typeof v === "number" ? v : parseFloat(v);
11 + return Number.isFinite(n) ? n : undefined;
12 +}
13 +
14 +/** Pricing values are USD per token (per request/image where applicable). */
15 +export function derivePricing(raw: ORModelRaw): ModelDefinition["pricing"] {
16 + const p = raw.pricing ?? {};
17 + return {
18 + prompt: toNumber(p.prompt),
19 + completion: toNumber(p.completion),
20 + image: toNumber(p.image),
21 + request: toNumber(p.request),
22 + };
23 +}
24 +
25 +/** Estimated cost in USD from token counts and per-token prices. */
26 +export function estimateCost(
27 + pricing: ModelDefinition["pricing"] | undefined,
28 + promptTokens: number | undefined,
29 + completionTokens: number | undefined
30 +): number | undefined {
31 + if (!pricing) return undefined;
32 + const inCost = pricing.prompt !== undefined && promptTokens !== undefined ? pricing.prompt * promptTokens : undefined;
33 + const outCost =
34 + pricing.completion !== undefined && completionTokens !== undefined
35 + ? pricing.completion * completionTokens
36 + : undefined;
37 + if (inCost === undefined && outCost === undefined) return undefined;
38 + return (inCost ?? 0) + (outCost ?? 0);
39 +}
40 +
41 +/** Format a USD amount for display: $0.0042, $0.42, $4.20. */
42 +export function formatUsd(v: number | undefined | null): string {
43 + if (v === undefined || v === null) return "—";
44 + if (v === 0) return "$0";
45 + if (v < 0.01) return `$${v.toFixed(4)}`;
46 + if (v < 1) return `$${v.toFixed(3)}`;
47 + return `$${v.toFixed(2)}`;
48 +}
49 +
50 +/** Price per 1M tokens for the model rail chip. */
51 +export function perMillion(perToken: number | undefined): string {
52 + if (perToken === undefined) return "—";
53 + const v = perToken * 1_000_000;
54 + if (v === 0) return "free";
55 + if (v < 1) return `$${v.toFixed(2)}/M`;
56 + return `$${v.toFixed(v < 10 ? 2 : 0)}/M`;
57 +}
added src/lib/openrouter/schemas.ts +68 −0
@@ -0,0 +1,68 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Minimal runtime validation of OpenRouter wire shapes (no external dep).
6 +
7 +export interface ORModelRaw {
8 + id: string;
9 + name?: string;
10 + description?: string;
11 + context_length?: number;
12 + created?: number;
13 + pricing?: Record<string, string | number | null>;
14 + architecture?: {
15 + modality?: string;
16 + input_modalities?: string[];
17 + output_modalities?: string[];
18 + tokenizer?: string;
19 + instruct_type?: string | null;
20 + };
21 + supported_parameters?: string[];
22 + top_provider?: { context_length?: number; max_completion_tokens?: number };
23 +}
24 +
25 +export function parseModelsResponse(json: unknown): ORModelRaw[] {
26 + if (typeof json !== "object" || json === null) return [];
27 + const data = (json as { data?: unknown }).data;
28 + if (!Array.isArray(data)) return [];
29 + return data.filter(
30 + (m): m is ORModelRaw => typeof m === "object" && m !== null && typeof (m as ORModelRaw).id === "string"
31 + );
32 +}
33 +
34 +/** One parsed SSE chunk from the OpenRouter chat stream. */
35 +export interface ORStreamChunk {
36 + id?: string;
37 + choices?: Array<{
38 + delta?: {
39 + content?: string | null;
40 + reasoning?: string | null;
41 + tool_calls?: Array<{
42 + index?: number;
43 + id?: string;
44 + function?: { name?: string; arguments?: string };
45 + }>;
46 + };
47 + finish_reason?: string | null;
48 + }>;
49 + usage?: {
50 + prompt_tokens?: number;
51 + completion_tokens?: number;
52 + total_tokens?: number;
53 + cost?: number;
54 + completion_tokens_details?: { reasoning_tokens?: number };
55 + prompt_tokens_details?: { cached_tokens?: number };
56 + };
57 + error?: { message?: string; code?: number | string };
58 +}
59 +
60 +export function parseStreamChunk(line: string): ORStreamChunk | null {
61 + try {
62 + const parsed = JSON.parse(line);
63 + if (typeof parsed !== "object" || parsed === null) return null;
64 + return parsed as ORStreamChunk;
65 + } catch {
66 + return null;
67 + }
68 +}
added src/lib/openrouter/stream.ts +125 −0
@@ -0,0 +1,125 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Parses the OpenRouter SSE wire stream into normalized ChatStreamEvents.
6 +
7 +import { orChatStream } from "./client";
8 +import { parseStreamChunk } from "./schemas";
9 +import { AIError, normalizeHttpError, normalizeNetworkError } from "./errors";
10 +import type { ChatStreamEvent, GenerationRequest, ModelDefinition, ModelGateway } from "./types";
11 +import { fetchModels } from "./models";
12 +
13 +/**
14 + * Stream a generation as normalized events. Cancellation propagates upstream
15 + * through request.signal — aborting truly stops the OpenRouter generation.
16 + */
17 +export async function* streamGeneration(
18 + request: GenerationRequest,
19 + generationId: string
20 +): AsyncGenerator<ChatStreamEvent> {
21 + const signal = request.signal ?? new AbortController().signal;
22 +
23 + const body: Record<string, unknown> = {
24 + model: request.model,
25 + messages: request.messages,
26 + };
27 + if (request.temperature !== undefined) body.temperature = request.temperature;
28 + if (request.maxTokens !== undefined) body.max_tokens = request.maxTokens;
29 + if (request.routing) Object.assign(body, request.routing);
30 +
31 + let res: Response;
32 + try {
33 + res = await orChatStream(body, signal);
34 + } catch (e) {
35 + const err = e instanceof AIError ? e : normalizeNetworkError(e, request.model);
36 + yield { type: "generation.error", message: err.message, retryable: err.retryable };
37 + return;
38 + }
39 +
40 + yield { type: "generation.start", generationId, model: request.model };
41 +
42 + const reader = res.body!.getReader();
43 + const decoder = new TextDecoder();
44 + let buffer = "";
45 + const openToolCalls = new Set<string>();
46 +
47 + try {
48 + for (;;) {
49 + const { done, value } = await reader.read();
50 + if (done) break;
51 + buffer += decoder.decode(value, { stream: true });
52 +
53 + // SSE frames are separated by newlines; each data line is "data: {json}" or "data: [DONE]"
54 + let newlineIdx: number;
55 + while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
56 + const line = buffer.slice(0, newlineIdx).trim();
57 + buffer = buffer.slice(newlineIdx + 1);
58 + if (!line.startsWith("data:")) continue; // comments / keep-alives
59 + const payload = line.slice(5).trim();
60 + if (payload === "[DONE]") continue;
61 +
62 + const chunk = parseStreamChunk(payload);
63 + if (!chunk) continue;
64 +
65 + if (chunk.error) {
66 + const status = typeof chunk.error.code === "number" ? chunk.error.code : 500;
67 + const err = normalizeHttpError(status, chunk.error.message ?? "", request.model);
68 + yield { type: "generation.error", message: err.message, retryable: err.retryable };
69 + return;
70 + }
71 +
72 + const delta = chunk.choices?.[0]?.delta;
73 + if (delta?.reasoning) {
74 + yield { type: "reasoning.delta", text: delta.reasoning };
75 + }
76 + if (delta?.content) {
77 + yield { type: "content.delta", text: delta.content };
78 + }
79 + if (delta?.tool_calls) {
80 + for (const tc of delta.tool_calls) {
81 + const id = tc.id ?? `tool_${tc.index ?? 0}`;
82 + if (tc.function?.name && !openToolCalls.has(id)) {
83 + openToolCalls.add(id);
84 + yield { type: "tool.start", toolCallId: id, name: tc.function.name };
85 + }
86 + if (tc.function?.arguments) {
87 + yield { type: "tool.delta", toolCallId: id, argumentsDelta: tc.function.arguments };
88 + }
89 + }
90 + }
91 + if (chunk.usage) {
92 + yield {
93 + type: "usage",
94 + promptTokens: chunk.usage.prompt_tokens,
95 + completionTokens: chunk.usage.completion_tokens,
96 + reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
97 + cachedTokens: chunk.usage.prompt_tokens_details?.cached_tokens,
98 + totalTokens: chunk.usage.total_tokens,
99 + cost: chunk.usage.cost,
100 + };
101 + }
102 + }
103 + }
104 + yield { type: "generation.end" };
105 + } catch (e) {
106 + if (signal.aborted) {
107 + // Cancellation is not an error; the caller marks the generation cancelled.
108 + return;
109 + }
110 + const err = normalizeNetworkError(e, request.model);
111 + yield { type: "generation.error", message: err.message, retryable: err.retryable };
112 + } finally {
113 + reader.cancel().catch(() => {});
114 + }
115 +}
116 +
117 +/** The initial (and only) gateway implementation. */
118 +export class OpenRouterGateway implements ModelGateway {
119 + async *stream(request: GenerationRequest): AsyncIterable<ChatStreamEvent> {
120 + yield* streamGeneration(request, crypto.randomUUID());
121 + }
122 + listModels(): Promise<ModelDefinition[]> {
123 + return fetchModels();
124 + }
125 +}
added src/lib/openrouter/types.ts +71 −0
@@ -0,0 +1,71 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +/** Normalized model definition, decoupled from the OpenRouter wire format. */
6 +export interface ModelDefinition {
7 + id: string; // exact OpenRouter model ID — opaque, sent back verbatim
8 + name: string;
9 + provider?: string;
10 + description?: string;
11 + contextLength?: number;
12 + pricing?: {
13 + prompt?: number; // USD per token
14 + completion?: number;
15 + image?: number;
16 + request?: number;
17 + };
18 + capabilities: {
19 + text: boolean;
20 + vision: boolean;
21 + reasoning: boolean;
22 + tools: boolean;
23 + structuredOutput: boolean;
24 + };
25 + metadata: {
26 + architecture?: string;
27 + tokenizer?: string;
28 + createdAt?: number;
29 + raw?: unknown;
30 + };
31 +}
32 +
33 +/** Normalized application stream events — the browser never sees raw OpenRouter frames. */
34 +export type ChatStreamEvent =
35 + | { type: "generation.start"; generationId: string; model: string }
36 + | { type: "content.delta"; text: string }
37 + | { type: "reasoning.delta"; text: string }
38 + | { type: "tool.start"; toolCallId: string; name: string }
39 + | { type: "tool.delta"; toolCallId: string; argumentsDelta: string }
40 + | {
41 + type: "usage";
42 + promptTokens?: number;
43 + completionTokens?: number;
44 + reasoningTokens?: number;
45 + cachedTokens?: number;
46 + totalTokens?: number;
47 + cost?: number;
48 + }
49 + | { type: "generation.end" }
50 + | { type: "generation.error"; message: string; retryable: boolean };
51 +
52 +export interface ChatMessageInput {
53 + role: "system" | "user" | "assistant";
54 + content: string;
55 +}
56 +
57 +export interface GenerationRequest {
58 + model: string;
59 + messages: ChatMessageInput[];
60 + signal?: AbortSignal;
61 + temperature?: number;
62 + maxTokens?: number;
63 + /** Reserved for future per-generation routing config (provider prefs, fallbacks). */
64 + routing?: Record<string, unknown>;
65 +}
66 +
67 +/** Narrow gateway abstraction so the frontend never becomes inseparable from OpenRouter. */
68 +export interface ModelGateway {
69 + stream(request: GenerationRequest): AsyncIterable<ChatStreamEvent>;
70 + listModels(): Promise<ModelDefinition[]>;
71 +}
added src/proxy.ts +66 −0
@@ -0,0 +1,66 @@
1 +// Author: Simon-Pierre Boucher
2 +// Contact: contact@spboucher.ai
3 +// Project: chat.spboucher.ai
4 +
5 +// Edge proxy (Next 16 middleware convention): cheap cookie-presence gate + security headers.
6 +// Real session validation (DB-backed) happens in every route handler via requireSession().
7 +import { NextResponse, type NextRequest } from "next/server";
8 +
9 +const SESSION_COOKIE = "spb_session";
10 +
11 +const PUBLIC_PATHS = new Set(["/login", "/api/auth/login", "/manifest.webmanifest", "/sw.js"]);
12 +
13 +function isPublic(pathname: string): boolean {
14 + if (PUBLIC_PATHS.has(pathname)) return true;
15 + if (pathname.startsWith("/_next/")) return true;
16 + if (pathname.startsWith("/icons/")) return true;
17 + if (pathname === "/favicon.ico") return true;
18 + return false;
19 +}
20 +
21 +export function proxy(req: NextRequest) {
22 + const { pathname } = req.nextUrl;
23 + const hasCookie = Boolean(req.cookies.get(SESSION_COOKIE)?.value);
24 +
25 + let res: NextResponse;
26 + if (!isPublic(pathname) && !hasCookie) {
27 + if (pathname.startsWith("/api/")) {
28 + res = NextResponse.json({ error: "Unauthorized" }, { status: 401 });
29 + } else {
30 + const url = req.nextUrl.clone();
31 + url.pathname = "/login";
32 + url.search = "";
33 + res = NextResponse.redirect(url);
34 + }
35 + } else if (pathname === "/login" && hasCookie) {
36 + const url = req.nextUrl.clone();
37 + url.pathname = "/";
38 + url.search = "";
39 + res = NextResponse.redirect(url);
40 + } else {
41 + res = NextResponse.next();
42 + }
43 +
44 + res.headers.set("X-Content-Type-Options", "nosniff");
45 + res.headers.set("Referrer-Policy", "no-referrer");
46 + res.headers.set("X-Frame-Options", "DENY");
47 + res.headers.set(
48 + "Content-Security-Policy",
49 + [
50 + "default-src 'self'",
51 + "script-src 'self' 'unsafe-inline'",
52 + "style-src 'self' 'unsafe-inline'",
53 + "img-src 'self' data: blob:",
54 + "font-src 'self' data:",
55 + "connect-src 'self'",
56 + "frame-ancestors 'none'",
57 + "base-uri 'self'",
58 + "form-action 'self'",
59 + ].join("; ")
60 + );
61 + return res;
62 +}
63 +
64 +export const config = {
65 + matcher: ["/((?!_next/static|_next/image).*)"],
66 +};
67