commit 4e38b5a79173f60118a93278908d0ee6148f3880 Author: Arnaud Nelissen Date: Sun Jun 21 22:25:43 2026 +0200 feat: initial commit — Curio mastery tutor Full Next.js App Router application with: - AI-graded lesson checkpoints (BKT/Elo mastery tracking) - Auth.js v5: credentials, Google, GitHub, generic OIDC - Anonymous-session-first with migrate-on-signin - Admin panel: users, blueprints, reports, site settings - Password reset + email verification (nodemailer/SMTP) - Site config: require_auth + signups_enabled flags - server-only guards on all DB/generation/verification modules - PostgreSQL 16 + pgvector, Redis cache, Drizzle ORM - 271 unit tests (Vitest), golden-eval harness, Playwright e2e stubs Co-Authored-By: Claude Sonnet 4.6 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bbcfd99 --- /dev/null +++ b/.env.example @@ -0,0 +1,107 @@ +DATABASE_URL=postgresql://curio:curio@localhost:5433/curio + +# Auth.js v5 +AUTH_SECRET= # openssl rand -hex 32 +AUTH_URL=http://localhost:3000 +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= +AUTH_GITHUB_ID= +AUTH_GITHUB_SECRET= + +# Authentik / generic OIDC (optional — any OIDC-compliant IdP: Keycloak, Dex, etc.) +# Leave AUTH_OIDC_ISSUER blank to disable the SSO button entirely. +AUTH_OIDC_ISSUER= # e.g. https://authentik.example.com/application/o/curio/ +AUTH_OIDC_CLIENT_ID= +AUTH_OIDC_CLIENT_SECRET= +AUTH_OIDC_DISPLAY_NAME=SSO # Label shown on the login button + +# Email verification (optional — nodemailer) +SMTP_HOST=smtp.eu.mailgun.org +SMTP_PORT=587 +SMTP_USER=curio@arnaudne.fr +SMTP_PASS=1e314233c1b2c72b89dee03622bde622-0b3150f2-43f373c4 +EMAIL_FROM=curio@arnaudne.fr + +# Langfuse — LLM call observability (optional; logs to console when absent) +LANGFUSE_SECRET_KEY= +LANGFUSE_PUBLIC_KEY= +LANGFUSE_HOST=https://cloud.langfuse.com + +# Per-session output token cap (~50k ≈ 3–4 full lessons of grading + probes) +SESSION_TOKEN_BUDGET=50000 +REDIS_URL=redis://localhost:6379 + +# Upstash Redis (REST API) — cold-start rate limiter; optional, degrades gracefully +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= + +# Generator model — strong, creative (defaults to Anthropic Claude) +LLM_GENERATOR_PROVIDER=anthropic +LLM_GENERATOR_MODEL=claude-sonnet-4-6 +ANTHROPIC_API_KEY= + +# Grader model — different provider for decorrelation (defaults to OpenAI) +LLM_GRADER_PROVIDER=openai +LLM_GRADER_MODEL=gpt-4o-mini +OPENAI_API_KEY= + +# Cheap grader — fast/low-cost model for rubric-matched majority cases (§10.2 cascade) +# Falls back to LLM_GRADER_* when not set (no performance benefit, but safe default) +LLM_CHEAP_GRADER_PROVIDER=openai +LLM_CHEAP_GRADER_MODEL=gpt-4o-mini + +# Safety mode: open | controlled (default) | enterprise-safe (§14) +CONTENT_SAFETY_MODE=controlled + +# Embedding model — used for RAG retrieval (defaults to OpenAI) +LLM_EMBED_PROVIDER=openai +LLM_EMBED_MODEL=text-embedding-3-small + +# ── Google Gemini (provider=google) ────────────────────────────────────────── +# Models: gemini-2.0-flash, gemini-2.5-pro, etc. Supports embeddings. +GOOGLE_GENERATIVE_AI_API_KEY= + +# ── Mistral (provider=mistral) ──────────────────────────────────────────────── +# Models: mistral-large-latest, mistral-small-latest. Supports embeddings. +MISTRAL_API_KEY= + +# ── Cohere (provider=cohere) ────────────────────────────────────────────────── +# Models: command-r-plus, command-r. Supports embeddings (embed-english-v3.0). +COHERE_API_KEY= + +# ── Groq (provider=groq) ────────────────────────────────────────────────────── +# Models: llama-3.3-70b-versatile, mixtral-8x7b-32768. No embeddings. +GROQ_API_KEY= + +# ── xAI / Grok (provider=xai) ──────────────────────────────────────────────── +# Models: grok-3, grok-3-mini. No embeddings. +XAI_API_KEY= + +# ── AWS Bedrock (provider=bedrock) ──────────────────────────────────────────── +# Models: anthropic.claude-sonnet-4-6-v1, amazon.titan-embed-text-v2:0, etc. +# Uses AWS SDK credential chain (env vars, ~/.aws/credentials, IAM role). +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# ── Azure OpenAI (provider=azure) ───────────────────────────────────────────── +# model = deployment name (not model name). Supports embeddings. +AZURE_OPENAI_RESOURCE_NAME= +AZURE_API_KEY= + +# ── OpenRouter (provider=openrouter) ───────────────────────────────────────── +# Routes to any model. No embeddings — keep LLM_EMBED_PROVIDER=openai. +# Model IDs: "anthropic/claude-sonnet-4-6", "google/gemini-2.0-flash", etc. +OPENROUTER_API_KEY= + +# ── Ollama (provider=ollama) ────────────────────────────────────────────────── +# Local. No API key. Supports embeddings (nomic-embed-text). +OLLAMA_BASE_URL=http://localhost:11434/api + +# ── Fully local example (no API keys) ──────────────────────────────────────── +# LLM_GENERATOR_PROVIDER=ollama +# LLM_GENERATOR_MODEL=llama3.2 +# LLM_GRADER_PROVIDER=ollama +# LLM_GRADER_MODEL=llama3.2 +# LLM_EMBED_PROVIDER=ollama +# LLM_EMBED_MODEL=nomic-embed-text diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3722418 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bd9f99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# dependencies +node_modules +.pnpm-store + +# Next.js +.next +out + +# environment +.env.local +.env*.local + +# drizzle — migrations and meta are committed (drizzle-kit needs the journal) + +# misc +.DS_Store +*.log +npm-debug.log* +pnpm-debug.log* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a60089d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# CLAUDE.md — Curio + +Repo context for Claude Code. Read this every session. Keep it current: if a convention changes, update this file in the same commit. Full product/architecture/design reference: `docs/curio-specification.md`. + +## What Curio is (one paragraph) + +Curio is a curiosity-led **mastery tutor**. A learner states intent → gets a Journey of bounded ~10-minute **Lessons** → each Lesson is short readable AI-generated explanation segments punctuated by **production checkpoints** (predict / explain-back / solve) → the AI **diagnoses** the learner's free-text response against pre-verified rubrics and a per-concept misconception library, gives targeted feedback, and gates advancement on demonstrated mastery. The moat is **diagnosis** (inferring *what* a learner misunderstands and *why*), not content generation. + +## Decided stack — single language (TypeScript), do not substitute without asking + +- **App:** Next.js (App Router) + TypeScript — RSC reading surface + Route Handlers (API) + native streaming/SSE +- **LLM:** Vercel AI SDK (provider-agnostic, streaming, schema-constrained structured output). Generator and grader configured to **different** models/providers. +- **Contracts:** Zod — shared across API I/O, LLM outputs, DB-derived types +- **DB:** PostgreSQL 16 + pgvector (single store, source of truth) +- **ORM/migrations:** Drizzle ORM + drizzle-kit +- **Cache/buffer:** Redis — a cache/buffer in *front* of Postgres, never load-bearing for availability +- **Background jobs:** Route Handlers + the `job` ledger table for the slice; **BullMQ** (Redis) when scale arrives — jobs spend money, so they must be **idempotent** with durable Postgres state +- **Styling/motion:** Tailwind + CSS custom properties (design tokens); motion via CSS transitions + **View Transitions API** (no animation library); honor `prefers-reduced-motion` +- **Observability:** Langfuse (or OpenTelemetry) for LLM call tracing +- **Rate limiting:** edge limiter (e.g. Upstash) on the cold-start generation endpoint +- **Tests:** Vitest (unit, LLM mocked) + golden-eval runner (real-model, CI) + Playwright (e2e) +- **Local infra:** docker-compose (postgres+pgvector, redis) + +## Repo structure + +``` +curio/ + CLAUDE.md + docker-compose.yml + docs/curio-specification.md + src/ + app/ # Next.js App Router: reading surface + api/ route handlers + components/ + lib/ + llm/ # provider-agnostic client (Vercel AI SDK) + versioned prompt registry + generation/ # RAG-grounded content generation + verification/ # content cascade (T0–T2) + response grading + memory/ # mastery (BKT/Elo) + spaced repetition + intent/ # intent normalization + blueprint lookup + db/ # drizzle schema, queries, migrations + schemas/ # zod contracts (shared) + styles/ # design tokens + jobs/ # BullMQ workers (deferred until P3) + tests/golden/ # frozen eval sets: content verification + response grading + e2e/ # playwright +``` + +## Architectural invariants — violating these is a bug + +1. **All LLM access goes through `src/lib/llm/client.ts`.** Never call a provider/AI-SDK model directly elsewhere. Generator and grader pull their model from config; they must differ. +2. **Prompts live in the versioned registry** (`src/lib/llm/prompts/`), never inline. Each has an id + version. +3. **Contracts are Zod schemas** in `src/schemas/`, shared by the LLM layer, the API, and the UI. LLM structured output is validated against them. +4. **The serve path reads from the Redis buffer; jobs generate.** No synchronous content generation in request handlers except the documented cold-start gate. Postgres is the source of truth — a Redis outage degrades latency, not availability. +5. **Generated content stores provenance** (`source_chunk_ids`) so verification and grading are *grounded* against sources, not free-judged. +6. **Grading is constrained.** The grader evaluates against a pre-verified rubric + reference answer + misconception library. Verdict ∈ {`mastered`,`partial`,`misconception`,`uncertain`}. It **must** support `uncertain` (abstain → reveal). **Bias against false-fail** — telling a correct learner they're wrong is the worst error. +7. **Verification is a cascade.** T0 structural (free) → T1 cheap-model → T2 strong-model (adversarial, at blueprint promotion). Expensive checks amortized once per blueprint, never per serve. +8. **Paid background jobs are idempotent** (idempotency key + durable `job` state). A retry must never regenerate-and-rebill. +9. **Per-session token budget is enforced in code.** The ~10-min Lesson bound is a real cap. +10. **Content is versioned** by `(intent_key, content_version, model_version)`; invalidation is lazy. + +## Design invariants (see spec §7) + +- **Thesis:** a calm, editorial reading surface that comes alive only when the learner responds. Not gamified, not a dashboard, not a chat-bubble app. +- **Signature:** diagnosis-as-**marginalia** — feedback threaded against the learner's own words, pointing at the `evidence_span` that tripped them. Spend the design's boldness here; keep everything else quiet. +- **Motion** is meaning, not decoration: reserved for the diagnosis moment (a brief "considering" beat → feedback eases in). CSS/View Transitions only. +- **Reading is the product:** humanist reading serif, generous measure (~62–68ch) and leading; display face used with restraint; tokens centralized in `src/styles/`. +- **Quality floor (non-negotiable):** responsive to mobile, visible keyboard focus, reduced-motion respected, WCAG-AA contrast (including every diagnosis-state color). +- **Before building any UI, run the frontend-design two-pass process** (plan tokens → critique for genericness → build). Avoid AI-default looks (cream+serif+terracotta, near-black+acid accent, broadsheet hairlines). +- Copy is design material: warm, specific, plain, on the learner's side; never gamified or condescending. + +## Commands + +```bash +docker compose up -d # postgres+pgvector, redis +pnpm install # or npm +pnpm db:generate && pnpm db:migrate +pnpm dev # Next.js +pnpm test # vitest (unit; LLM mocked) +pnpm test:golden # real-model eval harness (CI-gated) +pnpm test:e2e # playwright +pnpm lint && pnpm format # eslint + prettier +``` + +## Testing rules + +- Write/extend tests **with** each change; run the suite before declaring a task done. +- **Mock the LLM in unit tests** (deterministic fixtures). Real-model calls only in the golden-eval suite. +- Golden evals (`tests/golden/`) are the bar for the moat: response-grading accuracy (track **false-fail rate**) + content-verification accuracy. Don't regress them; add cases on new failure modes. +- Migrations: never edit an applied drizzle migration; add a new one. + +## Out of scope — do NOT build unless explicitly asked + +Gamification (XP/badges/leaderboards), infinite feed / social, multi-provider fallback orchestration, knowledge graph, voice, PDF ingestion, accounts (MVP is anonymous-session-first). If a task seems to require one of these, stop and ask. + +## Working agreement + +- **Plan before coding** for anything non-trivial; surface architecture- or design-affecting choices before implementing. +- Small, focused commits per task. One ticket = one logical change. +- Keep Route Handlers thin; logic lives in `src/lib/`. +- Run the frontend-design planning pass before UI tickets. +- If you change a convention here, update this file in the same commit. +- When unsure between two reasonable designs, ask rather than guess — especially on the grading/verification path and the diagnosis UI. \ No newline at end of file diff --git a/KICKOFF.md b/KICKOFF.md new file mode 100644 index 0000000..06b9e5e --- /dev/null +++ b/KICKOFF.md @@ -0,0 +1,84 @@ +# Curio — Claude Code Kickoff & Task Plan + +Two parts: the **kickoff prompt** to paste into Claude Code first, and the **phased tickets** to work through after. Both assume `docs/curio-specification.md` and `CLAUDE.md` are in the repo. + +--- + +## PART 1 — Kickoff prompt (paste this first) + +> You are building **Curio**, an AI mastery tutor (TypeScript, single stack). Read `CLAUDE.md` and `docs/curio-specification.md` in full before doing anything — `CLAUDE.md` holds the decided stack, repo structure, and the architectural + design invariants, all non-negotiable. +> +> **Do not write feature code yet.** First, produce a plan: propose the initial scaffold (matching the structure in `CLAUDE.md`) — Next.js (App Router, TS) + Tailwind, docker-compose for Postgres+pgvector and Redis, Drizzle schema + migration setup, Vitest, and a stubbed `src/lib/llm/client.ts` wrapping the Vercel AI SDK with generator + grader as distinct configured models, mockable in tests. Include a minimal `src/styles/` design-token file derived from the §7 direction. List the files you'll create and the commands that verify the scaffold runs. **Wait for my approval before implementing.** +> +> After I approve the scaffold, we work through the tickets in this plan **one at a time**, in order. For each ticket: plan briefly, implement, write tests, run the suite, then stop and summarize what changed and how to verify it. Do not start the next ticket until I confirm. +> +> For any ticket that builds UI, **first run the frontend-design two-pass process** (plan tokens → critique for genericness against the §7 brief → build); do not reach for AI-default looks. Honor every invariant in `CLAUDE.md` — especially: all LLM calls go through `src/lib/llm/client.ts`; contracts are shared Zod schemas; the grader supports `uncertain`/abstention and is biased against false-fail; content carries provenance; the serve path reads the buffer while jobs generate; motion is CSS/View-Transitions only. If a ticket appears to require anything in the "Out of scope" list, stop and ask. + +**Why this shape:** Claude Code performs best with (1) full context up front, (2) a plan-and-confirm gate before large work, (3) bounded tickets it completes and verifies one at a time, and (4) a fixed set of invariants it can check itself against. + +--- + +## PART 2 — Tickets (build in order; each is a vertical or testable slice) + +Format per ticket: **Goal · Scope · Acceptance criteria.** "Done" = acceptance criteria met **and** tests pass **and** invariants upheld. + +### P0-1 — Scaffold & infra +- **Goal:** Runnable skeleton. +- **Scope:** Repo structure per `CLAUDE.md`; Next.js (App Router, TS) + Tailwind; docker-compose (pg+pgvector, redis); Drizzle init + first migration; `/api/health` route handler; `src/lib/llm/client.ts` (Vercel AI SDK) with a **mock provider**; `src/schemas/` bootstrapped with Zod; `src/styles/` design tokens; Vitest + eslint/prettier configured. +- **Accept:** `docker compose up` + `pnpm dev` start; `/api/health` returns 200; `pnpm test` runs; LLM client is importable and mockable; tokens load in a sample page. + +### P1-1 — Content model + seeded corpus + retrieval +- **Goal:** Grounding substrate for one hardcoded topic. +- **Scope:** Drizzle schema + migrations for `source_chunk`, `concept`, `lesson`, `segment`, `checkpoint`, `misconception`, `job` (see spec §17). Seed a small trusted corpus for ONE topic. pgvector retrieval helper in `src/lib/intent`/`generation`. +- **Accept:** Given a concept, retrieval returns relevant chunks; migrations run clean up/down; seed script idempotent. + +### P1-2 — Lesson generation (RAG-grounded) +- **Goal:** Generate one Lesson for the seeded topic. +- **Scope:** `src/lib/generation` produces a Lesson = 2–3 segments, each with explanation + one checkpoint + **reference answer + rubric**, grounded in retrieved chunks, storing `source_chunk_ids`. Schema-constrained (Zod) structured output via the AI SDK. Prompts in the registry. +- **Accept:** Lesson persists with provenance on every segment; rubric + reference answer present per checkpoint; reproducible against a mocked LLM; one real-model smoke test. + +### P1-3 — Serve + render the reading surface (design-critical) +- **Goal:** Learner reads a beautiful Lesson and reaches a checkpoint. +- **Scope:** Route handler returns a Lesson (served from buffer/cache, not generated inline); reading surface renders segments as calm editorial prose with checkpoints punctuating (not peppering); free-text input at each checkpoint. **Run the frontend-design pass first**; implement the §7 direction (reading serif, generous measure, restrained chrome, tokens from `src/styles/`). Cold-start outline is a designed state, not a spinner. +- **Accept:** Reads like a short article; meets the quality floor (responsive, keyboard focus, reduced-motion, AA contrast); no synchronous generation in the request path (assert via test/log). + +### P1-4 — Response grading + marginalia (the moat MVP) +- **Goal:** AI diagnoses a free-text response and the tutor responds in the margin. +- **Scope:** `src/lib/verification` grader evaluates the response against the **pre-stored rubric + reference answer + misconception library**; returns the §10.3 grader contract (Zod-validated); supports `uncertain` → reveal model answer; biased against false-fail. UI renders **diagnosis-as-marginalia** threaded to the `evidence_span`, with the single View-Transitions motion moment. Persist `response` + `grade`. Wire Langfuse tracing on the call. +- **Accept:** Correct/partial/misconceived/empty responses each map to sensible verdicts on a mocked grader; `uncertain` triggers reveal; feedback points at the learner's own words; every response→grade is logged with cost/latency. + +### P1-5 — Golden eval harness (gate the moat) +- **Goal:** Measure grading quality; prevent regressions. +- **Scope:** `tests/golden/` with labeled `response → expected verdict` cases for the seeded topic; a runner reporting accuracy, **false-fail rate**, misconception-hit rate; wired into CI (`pnpm test:golden`). +- **Accept:** Runs against the real grader and prints metrics; CI fails if false-fail rate exceeds the set threshold; adding a case is trivial. + +> **P1 milestone = MVP hypothesis test:** one topic, generate → read → produce → diagnose-in-the-margin → reveal, with measured grading quality. Validate diagnosis quality (and that the marginalia experience lands) before expanding. + +### P2-1 — Misconception library quality +- **Goal:** Diagnosis depth. Generate distinct, real, diagnosable misconceptions per concept; verify them (T2). +- **Accept:** Golden misconception-hit rate beats a generic-feedback baseline; misconceptions are distinct and source-grounded. + +### P2-2 — Content verification cascade +- **Scope:** T0 structural + T1 cheap-model grounding + **blind checkpoint self-solve**; T2 adversarial at promotion. Amortized per blueprint. +- **Accept:** Bad content (unsupported claim, wrong reference answer) is caught; verification runs once per blueprint, not per serve; golden content-verification metrics tracked. + +### P3-1 — Mastery + spaced repetition +- **Scope:** `src/lib/memory`: per-concept mastery (BKT or Elo) updated from grades; spaced-rep schedule; mastery gate on advancement. +- **Accept:** Mastery moves correctly on repeated outcomes; due-review query returns the right concepts. + +### P3-2 — Blueprint cache + intent normalization + jobs +- **Scope:** Structured + embedding intent keying; blueprint lookup; **BullMQ** workers for `generate_next` / `promote_to_blueprint` with **idempotency keys + durable `job` state**; Redis buffer with backpressure; edge rate limiter on cold-start. +- **Accept:** Equivalent intents collapse to one key; warm serves are cache hits (no generation); cold path promotes into cache async; a retried job never regenerates-and-rebills. + +### P4 — Roadmap features (only when asked, one at a time) +Confidence rating before reveal · Socratic follow-up on `partial` · Daily Review · mastery map · source citations · variant-based personalization · accounts. (XP/feed/social/voice/PDF remain out of scope per `CLAUDE.md`.) + +--- + +## Claude Code workflow tips + +- Run `/init` once to seed a `CLAUDE.md`, then replace it with the curated one — keeps it accurate to the actual repo. +- Use **plan mode** for P0-1, any ticket on the grading/verification path, and any UI ticket (pair it with the frontend-design pass). +- Keep context lean: point it at specific files per ticket rather than the whole tree. +- After each ticket, run `pnpm test` + `pnpm test:golden` and report metrics before moving on. +- Treat the golden eval thresholds as the definition of "good enough" — they're how Claude Code knows whether its diagnosis work actually works. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..41d4d10 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Curio + +A curiosity-led mastery tutor. Learner states intent → gets a bounded ~10-minute Lesson → AI diagnoses free-text responses against pre-verified rubrics and a per-concept misconception library → gates advancement on demonstrated mastery. + +The moat is **diagnosis**: inferring *what* a learner misunderstands and *why*, not content generation. + +--- + +## Prerequisites + +- Node.js 20+, pnpm 9+ +- Docker (for Postgres + Redis) +- API keys: Anthropic (generator) + OpenAI (grader) at minimum + +--- + +## Quick start + +```bash +# 1. Clone and install +pnpm install + +# 2. Copy env and fill in API keys +cp .env.example .env.local +# Required: ANTHROPIC_API_KEY, OPENAI_API_KEY +# Optional: LANGFUSE_* for tracing, UPSTASH_* for rate limiting + +# 3. Start Postgres (port 5433) + Redis (port 6379) +docker compose up -d + +# 4. Run migrations +pnpm drizzle-kit generate && pnpm drizzle-kit migrate + +# 5. Seed the JS Closures demo topic +pnpm seed + +# 6. Start dev server +pnpm dev +# → http://localhost:3000 +``` + +--- + +## Environment variables + +All vars documented in `.env.example`. Key ones: + +| Variable | Required | Description | +|---|---|---| +| `DATABASE_URL` | ✓ | `postgresql://curio:curio@localhost:5433/curio` (docker default) | +| `REDIS_URL` | ✓ | `redis://localhost:6379` (docker default) | +| `ANTHROPIC_API_KEY` | ✓ | Generator model (default: `claude-sonnet-4-6`) | +| `OPENAI_API_KEY` | ✓ | Grader + embeddings (default: `gpt-4o-mini`, `text-embedding-3-small`) | +| `LANGFUSE_SECRET_KEY` | — | LLM call tracing; logs to console when absent | +| `SESSION_TOKEN_BUDGET` | — | Per-session output token cap (default: 50000) | +| `CONTENT_SAFETY_MODE` | — | `open` / `controlled` (default) / `enterprise-safe` | + +**Provider-agnostic:** generator, grader, and embeddings are each independently configurable. Supported: Anthropic, OpenAI, Google Gemini, Mistral, Cohere, Groq, xAI, AWS Bedrock, Azure OpenAI, OpenRouter, Ollama (fully local, no API keys). See `.env.example` for all options. + +--- + +## Commands + +```bash +pnpm dev # Next.js dev server +pnpm build # Production build +pnpm test # Vitest unit tests (LLM mocked — no API keys needed) +pnpm test:watch # Watch mode +pnpm test:e2e # Playwright end-to-end +pnpm test:golden # Real-model grading eval (CI-gated, needs API keys) +pnpm test:golden:misconceptions # Misconception quality eval +pnpm test:golden:content # Content verification eval +pnpm workers # BullMQ background workers (lesson generation, blueprint promotion) +pnpm seed # Seed JS Closures demo corpus (idempotent) +pnpm seed -- --force # Drop and re-seed +pnpm lint # ESLint +pnpm format # Prettier +pnpm drizzle-kit generate # Generate migration from schema changes +pnpm drizzle-kit migrate # Apply pending migrations +pnpm drizzle-kit studio # Drizzle Studio (DB browser) +``` + +--- + +## Architecture + +``` +Request path (fast, no generation): + GET /api/lessons → Redis cache → Postgres → LessonResponse + +Generation path (background, async): + POST /api/lessons cold-start → enqueue BullMQ job → generate-lesson-job + Blueprint promotion (T2 verify + variants) → promote-blueprint-job + +Grading (per response): + POST /api/grade → cheapGrader (rubric match) → escalate to strong grader if uncertain/novel + → persist response + grade → update mastery (BKT) → auto-flag collective failures + +LLM: + All calls via src/lib/llm/client.ts — never call providers directly + Prompts in src/lib/llm/prompts/ (versioned registry) + Contracts are Zod schemas in src/schemas/ (shared API ↔ LLM ↔ UI) +``` + +See [`docs/curio-specification.md`](docs/curio-specification.md) for full product + architecture reference. + +--- + +## Repo structure + +``` +src/ + app/ # Next.js App Router: pages + api/ route handlers + components/ # UI components (LessonReader, GradeDisplay, …) + lib/ + llm/ # LLM client + versioned prompt registry + generation/ # RAG-grounded content generation + verification/ # Content cascade (T0–T2) + response grader + memory/ # Mastery (BKT/Elo) + spaced repetition + intent/ # Intent normalization + blueprint lookup + db/ # Drizzle schema, queries, migrations, seed data + cache/ # Redis lesson buffer + jobs/ # BullMQ queue helpers + observability/ # Langfuse / OpenTelemetry tracing + schemas/ # Zod contracts (shared) + styles/ # Design tokens (CSS custom properties) +jobs/ # BullMQ workers (generation, promotion) +tests/golden/ # Frozen eval sets: grading accuracy + content verification +e2e/ # Playwright +docs/ + curio-specification.md +``` + +--- + +## Running workers + +Background jobs handle lesson generation and blueprint promotion (T2 verification + difficulty variant generation). Run separately from the web server: + +```bash +pnpm workers +``` + +Workers require Postgres, Redis, and API keys. In production, run as a separate process or container. + +--- + +## Tech stack + +Next.js 15 (App Router) · TypeScript · Vercel AI SDK · Zod · PostgreSQL 16 + pgvector · Drizzle ORM · Redis · BullMQ · Tailwind · Vitest · Playwright diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a58f1f7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + postgres: + image: pgvector/pgvector:pg16 + environment: + POSTGRES_DB: curio + POSTGRES_USER: curio + POSTGRES_PASSWORD: curio + ports: + - "5433:5432" + volumes: + - pgdata:/var/lib/postgresql/data + - ./docker/init.sql:/docker-entrypoint-initdb.d/01_init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U curio"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redisdata:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pgdata: + redisdata: diff --git a/docker/init.sql b/docker/init.sql new file mode 100644 index 0000000..31ea02b --- /dev/null +++ b/docker/init.sql @@ -0,0 +1,2 @@ +-- Enable pgvector extension (binary is pre-installed in pgvector/pgvector image) +CREATE EXTENSION IF NOT EXISTS vector; diff --git a/docs/curio-specification.md b/docs/curio-specification.md new file mode 100644 index 0000000..0b7f034 --- /dev/null +++ b/docs/curio-specification.md @@ -0,0 +1,381 @@ +# CURIO — PRODUCT & BUILD SPECIFICATION + +*An AI mastery tutor. Follow your curiosity.* + +--- + +## 1. OVERVIEW + +Curio is a curiosity-led learning platform where a learner states what they want to learn and receives a personalized **Journey** of short, bounded **Lessons**. Lessons are AI-generated, AI-verified, and — crucially — **AI-tutored**: the learner does not passively consume content, they *produce* (predict, explain, solve), and the AI diagnoses what they misunderstand and adapts. + +The product is built on one idea: **a generative model can do what a course or a feed cannot — respond to the individual learner.** Curio is organized entirely around that capability. + +--- + +## 2. CORE CONCEPT & DIFFERENTIATOR + +Curio is not a course catalog and not a content feed. Learning is **generated, not selected**, and **diagnosed, not just delivered**. + +The defensible capability — the moat — is **misconception diagnosis**: inferring *what* a learner misunderstands and *why* from what they actually say, and targeting exactly that. Generating explanations is now commodity; diagnosing a specific learner's wrong mental model and correcting it is not. **The product is built around the misconception model, not the content stream.** + +--- + +## 3. THE LEARNING UNIT: THE LESSON + +The atomic unit is a **Lesson**: a self-contained microlearning unit, completable in one sitting (~10 minutes), built from 2–4 **segments**. + +``` +Lesson (~10 min, bounded, one sitting) +├─ Segment 1: short readable explanation (~300–500 words, reads like an article) +│ └─ Checkpoint: learner produces (predict / explain-back / solve) +│ └─ AI diagnosis + targeted feedback (live) +├─ Segment 2: explanation → Checkpoint → diagnosis +└─ Segment 3: explanation → Checkpoint → diagnosis → Mastery gate +``` + +Design rules: +- **Reads like an article, not a quiz.** Explanation prose is continuous and calm; checkpoints *punctuate* (2–4 per Lesson) rather than interrupting every sentence. +- **Bounded.** Clear start and end, finishable in ~10 minutes. The session cap bounds learner attention *and* live token cost. +- **Active within the bound.** A passive and an active 10-minute session cost the same minutes; the active one retains far more. Microlearning brevity is preserved; passivity is removed. +- **Open-ended where possible.** Checkpoints favor free-text production over multiple choice — free text is what makes diagnosis possible. + +A **Journey** is an ordered sequence of Lessons for a topic, with adaptive branching driven by the memory engine. + +--- + +## 4. THE TUTORING LOOP + +Per checkpoint: + +1. **Read** — a short AI-generated explanation segment. +2. **Produce** — the learner predicts, explains in their own words, or solves a small problem. +3. **Diagnose** — the AI evaluates the *actual response* against a pre-verified rubric, reference answer, and per-concept misconception library; it names the specific misunderstanding. +4. **Adapt** — targeted feedback; re-explain differently if it didn't land; escalate difficulty if the learner is solid. +5. **Gate** — advancement is earned by demonstrated production, not by clicks. The outcome feeds the memory engine (§11). + +--- + +## 5. CURIOSITY RAILS + +The entry point is open: *"What do you want to learn today?"* Learners may pull tangents mid-Lesson ("wait, why does that happen?"). A tangent spawns a short sub-explanation and then **returns to the checkpoint**, so exploration never decays into aimless dabbling. Curiosity is encouraged; it is always brought back onto the attempt→diagnose loop. + +--- + +## 6. LEARNING MODES + +- **Journey mode** — structured progression through a topic's Lessons with adaptive branching and mastery tracking. +- **Daily Review** — surfaces concepts due for reinforcement from the spaced-repetition schedule. This is the substantive retention loop and the intended source of habit, in place of dopamine-driven gamification. + +--- + +## 7. DESIGN LANGUAGE & EXPERIENCE + +Curio should feel **modern, human, inspired, and beautiful** — not as decoration, but because the product is a reading-and-thinking environment, and the craft of that environment is part of the teaching. The interface should feel like a brilliant, warm tutor who has set your work down in front of them and is reading it carefully. + +### 7.1 Thesis + +The experience is **a calm reading surface that quietly comes alive when you respond.** Reading is unhurried and editorial; the moment of diagnosis is where the product breathes. Nothing competes with those two states. This is explicitly *not* a gamified, dashboard-dense, or chatbot-bubble app. + +### 7.2 Signature: marginalia + +The one memorable, bold element is the **diagnosis-as-marginalia** moment: when a learner submits a checkpoint response, the tutor's feedback arrives threaded against *their own words* — the way a great teacher annotates your answer in the margin, pointing at the exact phrase where the misconception lives (the `evidence_span` from §10.3). The feedback references what the learner actually wrote. Spend the design's boldness here; keep everything around it quiet. + +### 7.3 Direction (a starting point, not a mandate) + +Build by working the two-pass design process (plan a token system → critique it against this brief for genericness → then build). The following is the intended direction; refine it, don't default to it. Deliberately avoid the current AI-design clichés (cream + high-contrast serif + terracotta; near-black + acid accent; broadsheet hairline columns). + +- **Palette — "ink & light."** A soft, slightly cool paper (not cream) as the reading ground; an inky near-black for text; a considered deep indigo as the focus/brand accent (depth, study); a warm ochre reserved *only* for the human tutor moments (marginalia, encouragement). Diagnosis states are calm and legible, never an alarm: mastered = a quiet sage/teal, partial = ochre, misconception = a soft clay/rose that says *let's look again*, never buzzer-red. A genuinely comfortable dark mode (warm charcoal, not pure black). +- **Typography — reading is the product.** A humanist, low-contrast **reading serif** for body (e.g. Newsreader / Source Serif / Literata as candidates), set at a generous measure (~62–68ch) and line-height (~1.6); a characterful **display face used with restraint** for the prompt and headings; a humanist **mono** for mastery-map data and labels. Make the type a memorable part of the page, not a neutral delivery vehicle. +- **Motion — meaning, not decoration.** No animation library. Use CSS transitions and the **View Transitions API**. Reserve the one orchestrated motion moment for diagnosis: a brief "considering" beat, then feedback easing into the margin — it should feel like a person responding, not a form validating. Everything else is still. `prefers-reduced-motion` is honored everywhere. +- **Layout — one thing at a time.** Single reading column, generous margins, minimal chrome. The checkpoint is inset in the reading flow; feedback appears as marginalia, not a modal. The mastery map is a separate, richer view — it does not crowd the learn surface. + +### 7.4 Voice (copy is design material) + +Feedback copy *is* the tutor's character. Warm, specific, plain, and on the learner's side — names the exact thing that tripped them, never condescending, never gamified ("Not quite — the bit that slipped is *…*", never "❌ Wrong! −10 XP"). Active voice; actions say what they do. Empty and error states give direction in the interface's voice, not apologies. + +### 7.5 Quality floor (non-negotiable) + +Responsive to mobile; visible keyboard focus; reduced motion respected; WCAG-AA contrast on text and on every diagnosis-state color. Loading is part of the experience — the cold-start outline (§8.3) is a designed state, never a spinner. + +### 7.6 Anti-patterns + +No XP counters, badges, streak confetti, or leaderboards; no dense dashboard on the learn surface; no generic component-library default look; no chat-bubble UI; no autoplay or attention-grabbing motion. + +--- + +## 8. CONTENT ARCHITECTURE + +Curio is a **growing library of validated, generated content that gets personalized** — not a pure live-generation engine. The first learner to request a topic pays the generation cost; everyone after reuses validated content. This makes real-time UX, accuracy, and cost simultaneously feasible. + +### 8.1 Three tiers + +1. **Blueprint** — the canonical, fully verified Journey for a normalized intent: an ordered list of concepts with Lesson templates. Generated and verified **once**, reused by all learners. +2. **Variants** — pre-generated explanation/checkpoint bodies per concept at 2–3 difficulty levels, generated and verified at blueprint time. +3. **Live** — for long-tail intents with no blueprint hit: generated fresh through a lighter synchronous gate, then queued for full verification and **promoted into the blueprint cache** so the next learner gets a hit. + +Two cost classes follow: + +| Cached + verified once, reused by everyone | Live, per learner (pay tokens exactly here) | +|---|---| +| explanation segments | evaluation of the learner's free-text response | +| checkpoints + reference answers + rubrics | misconception diagnosis + targeted feedback | +| worked examples | adaptive re-explanation | +| per-concept misconception libraries | tangent handling | + +### 8.2 Intent normalization (drives cache hit rate) + +"Learn Python", "teach me python basics", and "i wanna learn python" must collapse to one key: +- **Structured key:** one cheap classification call → `{domain, subtopic, goal_type: skill|exam|concept, level_hint}` → canonical string. +- **Embedding fallback:** embed the raw intent; pgvector nearest-neighbor search against existing blueprint embeddings; reuse if cosine > threshold. +- Miss only when both fail. `goal_type` and `level_hint` are **parameters into** a blueprint, not separate blueprints. + +### 8.3 Serve flow (skeleton-first + buffer) + +Perceived latency is won by streaming *structure* before *content*: the Journey outline appears in under a second; the learner can't move faster than the first segment arrives. + +``` +on submit(intent): + key, ctx = normalize(intent) # ~100ms + bp = blueprint_cache.lookup(key, ctx) # exact key, then embedding NN + if bp: # WARM + lesson = hydrate(bp, next_concept, user) # variant SELECTION, ~150ms + buffer.push(session, lesson); stream(lesson) + else: # COLD + bp = gen_blueprint(intent, ctx) # structured call → concept list + stream(bp.outline) # outline visible now (designed state) + lesson = gen_lesson(bp, concept[0], rag) # light sync gate before serve + buffer.push(session, lesson); stream(lesson) + enqueue(promote_to_blueprint, bp) # full verification + cache, async + +on advance / buffer_low(session): + enqueue(generate_next, session, next_concept) # background job +``` + +The serve path reads from a Redis buffer fronting Postgres; a background job refills it. Concurrent generations per session are capped for backpressure. Postgres remains the source of truth — a Redis outage degrades latency, not availability. + +--- + +## 9. AI GENERATION PIPELINE + +Generation is **RAG-grounded** to constrain hallucination at the source. Per concept the generator produces, all grounded in retrieved trusted sources and stored with provenance (`source_chunk_ids`): + +- explanation segments +- checkpoints (prompts) + **reference answers** + **grading rubrics** +- worked examples +- a **misconception library**: likely wrong mental models for the concept, each with a tag and a diagnostic signature + +Rubrics and misconception libraries are first-class generated artifacts — they are what make live grading constrained and verifiable. All prompts live in a versioned registry; generation output is **structured (schema-constrained) JSON** via the model's native structured-output mode. + +--- + +## 10. AI VERIFICATION SYSTEM + +Curio verifies with AI on two surfaces. Both are made reliable by the same discipline: **ground the check against real artifacts, decorrelate from the generator, structure the output, and allow abstention.** + +### 10.1 Content verification — the cascade + +A naive "second LLM checks the card" shares the generator's blind spots and rubber-stamps confident-wrong content. Instead: + +- **Grounded, not free-judged.** The verifier checks each atomic claim against the retrieved source chunks ("is this supported by these sources?"), not against its own priors. +- **Independent solving.** The verifier never sees the marked answer; it **solves checkpoints/quizzes blind** (n-sample, majority vote) and compares to the reference answer. Mismatch → flag; sample disagreement → uncertainty. +- **Decorrelation.** A different model/provider and an adversarial prompt, so failure modes don't overlap the generator's. + +Run as a **cascade**, so expensive scrutiny is amortized: + +| Tier | Runs | Cost | Checks | +|---|---|---|---| +| **T0 Structural** | every item, always | ~free | schema valid; exactly one correct option; reference answer references a real option; refs resolve; non-empty; length bounds | +| **T1 Cheap-model** | every item at generation | low | claim grounding; blind solve; obvious safety. Clears the easy majority, else escalate | +| **T2 Adversarial** | blueprint promotion + escalations + low confidence | medium, **amortized once per blueprint** | full rubric, claim-by-claim grounding, n-sample blind solve, pedagogical soundness, cross-item consistency | +| **T3 Human** | top-traffic templates + persistent uncertainty | high, rare | spot review, golden-set labeling | + +Because content is cached at the template level, T2 runs **once per blueprint** and is reused by thousands of learners — near-zero per-serve cost. + +### 10.2 Response-grading verification + +The AI also grades free-text learner responses. Making this trustworthy: + +- **Grade against pre-verified artifacts.** At runtime the grader checks the response against the already-verified rubric + reference answer + misconception library — constrained, not freeform. +- **Misconception matching.** The grader maps the response to a known misconception tag or flags `novel` (→ escalate). Closed-set classification beats open-ended critique. +- **Two-sided error awareness.** A false-pass wastes learning; a **false-fail (telling a correct learner they're wrong) corrodes trust and is worse.** The grader is biased toward generous grading and always reveals its reasoning so the learner can contest. +- **Abstention + fallback.** The grader may return `uncertain` → reveal the model answer for self-assessment rather than confidently mis-grading. +- **Cascade.** A cheap model grades the rubric-matched majority; a strong model handles `novel`/`uncertain` only. + +### 10.3 Output contracts + +Content verifier: +```json +{ + "verdict": "pass | fail | uncertain", + "claims": [{"text": "", "status": "supported|unsupported|contradicted", + "evidence_chunk_ids": ["..."]}], + "solve_check": {"independent_answers": ["B","B","C"], "majority": "B", + "reference": "B", "agreement": 0.67, "status": "match|mismatch|ambiguous"}, + "pedagogy": {"teaches_concept": true, "prereqs_respected": true}, + "safety": {"in_scope": true, "mode_compliant": true}, + "confidence": 0.0 +} +``` + +Response grader: +```json +{ + "verdict": "mastered | partial | misconception | uncertain", + "rubric_hits": ["criterion_id"], + "misconception_tag": "tag | novel | none", + "evidence_span": "", + "feedback_directive": "advance | re-explain: | probe:", + "confidence": 0.0 +} +``` + +Contracts are defined as **Zod schemas** shared between the LLM layer, the API, and the frontend. + +### 10.4 Keeping verifiers honest + +- **Golden sets** of human-labeled cases for both surfaces; measure precision/recall continuously, tracking **false-fail rate** separately for grading. A model upgrade triggers an offline re-sweep of the cached library. +- **User-signal outer loop (free):** collective checkpoint failure (many learners miss the same item → suspect the reference answer), a report button, and dwell-time anomalies auto-trigger re-verification. +- **Versioning:** content keyed by `(intent_key, content_version, model_version)`; invalidation is lazy; a flagged item regenerates just itself. + +--- + +## 11. MEMORY ENGINE + +A **mastery-tracking engine**, not a knowledge graph: +- **Per-concept mastery score** updated from demonstrated production at checkpoints and from misconception tags, via Bayesian Knowledge Tracing or an Elo-style update. +- **Spaced-repetition schedule** per concept (SM-2-like) driving Daily Review. + +This drives difficulty scaling, review timing, reinforcement, and the mastery gate. A relational knowledge graph is deferred until there is data to justify it. + +--- + +## 12. PEDAGOGY + +The design implements the mechanics behind the **2-sigma** finding (1:1 tutoring + mastery learning outperforms classroom instruction by ~two standard deviations) — exactly what generative AI can deliver and a feed structurally cannot: +- **Active production / retrieval practice** at every checkpoint. +- **Mastery gating** before advancement. +- **Spaced repetition** via Daily Review. +- **Interleaving** of related concepts rather than blocking. + +--- + +## 13. PERSONALIZATION + +- **Variant selection** by mastery: pick the pre-generated difficulty variant matching the learner's score. Deterministic, sub-millisecond, zero live LLM cost. +- **Live personalization** is the feedback and re-explanation themselves — which is the core value, not overhead, and is where live tokens are deliberately spent. + +--- + +## 14. SAFETY & QUALITY CONTROL + +Three strictness modes — **open**, **controlled** (default), **enterprise-safe** — applied as parameters to generation, content verification, and response grading (including how off-topic or unsafe free-text input is handled). Filtered domains are enforced at intent normalization and at T0/T1. + +--- + +## 15. TELEMETRY & EVALUATION + +Because the product *is* diagnosis, instrumentation is a first-class requirement, not an afterthought: +- **Diagnosis telemetry:** every `response → grade` is logged; track false-fail rate, misconception-hit rate, and mastery gain per concept. You cannot improve a diagnosis engine you don't measure. +- **LLM observability:** per-call tracing of latency, tokens, and cost (Langfuse or OpenTelemetry). Without it the false-fail and cost metrics are aspirational. +- **Eval harness in CI:** golden sets for content verification and response grading run on every prompt change. Prompts are code — versioned and tested. The harness doubles as the build's definition of "good enough." +- **Cost/latency guards:** per-session token cap (the Lesson bound enforced in code), a provider circuit breaker, an **edge rate limiter on the cold-start generation endpoint** (it spends money on demand), and graceful degradation to abstain→reveal. + +--- + +## 16. FEATURE SET + +**Core (in the product):** intent entry; Journey mode; bounded Lessons with checkpoints; live diagnosis with abstention; mastery + spaced repetition; Daily Review; mastery map (learner-facing view of mastered / shaky / due); source citations (surfacing the provenance already stored). + +**Loop reinforcers (near-term):** confidence rating before reveal (catches *confidently wrong*); Socratic follow-up probe on a `partial` verdict. + +**Deferred:** learn from your own material (PDF/notes → personalized Journey — strong differentiator, leverages existing RAG infra, but a large surface); voice. + +**Explicitly out of scope:** XP / badges / leaderboards, infinite feed, social features, multi-provider fallback orchestration, knowledge graph. Microlearning + Daily Review + mastery map supply motivation intrinsically; bolt-on gamification risks reintroducing passive, low-retention "junk-food" learning. + +--- + +## 17. DATA MODEL + +``` +source_chunk(id, doc_ref, embedding, text) +blueprint(id, intent_key, embedding, title, model_version, content_version, + status[draft|verifying|published|flagged]) +concept(id, blueprint_id, ord, name, retrieval_ctx_ref) +lesson(id, blueprint_id, ord, est_minutes) +segment(id, lesson_id, ord, body_json, source_chunk_ids[], verify_status, content_version) +checkpoint(id, segment_id, prompt, kind[predict|explain|solve], + reference_answer, rubric_json, verify_status) +misconception(id, concept_id, tag, signature, verify_status) +verify_report(id, target_id, target_type, verdict, confidence, payload_json, model_version) + +user(id, ...) +journey_instance(id, user_id, blueprint_id, position) +mastery(user_id, concept_id, score, last_seen, next_review) +response(id, user_id, checkpoint_id, text, ts) +grade(id, response_id, verdict, misconception_tag, confidence, payload_json) + +job(id, type, idempotency_key, status, payload_json, attempts, created_at) -- durable ledger for paid background work + +-- Redis: buffer:{session} -> [hydrated lesson/segment ids]; generation-concurrency locks +``` + +--- + +## 18. TECH STACK + +**Single language, end to end (TypeScript).** One type system, one toolchain, and request/response/LLM contracts shared across the boundary. Nothing here requires a Python ML runtime — the work is hosted-LLM calls, embeddings, and pgvector queries, all first-class in TypeScript. + +- **App framework:** Next.js (App Router, TypeScript) full-stack — React Server Components for the reading surface, Route Handlers for the API, native streaming/SSE for skeleton-first delivery. Split out a thin service (e.g. Hono) only if the API outgrows Next. +- **LLM orchestration:** Vercel AI SDK — provider-agnostic, native streaming and **schema-constrained structured output** (Zod). Generator and grader configured to **different providers/models** (decorrelation). Exploit two cost levers: native structured output for verifier/grader contracts, and **prompt caching** for the large, session-reused grounding context (sources + rubric + misconception library). +- **Contracts/validation:** Zod, shared across API I/O, LLM outputs, and DB-derived types. +- **Database:** PostgreSQL + pgvector (single store, source of truth). +- **ORM & migrations:** Drizzle ORM + drizzle-kit (type-safe, SQL-first, clean pgvector support). +- **Cache / buffer:** Redis — a cache and serve-buffer in *front* of Postgres, never load-bearing for availability. +- **Background work:** for the vertical slice, Route Handlers + the `job` ledger table suffice. When scale work arrives: **BullMQ** (Redis-backed, TS), with **idempotency keys and durable Postgres job state** — these jobs spend money, so a retry must never regenerate-and-rebill. +- **Styling & motion:** Tailwind CSS + CSS custom properties for design tokens; motion via CSS transitions and the **View Transitions API** (no animation library). `prefers-reduced-motion` respected. +- **Observability:** Langfuse (or OpenTelemetry) for LLM call tracing; standard app metrics/logs. +- **Rate limiting:** edge limiter (e.g. Upstash Ratelimit on Redis) on the cold-start generation endpoint. +- **Testing:** Vitest (unit; LLM mocked) + a golden-eval runner (real-model, CI-gated) + Playwright for the core learn-flow e2e. +- **Hosting:** Next on Vercel (or Node host); managed Postgres+pgvector (Neon / Supabase / RDS) and managed Redis (Upstash). Keep the cold-start generation path on a runtime with adequate timeout (Node serverless/long-running, not edge). + +--- + +## 19. COST MODEL + +- **Scaffolding** (explanations, checkpoints, rubrics, misconception libraries) is generated and T2-verified **once per blueprint**, amortized to near-zero per serve. +- **Live cost** is grading + feedback turns: per Lesson ≈ 2–4 checkpoints × (grade + feedback) + occasional re-explanation/tangent. The ~10-minute Lesson bound **directly caps live turns per session** — microlearning bounds cost as well as attention. +- **Levers:** a grading **cascade** (cheap model for the rubric-matched majority, strong model only on `novel`/`uncertain`) and **prompt caching** of the reused grounding context keep per-turn cost low. +- Net: with a high cache-hit rate, live LLM spend concentrates on cold long-tail topics (throttleable) and on the irreplaceable act of responding to the learner. Model this against the chosen provider's pricing at assumed hit rates and checkpoint counts before building. + +--- + +## 20. MVP SCOPE & BUILD ROADMAP + +**The MVP answers one question:** *can an AI tutor diagnose what a learner misunderstands and fix it, well enough that they learn and return?* + +**MVP loop:** prompt → one Lesson (2–3 segments) → read + open-text checkpoints → live grading against verified rubric + misconception library → mastery gate. + +**Phases:** +- **P0 — Scaffold & infra.** Next.js + TypeScript + Tailwind app; docker-compose (Postgres+pgvector, Redis); Drizzle schema + migrations; Vitest; provider-agnostic LLM layer (Vercel AI SDK) with a mockable client; design tokens stubbed. +- **P1 — Vertical slice (single hardcoded topic).** Content schema + seeded corpus + pgvector retrieval → RAG-grounded Lesson generation (segments, checkpoints, reference answers, rubrics) → serve + render the reading surface and checkpoint → **response grading with abstention** → **golden eval harness gating false-fail rate in CI.** Build the marginalia feedback moment here — it is the core experience. This milestone validates diagnosis quality. +- **P2 — Diagnosis depth.** Misconception-library quality + the full content-verification cascade. +- **P3 — Adaptivity & scale.** Mastery + spaced repetition; blueprint cache, intent normalization, BullMQ workers with idempotent paid jobs. +- **P4 — Roadmap features**, one at a time: confidence rating, Socratic probe, Daily Review, mastery map, source citations, variant personalization, accounts. + +Anonymous-session-first; accounts arrive in P4. + +--- + +## 21. OPEN DECISIONS + +1. **Checkpoint count per Lesson** — fixed (e.g., 3) or adaptive to mastery? Sets the cost ceiling and the est-minutes contract. +2. **False-fail tolerance** — how generous is the grader, and when does it abstain vs. reveal? Sets the trust/rigor balance; prototype with real responses. *Settle before writing the grader.* +3. **Cold-start consistency** — may a cold long-tail Lesson be graded against a T1-only rubric (full T2 async), or does first serve block on T2? +4. **Concept granularity** — one concept per Lesson, or per Lesson-cluster? +5. **Source corpus policy** — define allowed, non-infringing sources before generation begins. + +--- + +## 22. NORTH STAR + +A learner types *"Teach me astrophysics."* A short, readable Lesson opens — finishable in ten minutes, set in calm, beautiful type. They read a few hundred words, then are asked to predict what happens to a collapsing star. They type a guess. The tutor reads it, pauses for a beat, and writes back in the margin — pointing at the exact phrase where their thinking went wrong, explaining the missing piece a different way, and asking again. They get it. Mastery updates; the next review is scheduled. Every explanation, problem, and rubric was generated and verified once and reused — only the part that read their actual answer ran live, which is the whole point. \ No newline at end of file diff --git a/docs/user-management-plan.md b/docs/user-management-plan.md new file mode 100644 index 0000000..c5a749a --- /dev/null +++ b/docs/user-management-plan.md @@ -0,0 +1,180 @@ +# User Management Plan (T1 + T2 + T3) + +## Context + +Curio is anonymous-session-first (sessionStorage UUID). Adding Auth.js v5 (NextAuth) with email/password + OAuth (Google, GitHub), anonymous→auth migration, profile/settings, and admin panel. `users` table exists (id, createdAt) — needs column additions + 3 new Auth.js tables. + +--- + +## Stack additions + +``` +next-auth@beta # Auth.js v5 +@auth/drizzle-adapter # Drizzle adapter for Auth.js +bcryptjs + @types/bcryptjs # Password hashing +nodemailer + @types/nodemailer # Email verification +``` + +--- + +## Phase 1 — Schema migration + +New migration via drizzle-kit generate → `drizzle/migrations/0004_auth.sql` + +**Alter `user` table** — add columns: +- `email` varchar(255) unique nullable (nullable: existing anonymous rows have none) +- `email_verified` timestamp nullable +- `name` varchar(255) nullable +- `image` text nullable +- `role` varchar(20) not null default `'learner'` — values: `learner | admin | suspended` +- `password_hash` text nullable (null for OAuth users) + +**New tables** (Auth.js Drizzle adapter required): +- `account` — OAuth provider links (userId FK → user.id, provider, providerAccountId, etc.) +- `session` — DB sessions (sessionToken PK, userId FK, expires) +- `verification_token` — email verification (identifier + token composite PK, expires) + +Schema file: `src/lib/db/schema.ts` — add 3 new tables, extend users inline. + +--- + +## Phase 2 — Auth.js core config + +**`src/auth.ts`** — single source of truth: +- Adapter: `DrizzleAdapter(db, { usersTable: users, ... })` +- Session strategy: `database` (allows session revocation for admin ban) +- Providers: `Credentials` (email+bcrypt), `Google`, `GitHub` +- Callbacks: `session` + `jwt` embed `user.id` + `user.role` +- Pages: `signIn: '/auth/login'`, `error: '/auth/error'` +- Exports: `{ handlers, auth, signIn, signOut }` + +**`src/app/api/auth/[...nextauth]/route.ts`** — `export const { GET, POST } = handlers` + +**`src/lib/auth-helpers.ts`** — server utilities: +- `requireAuth()` — redirects to `/auth/login` if no session +- `requireAdmin()` — requireAuth + role check +- `getOptionalSession()` — returns session or null + +--- + +## Phase 3 — Middleware + +**`src/middleware.ts`**: +- Protected prefixes: `/profile`, `/settings`, `/admin` +- Redirect unauthenticated → `/auth/login?callbackUrl=...` +- `/admin` prefix: redirect non-admin → `/` +- All other paths pass through + +--- + +## Phase 4 — Auth UI pages + +`src/app/auth/login/page.tsx` — email+password form + Google/GitHub buttons + link to register + +`src/app/auth/register/page.tsx` — email + name + password + confirm; POST `/api/auth/register`; then signIn + migrate anonymous session + +`src/app/auth/verify-email/page.tsx` — "check inbox" + token handler + +`src/app/auth/error/page.tsx` — maps Auth.js error codes to readable messages + +Design: ink & light tokens, no card shadows, no rounded-xl. Labels above inputs. + +--- + +## Phase 5 — Anonymous session migration + +**`src/app/api/auth/migrate-session/route.ts`** — POST `{ anonUserId }`: +- Auth-gated (requires valid session) +- Transfers all rows: `responses`, `mastery`, `journey_instance` — UPDATE user_id = auth WHERE user_id = anon +- Renames Redis budget key `budget:{anon}` → `budget:{auth}` +- Idempotent + +**Client side**: after `signIn` resolves, read `sessionStorage.getItem('curio_uid')`, POST migrate, clear key. + +--- + +## Phase 6 — Update existing API routes + +All 12 routes: prefer server session, fall back to client-supplied userId (keeps anonymous flow): + +```typescript +const session = await getOptionalSession(); +const userId = session?.user?.id ?? body.userId ?? searchParams.get('userId'); +``` + +Routes with body userId: `/api/grade`, `/api/advance`, `/api/explain`, `/api/tangent`, `/api/socratic` +Routes with query userId: `/api/mastery`, `/api/review` + +--- + +## Phase 7 — Learner account pages (T2) + +**`src/app/profile/page.tsx`** (RSC, protected) — name, email, joined date; mastery overview per blueprint + +**`src/app/settings/page.tsx`** (RSC, protected) — Profile section (name, avatar), Security (change password, linked OAuth accounts), Danger (delete account) + +New API routes (all auth-gated): +- `PATCH /api/user/profile` — update name/image +- `POST /api/user/change-password` — verify old bcrypt → hash new +- `DELETE /api/user/account` — delete + cascade + +--- + +## Phase 8 — Admin panel (T3) + +All protected by `requireAdmin()`. + +**`src/app/admin/layout.tsx`** — sidebar: Users | Reports | Blueprints + +**`src/app/admin/page.tsx`** — stats: total users, active today +→ `GET /api/admin/stats` + +**`src/app/admin/users/page.tsx`** — paginated user list; actions: promote to admin, suspend, delete +→ `GET /api/admin/users`, `PATCH /api/admin/users/[id]`, `DELETE /api/admin/users/[id]` + +**`src/app/admin/reports/page.tsx`** — content_report triage; actions: dismiss, flag for re-verify +→ `GET /api/admin/reports`, `PATCH /api/admin/reports/[id]` + +**`src/app/admin/blueprints/page.tsx`** — blueprint status management +→ `GET /api/admin/blueprints`, `PATCH /api/admin/blueprints/[id]` + +--- + +## New env vars + +``` +AUTH_SECRET= # openssl rand -hex 32 +AUTH_URL=http://localhost:3000 +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= +AUTH_GITHUB_ID= +AUTH_GITHUB_SECRET= +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= +EMAIL_FROM=noreply@yourdomain.com +``` + +--- + +## Files created / modified (summary) + +**New:** `src/auth.ts`, `src/middleware.ts`, `src/lib/auth-helpers.ts`, `src/app/api/auth/[...nextauth]/route.ts`, `src/app/api/auth/register/route.ts`, `src/app/api/auth/migrate-session/route.ts`, `src/app/auth/login|register|verify-email|error/page.tsx`, `src/app/profile/page.tsx`, `src/app/settings/page.tsx`, `src/app/api/user/profile|change-password|account/route.ts`, `src/app/admin/layout|page.tsx`, `src/app/admin/users|reports|blueprints/page.tsx`, `src/app/api/admin/stats|users/[id]|reports/[id]|blueprints/[id]/route.ts` + +**Modified:** `src/lib/db/schema.ts` (extend users + 3 tables), all 12 existing API routes (userId fallback), `src/app/page.tsx` (nav link), `src/app/layout.tsx` (SessionProvider), `.env.example` + +--- + +## Verification + +```bash +pnpm drizzle-kit generate && pnpm drizzle-kit migrate # 0004_auth applies clean +pnpm dev +curl http://localhost:3000/api/auth/providers # returns Google, GitHub, credentials +# Register → lesson → grade works (anonymous flow intact) +# Register → verify email → profile shows mastery +# Set role='admin' in DB → /admin accessible +# Non-admin → /admin redirects to / +pnpm test # existing suite passes (userId fallback preserves behavior) +``` diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..1ff8c90 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/lib/db/schema.ts', + out: './drizzle/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL ?? 'postgresql://curio:curio@localhost:5433/curio', + }, +}); diff --git a/drizzle/migrations/0000_overrated_liz_osborn.sql b/drizzle/migrations/0000_overrated_liz_osborn.sql new file mode 100644 index 0000000..e71315b --- /dev/null +++ b/drizzle/migrations/0000_overrated_liz_osborn.sql @@ -0,0 +1,140 @@ +CREATE TYPE "public"."blueprint_status" AS ENUM('draft', 'verifying', 'published', 'flagged');--> statement-breakpoint +CREATE TYPE "public"."checkpoint_kind" AS ENUM('predict', 'explain', 'solve');--> statement-breakpoint +CREATE TYPE "public"."grade_verdict" AS ENUM('mastered', 'partial', 'misconception', 'uncertain');--> statement-breakpoint +CREATE TYPE "public"."job_status" AS ENUM('pending', 'running', 'done', 'failed');--> statement-breakpoint +CREATE TYPE "public"."verify_status" AS ENUM('pending', 'pass', 'fail', 'uncertain');--> statement-breakpoint +CREATE TYPE "public"."verify_verdict" AS ENUM('pass', 'fail', 'uncertain');--> statement-breakpoint +CREATE TABLE "blueprint" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "intent_key" text NOT NULL, + "embedding" vector(1536), + "title" text NOT NULL, + "model_version" text NOT NULL, + "content_version" integer DEFAULT 1 NOT NULL, + "status" "blueprint_status" DEFAULT 'draft' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "blueprint_intent_key_unique" UNIQUE("intent_key") +); +--> statement-breakpoint +CREATE TABLE "checkpoint" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "segment_id" uuid NOT NULL, + "prompt" text NOT NULL, + "kind" "checkpoint_kind" NOT NULL, + "reference_answer" text NOT NULL, + "rubric_json" jsonb NOT NULL, + "verify_status" "verify_status" DEFAULT 'pending' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "concept" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "blueprint_id" uuid NOT NULL, + "ord" integer NOT NULL, + "name" text NOT NULL, + "retrieval_ctx_ref" text +); +--> statement-breakpoint +CREATE TABLE "grade" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "response_id" uuid NOT NULL, + "verdict" "grade_verdict" NOT NULL, + "misconception_tag" text, + "confidence" real NOT NULL, + "payload_json" jsonb NOT NULL +); +--> statement-breakpoint +CREATE TABLE "job" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "type" text NOT NULL, + "idempotency_key" text NOT NULL, + "status" "job_status" DEFAULT 'pending' NOT NULL, + "payload_json" jsonb NOT NULL, + "attempts" integer DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "job_idempotency_key_unique" UNIQUE("idempotency_key") +); +--> statement-breakpoint +CREATE TABLE "journey_instance" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "blueprint_id" uuid NOT NULL, + "position" integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE "lesson" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "blueprint_id" uuid NOT NULL, + "ord" integer NOT NULL, + "est_minutes" integer DEFAULT 10 NOT NULL +); +--> statement-breakpoint +CREATE TABLE "mastery" ( + "user_id" uuid NOT NULL, + "concept_id" uuid NOT NULL, + "score" real DEFAULT 0.5 NOT NULL, + "last_seen" timestamp DEFAULT now() NOT NULL, + "next_review" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "mastery_user_id_concept_id_pk" PRIMARY KEY("user_id","concept_id") +); +--> statement-breakpoint +CREATE TABLE "misconception" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "concept_id" uuid NOT NULL, + "tag" text NOT NULL, + "signature" text NOT NULL, + "verify_status" "verify_status" DEFAULT 'pending' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "response" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "checkpoint_id" uuid NOT NULL, + "text" text NOT NULL, + "ts" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "segment" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "lesson_id" uuid NOT NULL, + "ord" integer NOT NULL, + "body_json" jsonb NOT NULL, + "source_chunk_ids" uuid[] DEFAULT '{}' NOT NULL, + "verify_status" "verify_status" DEFAULT 'pending' NOT NULL, + "content_version" integer DEFAULT 1 NOT NULL +); +--> statement-breakpoint +CREATE TABLE "source_chunk" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "doc_ref" text NOT NULL, + "embedding" vector(1536), + "text" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "verify_report" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "target_id" uuid NOT NULL, + "target_type" text NOT NULL, + "verdict" "verify_verdict" NOT NULL, + "confidence" real NOT NULL, + "payload_json" jsonb NOT NULL, + "model_version" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "checkpoint" ADD CONSTRAINT "checkpoint_segment_id_segment_id_fk" FOREIGN KEY ("segment_id") REFERENCES "public"."segment"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "concept" ADD CONSTRAINT "concept_blueprint_id_blueprint_id_fk" FOREIGN KEY ("blueprint_id") REFERENCES "public"."blueprint"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "grade" ADD CONSTRAINT "grade_response_id_response_id_fk" FOREIGN KEY ("response_id") REFERENCES "public"."response"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "journey_instance" ADD CONSTRAINT "journey_instance_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "journey_instance" ADD CONSTRAINT "journey_instance_blueprint_id_blueprint_id_fk" FOREIGN KEY ("blueprint_id") REFERENCES "public"."blueprint"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lesson" ADD CONSTRAINT "lesson_blueprint_id_blueprint_id_fk" FOREIGN KEY ("blueprint_id") REFERENCES "public"."blueprint"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mastery" ADD CONSTRAINT "mastery_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mastery" ADD CONSTRAINT "mastery_concept_id_concept_id_fk" FOREIGN KEY ("concept_id") REFERENCES "public"."concept"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "misconception" ADD CONSTRAINT "misconception_concept_id_concept_id_fk" FOREIGN KEY ("concept_id") REFERENCES "public"."concept"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "response" ADD CONSTRAINT "response_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "response" ADD CONSTRAINT "response_checkpoint_id_checkpoint_id_fk" FOREIGN KEY ("checkpoint_id") REFERENCES "public"."checkpoint"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "segment" ADD CONSTRAINT "segment_lesson_id_lesson_id_fk" FOREIGN KEY ("lesson_id") REFERENCES "public"."lesson"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/migrations/0001_marvelous_pepper_potts.sql b/drizzle/migrations/0001_marvelous_pepper_potts.sql new file mode 100644 index 0000000..3d28f15 --- /dev/null +++ b/drizzle/migrations/0001_marvelous_pepper_potts.sql @@ -0,0 +1,2 @@ +ALTER TABLE "misconception" ADD COLUMN "description" text;--> statement-breakpoint +ALTER TABLE "misconception" ADD COLUMN "correction" text; \ No newline at end of file diff --git a/drizzle/migrations/0002_response_confidence.sql b/drizzle/migrations/0002_response_confidence.sql new file mode 100644 index 0000000..96948e2 --- /dev/null +++ b/drizzle/migrations/0002_response_confidence.sql @@ -0,0 +1 @@ +ALTER TABLE "response" ADD COLUMN "self_confidence" smallint; \ No newline at end of file diff --git a/drizzle/migrations/0003_parched_wither.sql b/drizzle/migrations/0003_parched_wither.sql new file mode 100644 index 0000000..711bb71 --- /dev/null +++ b/drizzle/migrations/0003_parched_wither.sql @@ -0,0 +1,26 @@ +CREATE TYPE "public"."content_report_reason" AS ENUM('grade_wrong', 'content_error', 'other');--> statement-breakpoint +CREATE TABLE "content_report" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "checkpoint_id" uuid NOT NULL, + "grade_id" uuid, + "reason" "content_report_reason" NOT NULL, + "notes" text, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "novel_misconduct_queue" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "grade_id" uuid NOT NULL, + "checkpoint_id" uuid NOT NULL, + "response_text" text NOT NULL, + "evidence_span" text NOT NULL, + "processed" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "checkpoint" ADD COLUMN "content_version" integer DEFAULT 1 NOT NULL;--> statement-breakpoint +ALTER TABLE "segment" ADD COLUMN "difficulty_level" integer DEFAULT 1 NOT NULL;--> statement-breakpoint +ALTER TABLE "content_report" ADD CONSTRAINT "content_report_checkpoint_id_checkpoint_id_fk" FOREIGN KEY ("checkpoint_id") REFERENCES "public"."checkpoint"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "content_report" ADD CONSTRAINT "content_report_grade_id_grade_id_fk" FOREIGN KEY ("grade_id") REFERENCES "public"."grade"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "novel_misconduct_queue" ADD CONSTRAINT "novel_misconduct_queue_grade_id_grade_id_fk" FOREIGN KEY ("grade_id") REFERENCES "public"."grade"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "novel_misconduct_queue" ADD CONSTRAINT "novel_misconduct_queue_checkpoint_id_checkpoint_id_fk" FOREIGN KEY ("checkpoint_id") REFERENCES "public"."checkpoint"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/migrations/0004_auth.sql b/drizzle/migrations/0004_auth.sql new file mode 100644 index 0000000..6f7daf2 --- /dev/null +++ b/drizzle/migrations/0004_auth.sql @@ -0,0 +1,38 @@ +CREATE TYPE "public"."user_role" AS ENUM('learner', 'admin', 'suspended');--> statement-breakpoint +CREATE TABLE "account" ( + "user_id" uuid NOT NULL, + "type" text NOT NULL, + "provider" text NOT NULL, + "provider_account_id" text NOT NULL, + "refresh_token" text, + "access_token" text, + "expires_at" integer, + "token_type" text, + "scope" text, + "id_token" text, + "session_state" text, + CONSTRAINT "account_provider_provider_account_id_pk" PRIMARY KEY("provider","provider_account_id") +); +--> statement-breakpoint +CREATE TABLE "session" ( + "session_token" text PRIMARY KEY NOT NULL, + "user_id" uuid NOT NULL, + "expires" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "verification_token" ( + "identifier" text NOT NULL, + "token" text NOT NULL, + "expires" timestamp NOT NULL, + CONSTRAINT "verification_token_identifier_token_pk" PRIMARY KEY("identifier","token") +); +--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "email" varchar(255);--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "email_verified" timestamp;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "name" varchar(255);--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "image" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "role" "user_role" DEFAULT 'learner' NOT NULL;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "password_hash" text;--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user" ADD CONSTRAINT "user_email_unique" UNIQUE("email"); \ No newline at end of file diff --git a/drizzle/migrations/0005_site_email.sql b/drizzle/migrations/0005_site_email.sql new file mode 100644 index 0000000..d2aea17 --- /dev/null +++ b/drizzle/migrations/0005_site_email.sql @@ -0,0 +1,13 @@ +CREATE TABLE "password_reset_token" ( + "token" text PRIMARY KEY NOT NULL, + "user_id" uuid NOT NULL, + "expires" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "site_config" ( + "key" varchar(100) PRIMARY KEY NOT NULL, + "value" text NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "password_reset_token" ADD CONSTRAINT "password_reset_token_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..3f96c17 --- /dev/null +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -0,0 +1,951 @@ +{ + "id": "99287306-1d2d-4763-a7fa-4767e0247728", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.blueprint": { + "name": "blueprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "intent_key": { + "name": "intent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "blueprint_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blueprint_intent_key_unique": { + "name": "blueprint_intent_key_unique", + "nullsNotDistinct": false, + "columns": [ + "intent_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkpoint": { + "name": "checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "segment_id": { + "name": "segment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "checkpoint_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_answer": { + "name": "reference_answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rubric_json": { + "name": "rubric_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "checkpoint_segment_id_segment_id_fk": { + "name": "checkpoint_segment_id_segment_id_fk", + "tableFrom": "checkpoint", + "tableTo": "segment", + "columnsFrom": [ + "segment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.concept": { + "name": "concept", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieval_ctx_ref": { + "name": "retrieval_ctx_ref", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "concept_blueprint_id_blueprint_id_fk": { + "name": "concept_blueprint_id_blueprint_id_fk", + "tableFrom": "concept", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grade": { + "name": "grade", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "response_id": { + "name": "response_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "grade_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "misconception_tag": { + "name": "misconception_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "grade_response_id_response_id_fk": { + "name": "grade_response_id_response_id_fk", + "tableFrom": "grade", + "tableTo": "response", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "job_idempotency_key_unique": { + "name": "job_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journey_instance": { + "name": "journey_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "journey_instance_user_id_user_id_fk": { + "name": "journey_instance_user_id_user_id_fk", + "tableFrom": "journey_instance", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journey_instance_blueprint_id_blueprint_id_fk": { + "name": "journey_instance_blueprint_id_blueprint_id_fk", + "tableFrom": "journey_instance", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lesson": { + "name": "lesson", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "est_minutes": { + "name": "est_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "lesson_blueprint_id_blueprint_id_fk": { + "name": "lesson_blueprint_id_blueprint_id_fk", + "tableFrom": "lesson", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mastery": { + "name": "mastery", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0.5 + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_review": { + "name": "next_review", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mastery_user_id_user_id_fk": { + "name": "mastery_user_id_user_id_fk", + "tableFrom": "mastery", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mastery_concept_id_concept_id_fk": { + "name": "mastery_concept_id_concept_id_fk", + "tableFrom": "mastery", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mastery_user_id_concept_id_pk": { + "name": "mastery_user_id_concept_id_pk", + "columns": [ + "user_id", + "concept_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.misconception": { + "name": "misconception", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "misconception_concept_id_concept_id_fk": { + "name": "misconception_concept_id_concept_id_fk", + "tableFrom": "misconception", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.response": { + "name": "response", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "response_user_id_user_id_fk": { + "name": "response_user_id_user_id_fk", + "tableFrom": "response", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "response_checkpoint_id_checkpoint_id_fk": { + "name": "response_checkpoint_id_checkpoint_id_fk", + "tableFrom": "response", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.segment": { + "name": "segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body_json": { + "name": "body_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_chunk_ids": { + "name": "source_chunk_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "segment_lesson_id_lesson_id_fk": { + "name": "segment_lesson_id_lesson_id_fk", + "tableFrom": "segment", + "tableTo": "lesson", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source_chunk": { + "name": "source_chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "doc_ref": { + "name": "doc_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_report": { + "name": "verify_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "verify_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.blueprint_status": { + "name": "blueprint_status", + "schema": "public", + "values": [ + "draft", + "verifying", + "published", + "flagged" + ] + }, + "public.checkpoint_kind": { + "name": "checkpoint_kind", + "schema": "public", + "values": [ + "predict", + "explain", + "solve" + ] + }, + "public.grade_verdict": { + "name": "grade_verdict", + "schema": "public", + "values": [ + "mastered", + "partial", + "misconception", + "uncertain" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "pending", + "running", + "done", + "failed" + ] + }, + "public.verify_status": { + "name": "verify_status", + "schema": "public", + "values": [ + "pending", + "pass", + "fail", + "uncertain" + ] + }, + "public.verify_verdict": { + "name": "verify_verdict", + "schema": "public", + "values": [ + "pass", + "fail", + "uncertain" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..6aad443 --- /dev/null +++ b/drizzle/migrations/meta/0001_snapshot.json @@ -0,0 +1,963 @@ +{ + "id": "c96c484c-a142-4f76-a53e-f99a3f8688b2", + "prevId": "99287306-1d2d-4763-a7fa-4767e0247728", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.blueprint": { + "name": "blueprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "intent_key": { + "name": "intent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "blueprint_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blueprint_intent_key_unique": { + "name": "blueprint_intent_key_unique", + "nullsNotDistinct": false, + "columns": [ + "intent_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkpoint": { + "name": "checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "segment_id": { + "name": "segment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "checkpoint_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_answer": { + "name": "reference_answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rubric_json": { + "name": "rubric_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "checkpoint_segment_id_segment_id_fk": { + "name": "checkpoint_segment_id_segment_id_fk", + "tableFrom": "checkpoint", + "tableTo": "segment", + "columnsFrom": [ + "segment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.concept": { + "name": "concept", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieval_ctx_ref": { + "name": "retrieval_ctx_ref", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "concept_blueprint_id_blueprint_id_fk": { + "name": "concept_blueprint_id_blueprint_id_fk", + "tableFrom": "concept", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grade": { + "name": "grade", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "response_id": { + "name": "response_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "grade_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "misconception_tag": { + "name": "misconception_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "grade_response_id_response_id_fk": { + "name": "grade_response_id_response_id_fk", + "tableFrom": "grade", + "tableTo": "response", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "job_idempotency_key_unique": { + "name": "job_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journey_instance": { + "name": "journey_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "journey_instance_user_id_user_id_fk": { + "name": "journey_instance_user_id_user_id_fk", + "tableFrom": "journey_instance", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journey_instance_blueprint_id_blueprint_id_fk": { + "name": "journey_instance_blueprint_id_blueprint_id_fk", + "tableFrom": "journey_instance", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lesson": { + "name": "lesson", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "est_minutes": { + "name": "est_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "lesson_blueprint_id_blueprint_id_fk": { + "name": "lesson_blueprint_id_blueprint_id_fk", + "tableFrom": "lesson", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mastery": { + "name": "mastery", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0.5 + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_review": { + "name": "next_review", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mastery_user_id_user_id_fk": { + "name": "mastery_user_id_user_id_fk", + "tableFrom": "mastery", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mastery_concept_id_concept_id_fk": { + "name": "mastery_concept_id_concept_id_fk", + "tableFrom": "mastery", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mastery_user_id_concept_id_pk": { + "name": "mastery_user_id_concept_id_pk", + "columns": [ + "user_id", + "concept_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.misconception": { + "name": "misconception", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correction": { + "name": "correction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "misconception_concept_id_concept_id_fk": { + "name": "misconception_concept_id_concept_id_fk", + "tableFrom": "misconception", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.response": { + "name": "response", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "response_user_id_user_id_fk": { + "name": "response_user_id_user_id_fk", + "tableFrom": "response", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "response_checkpoint_id_checkpoint_id_fk": { + "name": "response_checkpoint_id_checkpoint_id_fk", + "tableFrom": "response", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.segment": { + "name": "segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body_json": { + "name": "body_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_chunk_ids": { + "name": "source_chunk_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "segment_lesson_id_lesson_id_fk": { + "name": "segment_lesson_id_lesson_id_fk", + "tableFrom": "segment", + "tableTo": "lesson", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source_chunk": { + "name": "source_chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "doc_ref": { + "name": "doc_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_report": { + "name": "verify_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "verify_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.blueprint_status": { + "name": "blueprint_status", + "schema": "public", + "values": [ + "draft", + "verifying", + "published", + "flagged" + ] + }, + "public.checkpoint_kind": { + "name": "checkpoint_kind", + "schema": "public", + "values": [ + "predict", + "explain", + "solve" + ] + }, + "public.grade_verdict": { + "name": "grade_verdict", + "schema": "public", + "values": [ + "mastered", + "partial", + "misconception", + "uncertain" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "pending", + "running", + "done", + "failed" + ] + }, + "public.verify_status": { + "name": "verify_status", + "schema": "public", + "values": [ + "pending", + "pass", + "fail", + "uncertain" + ] + }, + "public.verify_verdict": { + "name": "verify_verdict", + "schema": "public", + "values": [ + "pass", + "fail", + "uncertain" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0003_snapshot.json b/drizzle/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..b6b573c --- /dev/null +++ b/drizzle/migrations/meta/0003_snapshot.json @@ -0,0 +1,1156 @@ +{ + "id": "0dfa4f86-3983-4191-a496-575390f05802", + "prevId": "c96c484c-a142-4f76-a53e-f99a3f8688b2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.blueprint": { + "name": "blueprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "intent_key": { + "name": "intent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "blueprint_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blueprint_intent_key_unique": { + "name": "blueprint_intent_key_unique", + "nullsNotDistinct": false, + "columns": [ + "intent_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkpoint": { + "name": "checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "segment_id": { + "name": "segment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "checkpoint_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_answer": { + "name": "reference_answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rubric_json": { + "name": "rubric_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "checkpoint_segment_id_segment_id_fk": { + "name": "checkpoint_segment_id_segment_id_fk", + "tableFrom": "checkpoint", + "tableTo": "segment", + "columnsFrom": [ + "segment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.concept": { + "name": "concept", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieval_ctx_ref": { + "name": "retrieval_ctx_ref", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "concept_blueprint_id_blueprint_id_fk": { + "name": "concept_blueprint_id_blueprint_id_fk", + "tableFrom": "concept", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_report": { + "name": "content_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "grade_id": { + "name": "grade_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "content_report_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "content_report_checkpoint_id_checkpoint_id_fk": { + "name": "content_report_checkpoint_id_checkpoint_id_fk", + "tableFrom": "content_report", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "content_report_grade_id_grade_id_fk": { + "name": "content_report_grade_id_grade_id_fk", + "tableFrom": "content_report", + "tableTo": "grade", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grade": { + "name": "grade", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "response_id": { + "name": "response_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "grade_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "misconception_tag": { + "name": "misconception_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "grade_response_id_response_id_fk": { + "name": "grade_response_id_response_id_fk", + "tableFrom": "grade", + "tableTo": "response", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "job_idempotency_key_unique": { + "name": "job_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journey_instance": { + "name": "journey_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "journey_instance_user_id_user_id_fk": { + "name": "journey_instance_user_id_user_id_fk", + "tableFrom": "journey_instance", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journey_instance_blueprint_id_blueprint_id_fk": { + "name": "journey_instance_blueprint_id_blueprint_id_fk", + "tableFrom": "journey_instance", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lesson": { + "name": "lesson", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "est_minutes": { + "name": "est_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "lesson_blueprint_id_blueprint_id_fk": { + "name": "lesson_blueprint_id_blueprint_id_fk", + "tableFrom": "lesson", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mastery": { + "name": "mastery", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0.5 + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_review": { + "name": "next_review", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mastery_user_id_user_id_fk": { + "name": "mastery_user_id_user_id_fk", + "tableFrom": "mastery", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mastery_concept_id_concept_id_fk": { + "name": "mastery_concept_id_concept_id_fk", + "tableFrom": "mastery", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mastery_user_id_concept_id_pk": { + "name": "mastery_user_id_concept_id_pk", + "columns": [ + "user_id", + "concept_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.misconception": { + "name": "misconception", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correction": { + "name": "correction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "misconception_concept_id_concept_id_fk": { + "name": "misconception_concept_id_concept_id_fk", + "tableFrom": "misconception", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.novel_misconduct_queue": { + "name": "novel_misconduct_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "grade_id": { + "name": "grade_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "response_text": { + "name": "response_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_span": { + "name": "evidence_span", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "novel_misconduct_queue_grade_id_grade_id_fk": { + "name": "novel_misconduct_queue_grade_id_grade_id_fk", + "tableFrom": "novel_misconduct_queue", + "tableTo": "grade", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "novel_misconduct_queue_checkpoint_id_checkpoint_id_fk": { + "name": "novel_misconduct_queue_checkpoint_id_checkpoint_id_fk", + "tableFrom": "novel_misconduct_queue", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.response": { + "name": "response", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "self_confidence": { + "name": "self_confidence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ts": { + "name": "ts", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "response_user_id_user_id_fk": { + "name": "response_user_id_user_id_fk", + "tableFrom": "response", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "response_checkpoint_id_checkpoint_id_fk": { + "name": "response_checkpoint_id_checkpoint_id_fk", + "tableFrom": "response", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.segment": { + "name": "segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body_json": { + "name": "body_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_chunk_ids": { + "name": "source_chunk_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "difficulty_level": { + "name": "difficulty_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "segment_lesson_id_lesson_id_fk": { + "name": "segment_lesson_id_lesson_id_fk", + "tableFrom": "segment", + "tableTo": "lesson", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source_chunk": { + "name": "source_chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "doc_ref": { + "name": "doc_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_report": { + "name": "verify_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "verify_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.blueprint_status": { + "name": "blueprint_status", + "schema": "public", + "values": [ + "draft", + "verifying", + "published", + "flagged" + ] + }, + "public.checkpoint_kind": { + "name": "checkpoint_kind", + "schema": "public", + "values": [ + "predict", + "explain", + "solve" + ] + }, + "public.content_report_reason": { + "name": "content_report_reason", + "schema": "public", + "values": [ + "grade_wrong", + "content_error", + "other" + ] + }, + "public.grade_verdict": { + "name": "grade_verdict", + "schema": "public", + "values": [ + "mastered", + "partial", + "misconception", + "uncertain" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "pending", + "running", + "done", + "failed" + ] + }, + "public.verify_status": { + "name": "verify_status", + "schema": "public", + "values": [ + "pending", + "pass", + "fail", + "uncertain" + ] + }, + "public.verify_verdict": { + "name": "verify_verdict", + "schema": "public", + "values": [ + "pass", + "fail", + "uncertain" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0004_snapshot.json b/drizzle/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..c902484 --- /dev/null +++ b/drizzle/migrations/meta/0004_snapshot.json @@ -0,0 +1,1396 @@ +{ + "id": "43cc9612-0f99-4bcf-b520-fe15e9471cc6", + "prevId": "0dfa4f86-3983-4191-a496-575390f05802", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blueprint": { + "name": "blueprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "intent_key": { + "name": "intent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "blueprint_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blueprint_intent_key_unique": { + "name": "blueprint_intent_key_unique", + "nullsNotDistinct": false, + "columns": [ + "intent_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkpoint": { + "name": "checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "segment_id": { + "name": "segment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "checkpoint_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_answer": { + "name": "reference_answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rubric_json": { + "name": "rubric_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "checkpoint_segment_id_segment_id_fk": { + "name": "checkpoint_segment_id_segment_id_fk", + "tableFrom": "checkpoint", + "tableTo": "segment", + "columnsFrom": [ + "segment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.concept": { + "name": "concept", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieval_ctx_ref": { + "name": "retrieval_ctx_ref", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "concept_blueprint_id_blueprint_id_fk": { + "name": "concept_blueprint_id_blueprint_id_fk", + "tableFrom": "concept", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_report": { + "name": "content_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "grade_id": { + "name": "grade_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "content_report_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "content_report_checkpoint_id_checkpoint_id_fk": { + "name": "content_report_checkpoint_id_checkpoint_id_fk", + "tableFrom": "content_report", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "content_report_grade_id_grade_id_fk": { + "name": "content_report_grade_id_grade_id_fk", + "tableFrom": "content_report", + "tableTo": "grade", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grade": { + "name": "grade", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "response_id": { + "name": "response_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "grade_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "misconception_tag": { + "name": "misconception_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "grade_response_id_response_id_fk": { + "name": "grade_response_id_response_id_fk", + "tableFrom": "grade", + "tableTo": "response", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "job_idempotency_key_unique": { + "name": "job_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journey_instance": { + "name": "journey_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "journey_instance_user_id_user_id_fk": { + "name": "journey_instance_user_id_user_id_fk", + "tableFrom": "journey_instance", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journey_instance_blueprint_id_blueprint_id_fk": { + "name": "journey_instance_blueprint_id_blueprint_id_fk", + "tableFrom": "journey_instance", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lesson": { + "name": "lesson", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "est_minutes": { + "name": "est_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "lesson_blueprint_id_blueprint_id_fk": { + "name": "lesson_blueprint_id_blueprint_id_fk", + "tableFrom": "lesson", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mastery": { + "name": "mastery", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0.5 + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_review": { + "name": "next_review", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mastery_user_id_user_id_fk": { + "name": "mastery_user_id_user_id_fk", + "tableFrom": "mastery", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mastery_concept_id_concept_id_fk": { + "name": "mastery_concept_id_concept_id_fk", + "tableFrom": "mastery", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mastery_user_id_concept_id_pk": { + "name": "mastery_user_id_concept_id_pk", + "columns": [ + "user_id", + "concept_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.misconception": { + "name": "misconception", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correction": { + "name": "correction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "misconception_concept_id_concept_id_fk": { + "name": "misconception_concept_id_concept_id_fk", + "tableFrom": "misconception", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.novel_misconduct_queue": { + "name": "novel_misconduct_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "grade_id": { + "name": "grade_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "response_text": { + "name": "response_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_span": { + "name": "evidence_span", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "novel_misconduct_queue_grade_id_grade_id_fk": { + "name": "novel_misconduct_queue_grade_id_grade_id_fk", + "tableFrom": "novel_misconduct_queue", + "tableTo": "grade", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "novel_misconduct_queue_checkpoint_id_checkpoint_id_fk": { + "name": "novel_misconduct_queue_checkpoint_id_checkpoint_id_fk", + "tableFrom": "novel_misconduct_queue", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.response": { + "name": "response", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "self_confidence": { + "name": "self_confidence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ts": { + "name": "ts", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "response_user_id_user_id_fk": { + "name": "response_user_id_user_id_fk", + "tableFrom": "response", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "response_checkpoint_id_checkpoint_id_fk": { + "name": "response_checkpoint_id_checkpoint_id_fk", + "tableFrom": "response", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.segment": { + "name": "segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body_json": { + "name": "body_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_chunk_ids": { + "name": "source_chunk_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "difficulty_level": { + "name": "difficulty_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "segment_lesson_id_lesson_id_fk": { + "name": "segment_lesson_id_lesson_id_fk", + "tableFrom": "segment", + "tableTo": "lesson", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source_chunk": { + "name": "source_chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "doc_ref": { + "name": "doc_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'learner'" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_token": { + "name": "verification_token", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_token_identifier_token_pk": { + "name": "verification_token_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_report": { + "name": "verify_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "verify_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.blueprint_status": { + "name": "blueprint_status", + "schema": "public", + "values": [ + "draft", + "verifying", + "published", + "flagged" + ] + }, + "public.checkpoint_kind": { + "name": "checkpoint_kind", + "schema": "public", + "values": [ + "predict", + "explain", + "solve" + ] + }, + "public.content_report_reason": { + "name": "content_report_reason", + "schema": "public", + "values": [ + "grade_wrong", + "content_error", + "other" + ] + }, + "public.grade_verdict": { + "name": "grade_verdict", + "schema": "public", + "values": [ + "mastered", + "partial", + "misconception", + "uncertain" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "pending", + "running", + "done", + "failed" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "learner", + "admin", + "suspended" + ] + }, + "public.verify_status": { + "name": "verify_status", + "schema": "public", + "values": [ + "pending", + "pass", + "fail", + "uncertain" + ] + }, + "public.verify_verdict": { + "name": "verify_verdict", + "schema": "public", + "values": [ + "pass", + "fail", + "uncertain" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0005_snapshot.json b/drizzle/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..09b46d2 --- /dev/null +++ b/drizzle/migrations/meta/0005_snapshot.json @@ -0,0 +1,1473 @@ +{ + "id": "c3ef9123-a7c3-43c7-8207-95e5c9441891", + "prevId": "43cc9612-0f99-4bcf-b520-fe15e9471cc6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blueprint": { + "name": "blueprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "intent_key": { + "name": "intent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "blueprint_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blueprint_intent_key_unique": { + "name": "blueprint_intent_key_unique", + "nullsNotDistinct": false, + "columns": [ + "intent_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkpoint": { + "name": "checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "segment_id": { + "name": "segment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "checkpoint_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_answer": { + "name": "reference_answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rubric_json": { + "name": "rubric_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "checkpoint_segment_id_segment_id_fk": { + "name": "checkpoint_segment_id_segment_id_fk", + "tableFrom": "checkpoint", + "tableTo": "segment", + "columnsFrom": [ + "segment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.concept": { + "name": "concept", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieval_ctx_ref": { + "name": "retrieval_ctx_ref", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "concept_blueprint_id_blueprint_id_fk": { + "name": "concept_blueprint_id_blueprint_id_fk", + "tableFrom": "concept", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_report": { + "name": "content_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "grade_id": { + "name": "grade_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "content_report_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "content_report_checkpoint_id_checkpoint_id_fk": { + "name": "content_report_checkpoint_id_checkpoint_id_fk", + "tableFrom": "content_report", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "content_report_grade_id_grade_id_fk": { + "name": "content_report_grade_id_grade_id_fk", + "tableFrom": "content_report", + "tableTo": "grade", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grade": { + "name": "grade", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "response_id": { + "name": "response_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "grade_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "misconception_tag": { + "name": "misconception_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "grade_response_id_response_id_fk": { + "name": "grade_response_id_response_id_fk", + "tableFrom": "grade", + "tableTo": "response", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "job_idempotency_key_unique": { + "name": "job_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journey_instance": { + "name": "journey_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "journey_instance_user_id_user_id_fk": { + "name": "journey_instance_user_id_user_id_fk", + "tableFrom": "journey_instance", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journey_instance_blueprint_id_blueprint_id_fk": { + "name": "journey_instance_blueprint_id_blueprint_id_fk", + "tableFrom": "journey_instance", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lesson": { + "name": "lesson", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "est_minutes": { + "name": "est_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "lesson_blueprint_id_blueprint_id_fk": { + "name": "lesson_blueprint_id_blueprint_id_fk", + "tableFrom": "lesson", + "tableTo": "blueprint", + "columnsFrom": [ + "blueprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mastery": { + "name": "mastery", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0.5 + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_review": { + "name": "next_review", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mastery_user_id_user_id_fk": { + "name": "mastery_user_id_user_id_fk", + "tableFrom": "mastery", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mastery_concept_id_concept_id_fk": { + "name": "mastery_concept_id_concept_id_fk", + "tableFrom": "mastery", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mastery_user_id_concept_id_pk": { + "name": "mastery_user_id_concept_id_pk", + "columns": [ + "user_id", + "concept_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.misconception": { + "name": "misconception", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "concept_id": { + "name": "concept_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correction": { + "name": "correction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "misconception_concept_id_concept_id_fk": { + "name": "misconception_concept_id_concept_id_fk", + "tableFrom": "misconception", + "tableTo": "concept", + "columnsFrom": [ + "concept_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.novel_misconduct_queue": { + "name": "novel_misconduct_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "grade_id": { + "name": "grade_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "response_text": { + "name": "response_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_span": { + "name": "evidence_span", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "novel_misconduct_queue_grade_id_grade_id_fk": { + "name": "novel_misconduct_queue_grade_id_grade_id_fk", + "tableFrom": "novel_misconduct_queue", + "tableTo": "grade", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "novel_misconduct_queue_checkpoint_id_checkpoint_id_fk": { + "name": "novel_misconduct_queue_checkpoint_id_checkpoint_id_fk", + "tableFrom": "novel_misconduct_queue", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_token": { + "name": "password_reset_token", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_token_user_id_user_id_fk": { + "name": "password_reset_token_user_id_user_id_fk", + "tableFrom": "password_reset_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.response": { + "name": "response", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "self_confidence": { + "name": "self_confidence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ts": { + "name": "ts", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "response_user_id_user_id_fk": { + "name": "response_user_id_user_id_fk", + "tableFrom": "response", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "response_checkpoint_id_checkpoint_id_fk": { + "name": "response_checkpoint_id_checkpoint_id_fk", + "tableFrom": "response", + "tableTo": "checkpoint", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.segment": { + "name": "segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body_json": { + "name": "body_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_chunk_ids": { + "name": "source_chunk_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "verify_status": { + "name": "verify_status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "content_version": { + "name": "content_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "difficulty_level": { + "name": "difficulty_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "segment_lesson_id_lesson_id_fk": { + "name": "segment_lesson_id_lesson_id_fk", + "tableFrom": "segment", + "tableTo": "lesson", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_config": { + "name": "site_config", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(100)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source_chunk": { + "name": "source_chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "doc_ref": { + "name": "doc_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'learner'" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_token": { + "name": "verification_token", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_token_identifier_token_pk": { + "name": "verification_token_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_report": { + "name": "verify_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "verify_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.blueprint_status": { + "name": "blueprint_status", + "schema": "public", + "values": [ + "draft", + "verifying", + "published", + "flagged" + ] + }, + "public.checkpoint_kind": { + "name": "checkpoint_kind", + "schema": "public", + "values": [ + "predict", + "explain", + "solve" + ] + }, + "public.content_report_reason": { + "name": "content_report_reason", + "schema": "public", + "values": [ + "grade_wrong", + "content_error", + "other" + ] + }, + "public.grade_verdict": { + "name": "grade_verdict", + "schema": "public", + "values": [ + "mastered", + "partial", + "misconception", + "uncertain" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "pending", + "running", + "done", + "failed" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "learner", + "admin", + "suspended" + ] + }, + "public.verify_status": { + "name": "verify_status", + "schema": "public", + "values": [ + "pending", + "pass", + "fail", + "uncertain" + ] + }, + "public.verify_verdict": { + "name": "verify_verdict", + "schema": "public", + "values": [ + "pass", + "fail", + "uncertain" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json new file mode 100644 index 0000000..2f52d9f --- /dev/null +++ b/drizzle/migrations/meta/_journal.json @@ -0,0 +1,48 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1781991328126, + "tag": "0000_overrated_liz_osborn", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1782018031574, + "tag": "0001_marvelous_pepper_potts", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1782100000000, + "tag": "0002_response_confidence", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1782050126016, + "tag": "0003_parched_wither", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1782066514369, + "tag": "0004_auth", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1782070177922, + "tag": "0005_site_email", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/e2e/learn-flow.test.ts b/e2e/learn-flow.test.ts new file mode 100644 index 0000000..861ddce --- /dev/null +++ b/e2e/learn-flow.test.ts @@ -0,0 +1,124 @@ +/** + * E2E: full learn flow. + * + * Runs against the live dev server with seeded data (pnpm seed). + * Mocks only the grade and socratic API calls (avoid real LLM cost). + * Lesson data comes from the DB (seeded js-closures corpus). + * + * Pre-condition: `docker compose up -d && pnpm seed` must have run. + */ +import { test, expect } from '@playwright/test'; + +// ── Mock payloads ───────────────────────────────────────────────────────────── + +const MOCK_GRADE_MASTERED = { + responseId: 'resp-uuid-001', + gradeId: 'grade-uuid-001', + grade: { + verdict: 'mastered', + rubric_hits: ['r1', 'r2'], + misconception_tag: 'none', + evidence_span: 'retains access to variables from its enclosing scope', + feedback_directive: 'advance', + confidence: 0.92, + }, +}; + +const MOCK_GRADE_PARTIAL = { + responseId: 'resp-uuid-002', + gradeId: 'grade-uuid-002', + grade: { + verdict: 'partial', + rubric_hits: ['r1'], + misconception_tag: 'none', + evidence_span: 'a function inside a function', + feedback_directive: 'probe: Can you say more about what happens after the outer function returns?', + confidence: 0.78, + }, +}; + +const MOCK_GRADE_BUDGET_EXCEEDED = { + status: 429, + json: { error: 'Session limit reached. Start a new session to continue.' }, +}; + +// ── Tests ───────────────────────────────────────────────────────────────────── + +test.describe('learn flow (seeded DB)', () => { + test('homepage loads with intent input', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toContainText('Curio'); + await expect(page.locator('[aria-label="Learning intent"]')).toBeVisible(); + await expect(page.locator('button[type="submit"]')).toBeDisabled(); + }); + + test('intent submission navigates to lesson page', async ({ page }) => { + await page.route('/api/intent', (route) => + route.fulfill({ + json: { intentKey: 'javascript-closures', blueprintId: 'bp-seed', isNew: false }, + }), + ); + + await page.goto('/'); + await page.fill('[aria-label="Learning intent"]', 'JavaScript closures'); + await page.click('button[type="submit"]'); + + await expect(page).toHaveURL('/learn/javascript-closures'); + }); + + test('lesson page renders seeded content', async ({ page }) => { + await page.goto('/learn/javascript-closures'); + + // Blueprint title + await expect(page.locator('h1')).toContainText('JavaScript Closures'); + + // First segment body is visible + await expect(page.getByText(/closure/i).first()).toBeVisible(); + + // First checkpoint prompt is visible + await expect(page.getByText(/explain what a closure is/i)).toBeVisible(); + }); + + test('mastered verdict unlocks next segment', async ({ page }) => { + await page.route('/api/grade', (route) => route.fulfill({ json: MOCK_GRADE_MASTERED })); + + await page.goto('/learn/javascript-closures'); + + const textarea = page.locator('textarea').first(); + await textarea.fill('A closure retains access to variables from its enclosing scope even after the outer function returns.'); + await page.locator('button[type="submit"]').first().click(); + + await expect(page.getByText(/mastered/i)).toBeVisible(); + // Second segment should now be unlocked (no longer shows "locked" hint) + await expect(page.getByText('Complete the previous checkpoint to continue.')).not.toBeVisible(); + }); + + test('partial verdict shows probe question', async ({ page }) => { + await page.route('/api/grade', (route) => route.fulfill({ json: MOCK_GRADE_PARTIAL })); + + await page.goto('/learn/javascript-closures'); + + const textarea = page.locator('textarea').first(); + await textarea.fill('a function inside a function'); + await page.locator('button[type="submit"]').first().click(); + + await expect(page.getByText(/partial/i)).toBeVisible(); + await expect( + page.getByText(/what happens after the outer function returns/i), + ).toBeVisible(); + }); + + test('budget exceeded shows friendly error', async ({ page }) => { + await page.route('/api/grade', (route) => + route.fulfill(MOCK_GRADE_BUDGET_EXCEEDED), + ); + + await page.goto('/learn/javascript-closures'); + + const textarea = page.locator('textarea').first(); + await textarea.fill('some answer'); + await page.locator('button[type="submit"]').first().click(); + + await expect(page.getByText(/session limit/i)).toBeVisible(); + }); +}); diff --git a/e2e/smoke.test.ts b/e2e/smoke.test.ts new file mode 100644 index 0000000..812110f --- /dev/null +++ b/e2e/smoke.test.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test'; + +test('homepage loads with intent input', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toContainText('Curio'); + await expect(page.locator('[aria-label="Learning intent"]')).toBeVisible(); + await expect(page.locator('button[type="submit"]')).toBeDisabled(); +}); + +test('submit button enables when input has text', async ({ page }) => { + await page.goto('/'); + await page.fill('[aria-label="Learning intent"]', 'JavaScript closures'); + await expect(page.locator('button[type="submit"]')).toBeEnabled(); +}); diff --git a/jobs/generate-lesson-job.ts b/jobs/generate-lesson-job.ts new file mode 100644 index 0000000..843ad7f --- /dev/null +++ b/jobs/generate-lesson-job.ts @@ -0,0 +1,137 @@ +/** + * BullMQ worker: generate lesson content for a blueprint. + * + * Idempotent: checks `job` ledger before generating. + * On success: writes lesson to Redis cache and updates job status. + * On failure: increments attempt count; sets status to 'failed' after processing. + */ +import { Worker } from 'bullmq'; +import { eq } from 'drizzle-orm'; +import { db } from '../src/lib/db'; +import { jobs, blueprints, concepts, lessons } from '../src/lib/db/schema'; +import { generateLesson } from '../src/lib/generation/generate-lesson'; +import { verifyLesson } from '../src/lib/verification/verify-content'; +import { getLessonResponse } from '../src/lib/db/queries'; +import { setCachedLesson } from '../src/lib/cache/lesson'; +import { enqueuePromoteBlueprint } from '../src/lib/jobs/queue'; +import type { GenerateLessonJobData } from '../src/lib/jobs/queue'; + +const connection = { + url: process.env.REDIS_URL ?? 'redis://localhost:6379', +}; + +const worker = new Worker( + 'generate-lesson', + async (job) => { + const { intentKey, blueprintId, idempotencyKey } = job.data; + + // 1. Idempotency check — find the job row by idempotency key + const [jobRow] = await db + .select({ id: jobs.id, status: jobs.status, attempts: jobs.attempts }) + .from(jobs) + .where(eq(jobs.id, idempotencyKey)) + .limit(1); + + if (jobRow?.status === 'done') { + // Already completed — warm cache if needed and return + const cached = await getLessonResponse(intentKey); + if (cached?.state === 'ready') { + await setCachedLesson(intentKey, cached); + } + return; + } + + // Mark running + await db + .update(jobs) + .set({ status: 'running', attempts: (jobRow?.attempts ?? 0) + 1 }) + .where(eq(jobs.id, idempotencyKey)); + + // 2. Check lesson doesn't already exist + const existing = await getLessonResponse(intentKey); + if (existing?.state === 'ready') { + await setCachedLesson(intentKey, existing); + await db.update(jobs).set({ status: 'done' }).where(eq(jobs.id, idempotencyKey)); + return; + } + + // 3. Fetch blueprint + concepts + const [blueprint] = await db + .select() + .from(blueprints) + .where(eq(blueprints.id, blueprintId)) + .limit(1); + + if (!blueprint) throw new Error(`Blueprint not found: ${blueprintId}`); + + const conceptRows = await db + .select({ id: concepts.id, name: concepts.name, ord: concepts.ord, retrievalCtxRef: concepts.retrievalCtxRef }) + .from(concepts) + .where(eq(concepts.blueprintId, blueprintId)); + + if (conceptRows.length === 0) throw new Error(`No concepts for blueprint: ${blueprintId}`); + + // 4. Generate lesson + const { lessonId } = await generateLesson({ + blueprintId, + lessonOrd: 0, + topicTitle: blueprint.title, + conceptsToGenerate: conceptRows.map((c) => ({ + id: c.id, + name: c.name, + retrievalCtxRef: c.retrievalCtxRef, + })), + }); + + // 5. T1 verification (non-blocking for serve — T2 + promotion handled by promote-blueprint job) + await verifyLesson({ lessonId, runT2: false }); + + // 6. Write to Redis cache + const lesson = await getLessonResponse(intentKey); + if (lesson?.state === 'ready') { + await setCachedLesson(intentKey, lesson); + } + + // 7. Update job ledger + await db.update(jobs).set({ status: 'done' }).where(eq(jobs.id, idempotencyKey)); + + // 8. Enqueue blueprint promotion (T2 verify + misconceptions) + const promoteKey = `promote-blueprint:${blueprintId}:v1`; + const [existingPromote] = await db + .select({ id: jobs.id }) + .from(jobs) + .where(eq(jobs.idempotencyKey, promoteKey)) + .limit(1); + + if (!existingPromote) { + const [promoteJobRow] = await db + .insert(jobs) + .values({ + type: 'promote-blueprint', + idempotencyKey: promoteKey, + status: 'pending', + payloadJson: { blueprintId, intentKey, lessonId }, + }) + .returning({ id: jobs.id }); + + await enqueuePromoteBlueprint({ + blueprintId, + intentKey, + idempotencyKey: promoteJobRow.id, + }); + } + }, + { connection, concurrency: 2 }, +); + +worker.on('failed', async (job, err) => { + if (job?.data.idempotencyKey) { + await db + .update(jobs) + .set({ status: 'failed' }) + .where(eq(jobs.id, job.data.idempotencyKey)); + } + console.error('[generate-lesson] job failed:', err); +}); + +export default worker; diff --git a/jobs/index.ts b/jobs/index.ts new file mode 100644 index 0000000..111fae5 --- /dev/null +++ b/jobs/index.ts @@ -0,0 +1,17 @@ +import generateLessonWorker from './generate-lesson-job'; +import promoteBlueprintWorker from './promote-blueprint-job'; + +console.log('[workers] Running: generate-lesson, promote-blueprint'); + +async function shutdown(signal: string) { + console.log(`[workers] ${signal} received — draining and closing workers...`); + await Promise.all([ + generateLessonWorker.close(), + promoteBlueprintWorker.close(), + ]); + console.log('[workers] Shutdown complete.'); + process.exit(0); +} + +process.on('SIGTERM', () => void shutdown('SIGTERM')); +process.on('SIGINT', () => void shutdown('SIGINT')); diff --git a/jobs/promote-blueprint-job.ts b/jobs/promote-blueprint-job.ts new file mode 100644 index 0000000..6d5467f --- /dev/null +++ b/jobs/promote-blueprint-job.ts @@ -0,0 +1,120 @@ +/** + * BullMQ worker: T2 content verification + misconception generation + blueprint promotion. + * + * After T2 passes, generates difficulty level 2 and 3 variants (§8.1, §13) so the serve + * path can select the right level per learner without synchronous generation. + * + * Idempotent: checks job ledger before running, skips variant generation if segments + * at that level already exist. + */ +import { Worker } from 'bullmq'; +import { eq, and } from 'drizzle-orm'; +import { db } from '../src/lib/db'; +import { jobs, concepts, lessons, segments, blueprints } from '../src/lib/db/schema'; +import { verifyLesson } from '../src/lib/verification/verify-content'; +import { generateMisconceptions } from '../src/lib/generation/generate-misconceptions'; +import { verifyMisconceptions } from '../src/lib/verification/verify-misconceptions'; +import { generateLesson } from '../src/lib/generation/generate-lesson'; +import { invalidateCachedLesson } from '../src/lib/cache/lesson'; +import type { PromoteBlueprintJobData } from '../src/lib/jobs/queue'; + +const connection = { + url: process.env.REDIS_URL ?? 'redis://localhost:6379', +}; + +const worker = new Worker( + 'promote-blueprint', + async (job) => { + const { blueprintId, intentKey, idempotencyKey } = job.data; + + // 1. Idempotency check + const [jobRow] = await db + .select({ id: jobs.id, status: jobs.status, payloadJson: jobs.payloadJson }) + .from(jobs) + .where(eq(jobs.id, idempotencyKey)) + .limit(1); + + if (jobRow?.status === 'done') return; + + await db + .update(jobs) + .set({ status: 'running', attempts: (jobRow as { attempts?: number })?.attempts ?? 1 }) + .where(eq(jobs.id, idempotencyKey)); + + // 2. Find lesson for this blueprint + const [lesson] = await db + .select({ id: lessons.id }) + .from(lessons) + .where(eq(lessons.blueprintId, blueprintId)) + .limit(1); + + if (!lesson) throw new Error(`No lesson for blueprint: ${blueprintId}`); + + // 3. T2 verification (strong model — promotes blueprint to 'published' if all pass) + await verifyLesson({ lessonId: lesson.id, runT2: true }); + + // 4. Generate misconceptions for each concept + const conceptRows = await db + .select({ id: concepts.id }) + .from(concepts) + .where(eq(concepts.blueprintId, blueprintId)); + + for (const concept of conceptRows) { + await generateMisconceptions({ conceptId: concept.id }); + await verifyMisconceptions({ conceptId: concept.id }); + } + + // 5. Generate difficulty variants 2 and 3 (§8.1, §13) + // Each variant is a new set of segments at a higher explanation level. + // Skip if segments at that level already exist (idempotency). + const [blueprint] = await db + .select({ title: blueprints.title }) + .from(blueprints) + .where(eq(blueprints.id, blueprintId)) + .limit(1); + + const conceptRowsFull = await db + .select({ id: concepts.id, name: concepts.name, retrievalCtxRef: concepts.retrievalCtxRef, ord: concepts.ord }) + .from(concepts) + .where(eq(concepts.blueprintId, blueprintId)); + + if (blueprint && conceptRowsFull.length > 0) { + const lessonOrd = 0; + for (const level of [2, 3] as const) { + const existing = await db + .select({ id: segments.id }) + .from(segments) + .where(and(eq(segments.lessonId, lesson.id), eq(segments.difficultyLevel, level))) + .limit(1); + + if (existing.length === 0) { + await generateLesson({ + blueprintId, + lessonOrd, + topicTitle: blueprint.title, + conceptsToGenerate: conceptRowsFull, + difficultyLevel: level, + }); + } + } + } + + // 6. Invalidate cache so next request fetches fresh lesson with updated verify status + await invalidateCachedLesson(intentKey); + + await db.update(jobs).set({ status: 'done' }).where(eq(jobs.id, idempotencyKey)); + }, + { connection, concurrency: 1 }, +); + +worker.on('failed', async (job, err) => { + if (job?.data.idempotencyKey) { + await db + .update(jobs) + .set({ status: 'failed' }) + .where(eq(jobs.id, job.data.idempotencyKey)); + } + console.error('[promote-blueprint] job failed:', err); +}); + +export default worker; diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..b22af96 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..efa66e2 --- /dev/null +++ b/package.json @@ -0,0 +1,75 @@ +{ + "name": "curio", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "drizzle-kit": "drizzle-kit", + "db:generate": "dotenv -e .env.local -- drizzle-kit generate", + "db:migrate": "dotenv -e .env.local -- drizzle-kit migrate", + "db:studio": "dotenv -e .env.local -- drizzle-kit studio", + "seed": "tsx --env-file=.env.local scripts/seed.ts", + "test:golden": "tsx --env-file=.env.local tests/golden/run-grading-eval.ts", + "test:golden:misconceptions": "tsx --env-file=.env.local tests/golden/run-misconception-eval.ts", + "test:golden:content": "tsx --env-file=.env.local tests/golden/run-content-eval.ts", + "workers": "tsx --env-file=.env.local jobs/index.ts" + }, + "dependencies": { + "@ai-sdk/amazon-bedrock": "^4.0.119", + "@ai-sdk/anthropic": "^1.0.0", + "@ai-sdk/azure": "^3.0.76", + "@ai-sdk/cohere": "^3.0.39", + "@ai-sdk/google": "^3.0.83", + "@ai-sdk/groq": "^3.0.42", + "@ai-sdk/mistral": "^3.0.40", + "@ai-sdk/openai": "^1.0.0", + "@ai-sdk/provider": "^1.0.0", + "@ai-sdk/xai": "^3.0.96", + "@auth/drizzle-adapter": "^1.11.2", + "@upstash/ratelimit": "^2.0.8", + "@upstash/redis": "^1.38.0", + "ai": "^4.3.0", + "bcryptjs": "^3.0.3", + "bullmq": "^5.79.0", + "drizzle-orm": "^0.40.0", + "ioredis": "^5.4.0", + "langfuse": "^3.0.0", + "next": "^15.3.0", + "next-auth": "5.0.0-beta.31", + "nodemailer": "^9.0.1", + "ollama-ai-provider": "^1.2.0", + "pg": "^8.13.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "server-only": "^0.0.1", + "zod": "^3.24.0" + }, + "devDependencies": { + "@playwright/test": "^1.49.0", + "@tailwindcss/postcss": "^4.1.0", + "@types/bcryptjs": "^3.0.0", + "@types/node": "^22.0.0", + "@types/nodemailer": "^8.0.1", + "@types/pg": "^8.11.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.4.0", + "dotenv": "^16.4.0", + "dotenv-cli": "^11.0.0", + "drizzle-kit": "^0.30.0", + "eslint": "^9.0.0", + "eslint-config-next": "^15.3.0", + "prettier": "^3.5.0", + "tailwindcss": "^4.1.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0", + "vitest": "^3.1.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d7172c5 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'list', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + ], + webServer: { + command: 'pnpm dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..dee405c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7186 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ai-sdk/amazon-bedrock': + specifier: ^4.0.119 + version: 4.0.119(zod@3.25.76) + '@ai-sdk/anthropic': + specifier: ^1.0.0 + version: 1.2.12(zod@3.25.76) + '@ai-sdk/azure': + specifier: ^3.0.76 + version: 3.0.76(zod@3.25.76) + '@ai-sdk/cohere': + specifier: ^3.0.39 + version: 3.0.39(zod@3.25.76) + '@ai-sdk/google': + specifier: ^3.0.83 + version: 3.0.83(zod@3.25.76) + '@ai-sdk/groq': + specifier: ^3.0.42 + version: 3.0.42(zod@3.25.76) + '@ai-sdk/mistral': + specifier: ^3.0.40 + version: 3.0.40(zod@3.25.76) + '@ai-sdk/openai': + specifier: ^1.0.0 + version: 1.3.24(zod@3.25.76) + '@ai-sdk/provider': + specifier: ^1.0.0 + version: 1.1.3 + '@ai-sdk/xai': + specifier: ^3.0.96 + version: 3.0.96(zod@3.25.76) + '@auth/drizzle-adapter': + specifier: ^1.11.2 + version: 1.11.2(nodemailer@9.0.1) + '@upstash/ratelimit': + specifier: ^2.0.8 + version: 2.0.8(@upstash/redis@1.38.0) + '@upstash/redis': + specifier: ^1.38.0 + version: 1.38.0 + ai: + specifier: ^4.3.0 + version: 4.3.19(react@19.2.7)(zod@3.25.76) + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 + bullmq: + specifier: ^5.79.0 + version: 5.79.0 + drizzle-orm: + specifier: ^0.40.0 + version: 0.40.1(@opentelemetry/api@1.9.0)(@types/pg@8.20.0)(gel@2.2.0)(pg@8.22.0) + ioredis: + specifier: ^5.4.0 + version: 5.11.1 + langfuse: + specifier: ^3.0.0 + version: 3.38.20 + next: + specifier: ^15.3.0 + version: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next-auth: + specifier: 5.0.0-beta.31 + version: 5.0.0-beta.31(next@15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(nodemailer@9.0.1)(react@19.2.7) + nodemailer: + specifier: ^9.0.1 + version: 9.0.1 + ollama-ai-provider: + specifier: ^1.2.0 + version: 1.2.0(zod@3.25.76) + pg: + specifier: ^8.13.0 + version: 8.22.0 + react: + specifier: ^19.0.0 + version: 19.2.7 + react-dom: + specifier: ^19.0.0 + version: 19.2.7(react@19.2.7) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + zod: + specifier: ^3.24.0 + version: 3.25.76 + devDependencies: + '@playwright/test': + specifier: ^1.49.0 + version: 1.61.0 + '@tailwindcss/postcss': + specifier: ^4.1.0 + version: 4.3.1 + '@types/bcryptjs': + specifier: ^3.0.0 + version: 3.0.0 + '@types/node': + specifier: ^22.0.0 + version: 22.19.21 + '@types/nodemailer': + specifier: ^8.0.1 + version: 8.0.1 + '@types/pg': + specifier: ^8.11.0 + version: 8.20.0 + '@types/react': + specifier: ^19.0.0 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.7.0(vite@7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + dotenv-cli: + specifier: ^11.0.0 + version: 11.0.0 + drizzle-kit: + specifier: ^0.30.0 + version: 0.30.6 + eslint: + specifier: ^9.0.0 + version: 9.39.4(jiti@2.7.0) + eslint-config-next: + specifier: ^15.3.0 + version: 15.5.19(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + prettier: + specifier: ^3.5.0 + version: 3.8.4 + tailwindcss: + specifier: ^4.1.0 + version: 4.3.1 + tsx: + specifier: ^4.19.0 + version: 4.22.4 + typescript: + specifier: ^5.8.0 + version: 5.9.3 + vitest: + specifier: ^3.1.0 + version: 3.2.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + +packages: + + '@ai-sdk/amazon-bedrock@4.0.119': + resolution: {integrity: sha512-DgJugGl0Euja2g+60p3eV+2k4kQvtLtJFeYskVrAlt0KgpVGYot0XEPCOBgjDsn6aAEuyibWDSIddxJU8k9wug==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/anthropic@1.2.12': + resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/anthropic@3.0.85': + resolution: {integrity: sha512-fNeDB644l5wbRNQU0FnI+F7UTtOenMnPtACfMPUJaS2zJfuBlseEa1TMg+otHkETZgaJB+6Na51NQEv0+m7czw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/azure@3.0.76': + resolution: {integrity: sha512-Z7CsB0Vc/UsM3gkrUb9BiEJ/Lvf1mABj17UMjHFuK0GzyYRRMb6anxvJ9FtP7G5lItnifRHZ4ZrTSi5e7ghTYQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/cohere@3.0.39': + resolution: {integrity: sha512-BLj9ZCwuoXJDRP6UbgM6ufvkcCVRnbS1SyA5j6pgt6exnlfLdo6UmpCX5T99/YdPvySUX8agvjTPC5EZw5mJ3A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/deepseek@2.0.39': + resolution: {integrity: sha512-3wUq1cvqSWkRadCSqcDXBLpOwUAe+JqfWQZS0fK0sWiqDeTIEzsse+r7xcN2x5XpJjefT0d7FQTa3u2lZ/yxbg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@3.0.83': + resolution: {integrity: sha512-Pz7aCX0dy+5x+r4K/37HbLZNaPtPL4q2NduzJW64VffLv5sI9Nb478wAd7PlH2r2asiypJsz/Jerf9draTciUA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/groq@3.0.42': + resolution: {integrity: sha512-3qJSxYTSxCLIxArQSP5hvaRmo4DkqNrFatDzyosCTSCqzWBGP7Xu3zeYzf+HkGuY0gxI9f6C2aHoUXvUmTqEkA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/mistral@3.0.40': + resolution: {integrity: sha512-HzCV9jFsb04kpL/N+G7SCjFKJA0Q33p4Hc+1quYGGD8Ta37Pe5bzk9AjxaS8OYd//jmcny16yKhJ+e+h+P0AJg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai-compatible@2.0.51': + resolution: {integrity: sha512-A6qfyaVs4lxmRxRux6U3ViOa8mMbsSd0OaHghpei2MpiBT6791J4zFH5MN7kaW1tLfQ246rSC2DVTMOavsycjQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@1.3.24': + resolution: {integrity: sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/openai@3.0.73': + resolution: {integrity: sha512-+3x9oxHv9Xp33Iv2L8D+e5hqmZi64jofBKig/9611JKyfV59NdkaDDajtwc0CxOEfARgCVq1BW7dP+526gKOKw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider-utils@4.0.30': + resolution: {integrity: sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/provider@3.0.10': + resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.12': + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/xai@3.0.96': + resolution: {integrity: sha512-a24jD29cQ3YocP7B4NUvNTb9s5CYR1ao2yU/QncT7QDwwOp4x/lq+6W+BdXGf1VucG7A0dx9jtyIOm6PE4JPyg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@auth/core@0.41.2': + resolution: {integrity: sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^7.0.7 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + '@auth/drizzle-adapter@1.11.2': + resolution: {integrity: sha512-VOuj7REI8jfJjpSbsYwDM/Zrn55T6lS9Yc+29V+EXMcel8eqG7x+7LodNfd1WHjakfkLYi+qsbYUHB3E6aDA4w==} + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@next/env@15.5.19': + resolution: {integrity: sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==} + + '@next/eslint-plugin-next@15.5.19': + resolution: {integrity: sha512-Ctwb4qYuMbHN/1oXLlTdMchwG8h8Xzwq+wGZZMgF3o6+uwyBKAI2c96bdOsl+C62PaUD0Jkh+QpNkhUeDlam0Q==} + + '@next/swc-darwin-arm64@15.5.19': + resolution: {integrity: sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.19': + resolution: {integrity: sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.19': + resolution: {integrity: sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@15.5.19': + resolution: {integrity: sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@15.5.19': + resolution: {integrity: sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@15.5.19': + resolution: {integrity: sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@15.5.19': + resolution: {integrity: sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.19': + resolution: {integrity: sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + + '@petamoriken/float16@3.9.3': + resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + + '@playwright/test@1.61.0': + resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.16.1': + resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==} + + '@smithy/core@3.25.1': + resolution: {integrity: sha512-zpDbpXBCBsxfLtG2GEUyfgvHvSFrw5CwDZSNzL0v52gx/c3oPlPbm+7W7num8xs6vyiUBn+bvYPHcQDOXZynCQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.4.1': + resolution: {integrity: sha512-doX7qXMdrweuhh9bMBhyflxrmK4gAHm4E8CDKRS8W6sgXncrHR2Jh4YSAuHmTVjRk38GJjERH2QBUSrMV4Ee+A==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.4.1': + resolution: {integrity: sha512-OCnvu9xJnNJf0kJHI4101r4bf2l4niSMOzeiN5AgE97RnbzmYKmixc95VpiRRU5QqEab8jq2Afpfs1xM86KRSw==} + engines: {node: '>=18.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.3.1': + resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} + + '@tailwindcss/oxide-android-arm64@4.3.1': + resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.1': + resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.1': + resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.1': + resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bcryptjs@3.0.0': + resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} + deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.19.21': + resolution: {integrity: sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==} + + '@types/nodemailer@8.0.1': + resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@typescript-eslint/eslint-plugin@8.61.1': + resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.61.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.61.1': + resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.61.1': + resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.61.1': + resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.61.1': + resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.61.1': + resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.61.1': + resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.61.1': + resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.61.1': + resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.61.1': + resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@upstash/core-analytics@0.0.10': + resolution: {integrity: sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==} + engines: {node: '>=16.0.0'} + + '@upstash/ratelimit@2.0.8': + resolution: {integrity: sha512-YSTMBJ1YIxsoPkUMX/P4DDks/xV5YYCswWMamU8ZIfK9ly6ppjRnVOyBhMDXBmzjODm4UQKcxsJPvaeFAijp5w==} + peerDependencies: + '@upstash/redis': ^1.34.3 + + '@upstash/redis@1.38.0': + resolution: {integrity: sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@3.2.6': + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + + '@vitest/mocker@3.2.6': + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.6': + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + + '@vitest/runner@3.2.6': + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + + '@vitest/snapshot@3.2.6': + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + + '@vitest/spy@3.2.6': + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + + '@vitest/utils@3.2.6': + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ai@4.3.19: + resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.38: + resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bullmq@5.79.0: + resolution: {integrity: sha512-sg+kYGn7PIDI/AAkSWINPNz0vMp745YaBYMyo3AtcfXk5iCvjEPU+XMbU3yf7rSf1KsU+OfU+GH8FhxUa+WDNA==} + engines: {node: '>=12.22.0'} + peerDependencies: + redis: '>=5.0.0' + peerDependenciesMeta: + redis: + optional: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dotenv-cli@11.0.0: + resolution: {integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==} + hasBin: true + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + drizzle-kit@0.30.6: + resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==} + hasBin: true + + drizzle-orm@0.40.1: + resolution: {integrity: sha512-aPNhtiJiPfm3qxz1czrnIDkfvkSdKGXYeZkpG55NPTVI186LmK2fBLMi4dsHpPHlJrZeQ92D322YFPHADBALew==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.376: + resolution: {integrity: sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} + engines: {node: '>=10.13.0'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.3: + resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.1: + resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@15.5.19: + resolution: {integrity: sha512-UZwkuhBCNxVZfo93MSHRDOVNWXooJJGcAUyTAVIp0+9QFhH4SqJxWY0s6Mk9C2kMi777HPMn3dseOrZshWpG9Q==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.13.0: + resolution: {integrity: sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gel@2.2.0: + resolution: {integrity: sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + langfuse-core@3.38.20: + resolution: {integrity: sha512-zBKVmQN/1oT5VWZUBYlWzvokIlkC/6mnpgr/2atMyTeAm+jR3ia7w2iJMjlrF5/oG8ukO1s8+LDRCzJpF1QeEA==} + engines: {node: '>=18'} + + langfuse@3.38.20: + resolution: {integrity: sha512-MAmBAASSzJtmK1O9HQegA1mFsQhT8Yf+OJRGvE7FXkyv3g/eiBE0glLD0Ohg3pkxhoPdggM5SejK7ue9ctlaMA==} + engines: {node: '>=18'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} + hasBin: true + + msgpackr@2.0.2: + resolution: {integrity: sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + nanoid@3.3.13: + resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next-auth@5.0.0-beta.31: + resolution: {integrity: sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + next: ^14.0.0-0 || ^15.0.0 || ^16.0.0 + nodemailer: ^7.0.7 + react: ^18.2.0 || ^19.0.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + next@15.5.19: + resolution: {integrity: sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.48: + resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==} + engines: {node: '>=18'} + + nodemailer@9.0.1: + resolution: {integrity: sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==} + engines: {node: '>=6.0.0'} + + oauth4webapi@3.8.6: + resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + ollama-ai-provider@1.2.0: + resolution: {integrity: sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + playwright-core@1.61.0: + resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.0: + resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} + engines: {node: '>=18'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.4: + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tailwindcss@4.3.1: + resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.6: + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.6 + '@vitest/ui': 3.2.6 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@ai-sdk/amazon-bedrock@4.0.119(zod@3.25.76)': + dependencies: + '@ai-sdk/anthropic': 3.0.85(zod@3.25.76) + '@ai-sdk/openai': 3.0.73(zod@3.25.76) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + '@smithy/eventstream-codec': 4.4.1 + '@smithy/util-utf8': 4.4.1 + aws4fetch: 1.0.20 + zod: 3.25.76 + + '@ai-sdk/anthropic@1.2.12(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/anthropic@3.0.85(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/azure@3.0.76(zod@3.25.76)': + dependencies: + '@ai-sdk/deepseek': 2.0.39(zod@3.25.76) + '@ai-sdk/openai': 3.0.73(zod@3.25.76) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/cohere@3.0.39(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/deepseek@2.0.39(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/google@3.0.83(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/groq@3.0.42(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/mistral@3.0.40(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai-compatible@2.0.51(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@1.3.24(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@3.0.73(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.13 + secure-json-parse: 2.7.0 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.30(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.1.0 + zod: 3.25.76 + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@3.0.10': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.12(react@19.2.7)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + react: 19.2.7 + swr: 2.4.1(react@19.2.7) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.76 + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + + '@ai-sdk/xai@3.0.96(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 2.0.51(zod@3.25.76) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + zod: 3.25.76 + + '@alloc/quick-lru@5.2.0': {} + + '@auth/core@0.41.2(nodemailer@9.0.1)': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.2.3 + oauth4webapi: 3.8.6 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + optionalDependencies: + nodemailer: 9.0.1 + + '@auth/drizzle-adapter@1.11.2(nodemailer@9.0.1)': + dependencies: + '@auth/core': 0.41.2(nodemailer@9.0.1) + transitivePeerDependencies: + - '@simplewebauthn/browser' + - '@simplewebauthn/server' + - nodemailer + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.0 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@ioredis/commands@1.10.0': {} + + '@ioredis/commands@1.5.1': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + optional: true + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@next/env@15.5.19': {} + + '@next/eslint-plugin-next@15.5.19': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.5.19': + optional: true + + '@next/swc-darwin-x64@15.5.19': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.19': + optional: true + + '@next/swc-linux-arm64-musl@15.5.19': + optional: true + + '@next/swc-linux-x64-gnu@15.5.19': + optional: true + + '@next/swc-linux-x64-musl@15.5.19': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.19': + optional: true + + '@next/swc-win32-x64-msvc@15.5.19': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@opentelemetry/api@1.9.0': {} + + '@panva/hkdf@1.2.1': {} + + '@petamoriken/float16@3.9.3': {} + + '@playwright/test@1.61.0': + dependencies: + playwright: 1.61.0 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.16.1': {} + + '@smithy/core@3.25.1': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.4.1': + dependencies: + '@smithy/core': 3.25.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/types@4.15.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.4.1': + dependencies: + '@smithy/core': 3.25.1 + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.6 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.1 + + '@tailwindcss/oxide-android-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide@4.3.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-x64': 4.3.1 + '@tailwindcss/oxide-freebsd-x64': 4.3.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-x64-musl': 4.3.1 + '@tailwindcss/oxide-wasm32-wasi': 4.3.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 + + '@tailwindcss/postcss@4.3.1': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.1 + '@tailwindcss/oxide': 4.3.1 + postcss: 8.5.15 + tailwindcss: 4.3.1 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/bcryptjs@3.0.0': + dependencies: + bcryptjs: 3.0.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/diff-match-patch@1.0.36': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.19.21': + dependencies: + undici-types: 6.21.0 + + '@types/nodemailer@8.0.1': + dependencies: + '@types/node': 22.19.21 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 22.19.21 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + eslint: 9.39.4(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + + '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.61.1': {} + + '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@upstash/core-analytics@0.0.10': + dependencies: + '@upstash/redis': 1.38.0 + + '@upstash/ratelimit@2.0.8(@upstash/redis@1.38.0)': + dependencies: + '@upstash/core-analytics': 0.0.10 + '@upstash/redis': 1.38.0 + + '@upstash/redis@1.38.0': + dependencies: + uncrypto: 0.1.3 + + '@vitejs/plugin-react@4.7.0(vite@7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.6': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))': + dependencies: + '@vitest/spy': 3.2.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + + '@vitest/pretty-format@3.2.6': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.6': + dependencies: + '@vitest/utils': 3.2.6 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.6': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ai@4.3.19(react@19.2.7)(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/react': 1.2.12(react@19.2.7)(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.76 + optionalDependencies: + react: 19.2.7 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws4fetch@1.0.20: {} + + axe-core@4.12.1: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.38: {} + + bcryptjs@3.0.3: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.38 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.376 + node-releases: 2.0.48 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer-from@1.1.2: {} + + bullmq@5.79.0: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.10.1 + msgpackr: 2.0.2 + node-abort-controller: 3.1.1 + semver: 7.8.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001799: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + check-error@2.1.3: {} + + client-only@0.0.1: {} + + cluster-key-slot@1.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + denque@2.1.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + diff-match-patch@1.0.5: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dotenv-cli@11.0.0: + dependencies: + cross-spawn: 7.0.6 + dotenv: 17.4.2 + dotenv-expand: 12.0.3 + minimist: 1.2.8 + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.4.2: {} + + drizzle-kit@0.30.6: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.19.12 + esbuild-register: 3.6.0(esbuild@0.19.12) + gel: 2.2.0 + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@types/pg@8.20.0)(gel@2.2.0)(pg@8.22.0): + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/pg': 8.20.0 + gel: 2.2.0 + pg: 8.22.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.376: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.21.6: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + env-paths@3.0.0: {} + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.1 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.1: + dependencies: + es-abstract-get: 1.0.0 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-register@3.6.0(esbuild@0.19.12): + dependencies: + debug: 4.4.3 + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@15.5.19(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 15.5.19 + '@rushstack/eslint-patch': 1.16.1 + '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.7.0)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + hasown: 2.0.4 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.7.0)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.12.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.4(jiti@2.7.0) + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.7.0)): + dependencies: + eslint: 9.39.4(jiti@2.7.0) + + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.3 + eslint: 9.39.4(jiti@2.7.0) + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + eventsource-parser@3.1.0: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + gel@2.2.0: + dependencies: + '@petamoriken/float16': 3.9.3 + debug: 4.4.3 + env-paths: 3.0.0 + semver: 7.8.5 + shell-quote: 1.8.4 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.7.0: {} + + jose@6.2.3: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsondiffpatch@0.6.0: + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.6.2 + diff-match-patch: 1.0.5 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + langfuse-core@3.38.20: + dependencies: + mustache: 4.2.0 + + langfuse@3.38.20: + dependencies: + langfuse-core: 3.38.20 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + luxon@3.7.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.4: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 + optional: true + + msgpackr@2.0.2: + optionalDependencies: + msgpackr-extract: 3.0.4 + + mustache@4.2.0: {} + + nanoid@3.3.13: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next-auth@5.0.0-beta.31(next@15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(nodemailer@9.0.1)(react@19.2.7): + dependencies: + '@auth/core': 0.41.2(nodemailer@9.0.1) + next: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + optionalDependencies: + nodemailer: 9.0.1 + + next@15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@next/env': 15.5.19 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001799 + postcss: 8.4.31 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.19 + '@next/swc-darwin-x64': 15.5.19 + '@next/swc-linux-arm64-gnu': 15.5.19 + '@next/swc-linux-arm64-musl': 15.5.19 + '@next/swc-linux-x64-gnu': 15.5.19 + '@next/swc-linux-x64-musl': 15.5.19 + '@next/swc-win32-arm64-msvc': 15.5.19 + '@next/swc-win32-x64-msvc': 15.5.19 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.61.0 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-abort-controller@3.1.1: {} + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-releases@2.0.48: {} + + nodemailer@9.0.1: {} + + oauth4webapi@3.8.6: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + ollama-ai-provider@1.2.0(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + partial-json: 0.1.7 + optionalDependencies: + zod: 3.25.76 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + partial-json@0.1.7: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + playwright-core@1.61.0: {} + + playwright@1.61.0: + dependencies: + playwright-core: 1.61.0 + optionalDependencies: + fsevents: 2.3.2 + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.13 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.13 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + + prelude-ls@1.2.1: {} + + prettier@3.8.4: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-refresh@0.17.0: {} + + react@19.2.7: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.27.0: {} + + secure-json-parse@2.7.0: {} + + semver@6.3.1: {} + + semver@7.8.1: {} + + semver@7.8.5: {} + + server-only@0.0.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.4: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7): + dependencies: + client-only: 0.0.1 + react: 19.2.7 + optionalDependencies: + '@babel/core': 7.29.7 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swr@2.4.1(react@19.2.7): + dependencies: + dequal: 2.0.3 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + + tailwindcss@4.3.1: {} + + tapable@2.3.3: {} + + throttleit@2.1.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.22.4: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + uncrypto@0.1.3: {} + + undici-types@6.21.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + + vite-node@3.2.4(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.19.21 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + tsx: 4.22.4 + + vitest@3.2.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.6 + '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) + '@vitest/pretty-format': 3.2.6 + '@vitest/runner': 3.2.6 + '@vitest/snapshot': 3.2.6 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.5(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + vite-node: 3.2.4(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.21 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + xtend@4.0.2: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3c11f27 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +allowBuilds: + esbuild: true + msgpackr-extract: true + sharp: true + unrs-resolver: true diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..a34a3d5 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..a66a7c8 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,8 @@ +/** @type {import('prettier').Config} */ +export default { + semi: true, + singleQuote: true, + trailingComma: 'all', + printWidth: 100, + tabWidth: 2, +}; diff --git a/scripts/seed.ts b/scripts/seed.ts new file mode 100644 index 0000000..6be05be --- /dev/null +++ b/scripts/seed.ts @@ -0,0 +1,185 @@ +/** + * Seed script: JS Closures corpus + blueprint structure. + * + * Idempotent: exits early if source_chunks for this topic already exist. + * Use --force to re-seed (drops + re-inserts). + * + * Usage: + * pnpm seed # idempotent + * pnpm seed -- --force # drop and re-seed + */ + +import 'dotenv/config'; +import { like, eq } from 'drizzle-orm'; +import { db, pool } from '../src/lib/db/index.js'; +import { + sourceChunks, + blueprints, + concepts, + misconceptions, + lessons, + segments, + checkpoints, +} from '../src/lib/db/schema.js'; +import { embedTexts } from '../src/lib/generation/embed.js'; +import { + JS_CLOSURES_CHUNKS, + JS_CLOSURES_BLUEPRINT, + JS_CLOSURES_CONCEPTS, + JS_CLOSURES_MISCONCEPTIONS, + JS_CLOSURES_SEGMENTS, +} from '../src/lib/db/seed/js-closures.js'; + +const FORCE = process.argv.includes('--force'); + +async function seed() { + console.log('Seeding JS Closures corpus...'); + + // Idempotency check + if (!FORCE) { + const existing = await db + .select({ id: sourceChunks.id }) + .from(sourceChunks) + .where(like(sourceChunks.docRef, 'js-closures/%')) + .limit(1); + + if (existing.length > 0) { + console.log('Already seeded. Run with --force to re-seed.'); + return; + } + } else { + console.log('--force: removing existing js-closures data...'); + await db.delete(sourceChunks).where(like(sourceChunks.docRef, 'js-closures/%')); + + const existingBlueprint = await db + .select({ id: blueprints.id }) + .from(blueprints) + .where(eq(blueprints.intentKey, JS_CLOSURES_BLUEPRINT.intentKey)) + .limit(1); + + if (existingBlueprint.length > 0) { + const bpId = existingBlueprint[0].id; + + // Delete bottom-up to respect FK constraints: + // checkpoints → segments → lessons, misconceptions → concepts → blueprint + const lessonRows = await db.select({ id: lessons.id }).from(lessons).where(eq(lessons.blueprintId, bpId)); + for (const lesson of lessonRows) { + const segRows = await db.select({ id: segments.id }).from(segments).where(eq(segments.lessonId, lesson.id)); + for (const seg of segRows) { + await db.delete(checkpoints).where(eq(checkpoints.segmentId, seg.id)); + } + await db.delete(segments).where(eq(segments.lessonId, lesson.id)); + } + await db.delete(lessons).where(eq(lessons.blueprintId, bpId)); + + const conceptRows = await db.select({ id: concepts.id }).from(concepts).where(eq(concepts.blueprintId, bpId)); + for (const concept of conceptRows) { + await db.delete(misconceptions).where(eq(misconceptions.conceptId, concept.id)); + } + await db.delete(concepts).where(eq(concepts.blueprintId, bpId)); + await db.delete(blueprints).where(eq(blueprints.id, bpId)); + } + } + + // 1. Embed all chunks + blueprint intent text in one batch + // Lowercase matches normalizeIntent's query normalization (input.toLowerCase()) + const intentText = JS_CLOSURES_BLUEPRINT.title.toLowerCase(); // "javascript closures" + const allTexts = [intentText, ...JS_CLOSURES_CHUNKS.map((c) => c.text)]; + console.log(`Embedding ${allTexts.length} texts (1 blueprint + ${JS_CLOSURES_CHUNKS.length} chunks)...`); + const [blueprintEmbedding, ...chunkEmbeddings] = await embedTexts(allTexts); + console.log('Embeddings done.'); + + // 2. Insert source_chunks + const insertedChunks = await db + .insert(sourceChunks) + .values( + JS_CLOSURES_CHUNKS.map((chunk, i) => ({ + docRef: chunk.docRef, + text: chunk.text, + embedding: chunkEmbeddings[i], + })), + ) + .returning({ id: sourceChunks.id, docRef: sourceChunks.docRef }); + + console.log(`Inserted ${insertedChunks.length} source chunks.`); + + // 3. Insert blueprint (with embedding so normalizeIntent similarity search finds it) + const [blueprint] = await db + .insert(blueprints) + .values({ ...JS_CLOSURES_BLUEPRINT, embedding: blueprintEmbedding, status: 'published' }) + .returning({ id: blueprints.id }); + + console.log(`Inserted blueprint: ${blueprint.id}`); + + // 4. Insert concepts + const insertedConcepts = await db + .insert(concepts) + .values(JS_CLOSURES_CONCEPTS.map((c) => ({ ...c, blueprintId: blueprint.id }))) + .returning({ id: concepts.id, name: concepts.name }); + + console.log(`Inserted ${insertedConcepts.length} concepts.`); + + // 5. Insert misconceptions (keyed by concept name) + const conceptByName = Object.fromEntries(insertedConcepts.map((c) => [c.name, c.id])); + + const misconceptionRows = JS_CLOSURES_MISCONCEPTIONS.flatMap((m) => { + const conceptId = conceptByName[m.conceptName]; + if (!conceptId) { + console.warn(`No concept found for misconception "${m.conceptName}" — skipping.`); + return []; + } + return [{ conceptId, tag: m.tag, signature: m.signature, verifyStatus: 'pass' as const }]; + }); + + if (misconceptionRows.length > 0) { + await db.insert(misconceptions).values(misconceptionRows); + console.log(`Inserted ${misconceptionRows.length} misconceptions.`); + } + + // 6. Insert pre-built lesson (so app works without running generation workers) + const chunkIdByDocRef = Object.fromEntries(insertedChunks.map((c) => [c.docRef, c.id])); + + const [lesson] = await db + .insert(lessons) + .values({ blueprintId: blueprint.id, ord: 0, estMinutes: 10 }) + .returning({ id: lessons.id }); + + console.log(`Inserted lesson: ${lesson.id}`); + + for (const seg of JS_CLOSURES_SEGMENTS) { + const sourceChunkIds = seg.sourceDocRefs + .map((ref) => chunkIdByDocRef[ref]) + .filter((id): id is string => !!id); + + const [insertedSegment] = await db + .insert(segments) + .values({ + lessonId: lesson.id, + ord: seg.ord, + bodyJson: { text: seg.body }, + sourceChunkIds, + verifyStatus: 'pass', + }) + .returning({ id: segments.id }); + + await db.insert(checkpoints).values({ + segmentId: insertedSegment.id, + prompt: seg.checkpoint.prompt, + kind: seg.checkpoint.kind, + referenceAnswer: seg.checkpoint.referenceAnswer, + rubricJson: seg.checkpoint.rubric, + verifyStatus: 'pass', + }); + + console.log(` Segment ${seg.ord}: ${seg.sourceDocRefs.join(', ')}`); + } + + console.log('Seed complete.'); +} + +seed() + .catch((err) => { + console.error('Seed failed:', err); + process.exit(1); + }) + .finally(() => pool.end()); diff --git a/src/app/admin/blueprints/blueprints-client.tsx b/src/app/admin/blueprints/blueprints-client.tsx new file mode 100644 index 0000000..f236b8d --- /dev/null +++ b/src/app/admin/blueprints/blueprints-client.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +type Blueprint = { id: string; intentKey: string; title: string; status: 'draft' | 'verifying' | 'published' | 'flagged'; createdAt: Date }; +type Status = Blueprint['status']; + +const STATUS_OPTIONS: Status[] = ['draft', 'verifying', 'published', 'flagged']; + +export default function AdminBlueprintsClient({ blueprints }: { blueprints: Blueprint[] }) { + const router = useRouter(); + const [busy, setBusy] = useState(null); + + const setStatus = async (id: string, status: Status) => { + setBusy(id); + await fetch(`/api/admin/blueprints/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status }), + }); + setBusy(null); + router.refresh(); + }; + + return ( +
+

Blueprints

+
+ + + + {['Title', 'Intent key', 'Status', 'Created', 'Set status'].map((h) => ( + + ))} + + + + {blueprints.map((bp) => ( + + + + + + + + ))} + +
{h}
{bp.title}{bp.intentKey}{bp.status}{new Date(bp.createdAt).toLocaleDateString()} + +
+
+
+ ); +} + +const s = { + heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-6)' }, + table: { width: '100%', borderCollapse: 'collapse' as const, fontFamily: 'var(--font-display)', fontSize: '0.875rem' }, + th: { textAlign: 'left' as const, padding: 'var(--space-2) var(--space-3)', borderBottom: '2px solid var(--color-border)', color: 'var(--color-ink-faint)', fontWeight: 600 }, + td: { padding: 'var(--space-2) var(--space-3)', borderBottom: '1px solid var(--color-border-subtle)', color: 'var(--color-ink)' }, + select: { padding: 'var(--space-1) var(--space-2)', border: '1px solid var(--color-border)', borderRadius: '4px', background: 'var(--color-surface)', color: 'var(--color-ink)', fontSize: '0.875rem' }, +} as const; diff --git a/src/app/admin/blueprints/page.tsx b/src/app/admin/blueprints/page.tsx new file mode 100644 index 0000000..7d5c0ff --- /dev/null +++ b/src/app/admin/blueprints/page.tsx @@ -0,0 +1,16 @@ +import { requireAdmin } from '@/lib/auth-helpers'; +import { db } from '@/lib/db'; +import { blueprints } from '@/lib/db/schema'; +import { asc } from 'drizzle-orm'; +import AdminBlueprintsClient from './blueprints-client'; + +export default async function AdminBlueprintsPage() { + await requireAdmin(); + + const rows = await db + .select({ id: blueprints.id, intentKey: blueprints.intentKey, title: blueprints.title, status: blueprints.status, createdAt: blueprints.createdAt }) + .from(blueprints) + .orderBy(asc(blueprints.createdAt)); + + return ; +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..fd878c2 --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,61 @@ +import { requireAdmin } from '@/lib/auth-helpers'; +import Link from 'next/link'; +import type { ReactNode } from 'react'; + +export default async function AdminLayout({ children }: { children: ReactNode }) { + await requireAdmin(); + + return ( +
+ +
{children}
+
+ ); +} + +const s = { + shell: { display: 'flex', minHeight: '100svh' }, + sidebar: { + width: '14rem', + flexShrink: 0, + borderRight: '1px solid var(--color-border)', + padding: 'var(--space-8) var(--space-6)', + display: 'flex', + flexDirection: 'column' as const, + gap: 'var(--space-6)', + }, + brand: { + fontFamily: 'var(--font-display)', + fontWeight: 600, + fontSize: '0.875rem', + color: 'var(--color-ink-faint)', + textTransform: 'uppercase' as const, + letterSpacing: '0.1em', + }, + nav: { listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-1)' }, + navLink: { + display: 'block', + padding: 'var(--space-2) var(--space-3)', + borderRadius: '4px', + fontFamily: 'var(--font-display)', + fontSize: '0.9375rem', + color: 'var(--color-ink)', + textDecoration: 'none', + }, + content: { flex: 1, padding: 'var(--space-10) var(--space-10)' }, +} as const; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..b0510ae --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,30 @@ +import { db } from '@/lib/db'; +import { users, sessions } from '@/lib/db/schema'; +import { count, gte } from 'drizzle-orm'; + +export default async function AdminOverviewPage() { + const [{ total }] = await db.select({ total: count() }).from(users); + const dayAgo = new Date(Date.now() - 86_400_000); + const [{ active }] = await db.select({ active: count() }).from(sessions).where(gte(sessions.expires, dayAgo)); + + return ( +
+

+ Overview +

+
+ + +
+
+ ); +} + +function Stat({ label, value }: { label: string; value: number }) { + return ( +
+

{value}

+

{label}

+
+ ); +} diff --git a/src/app/admin/reports/page.tsx b/src/app/admin/reports/page.tsx new file mode 100644 index 0000000..2295b70 --- /dev/null +++ b/src/app/admin/reports/page.tsx @@ -0,0 +1,16 @@ +import { requireAdmin } from '@/lib/auth-helpers'; +import { db } from '@/lib/db'; +import { contentReports } from '@/lib/db/schema'; +import { asc } from 'drizzle-orm'; +import AdminReportsClient from './reports-client'; + +export default async function AdminReportsPage() { + await requireAdmin(); + + const rows = await db + .select() + .from(contentReports) + .orderBy(asc(contentReports.createdAt)); + + return ; +} diff --git a/src/app/admin/reports/reports-client.tsx b/src/app/admin/reports/reports-client.tsx new file mode 100644 index 0000000..2ac9fb5 --- /dev/null +++ b/src/app/admin/reports/reports-client.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +type Report = { id: string; checkpointId: string; gradeId: string | null; reason: string; notes: string | null; createdAt: Date }; + +export default function AdminReportsClient({ reports }: { reports: Report[] }) { + const router = useRouter(); + const [busy, setBusy] = useState(null); + + const act = async (id: string, action: 'dismiss' | 're-verify') => { + setBusy(id); + await fetch(`/api/admin/reports/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action }), + }); + setBusy(null); + router.refresh(); + }; + + return ( +
+

Content reports

+ {reports.length === 0 ? ( +

No pending reports.

+ ) : ( +
+ {reports.map((r) => ( +
+
+ {r.reason} + {new Date(r.createdAt).toLocaleDateString()} +
+ {r.notes &&

{r.notes}

} +

+ checkpoint {r.checkpointId.slice(0, 8)}… +

+
+ + +
+
+ ))} +
+ )} +
+ ); +} + +const s = { + heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-6)' }, + card: { padding: 'var(--space-5)', border: '1px solid var(--color-border)', borderRadius: '4px', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-2)' }, + cardMeta: { display: 'flex', gap: 'var(--space-3)', alignItems: 'center' }, + tag: { fontFamily: 'var(--font-display)', fontSize: '0.75rem', fontWeight: 600, padding: '2px 8px', background: 'var(--color-accent-subtle)', color: 'var(--color-accent)', borderRadius: '3px' }, + date: { fontSize: '0.8125rem', color: 'var(--color-ink-faint)', fontFamily: 'var(--font-display)' }, + notes: { fontSize: '0.9375rem', color: 'var(--color-ink-muted)' }, + btn: { padding: 'var(--space-1) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', cursor: 'pointer', fontFamily: 'var(--font-display)', fontSize: '0.875rem', color: 'var(--color-ink)' }, +} as const; diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx new file mode 100644 index 0000000..101290a --- /dev/null +++ b/src/app/admin/settings/page.tsx @@ -0,0 +1,7 @@ +import { getAllSettings } from '@/lib/site-config'; +import AdminSettingsClient from './settings-client'; + +export default async function AdminSettingsPage() { + const settings = await getAllSettings(); + return ; +} diff --git a/src/app/admin/settings/settings-client.tsx b/src/app/admin/settings/settings-client.tsx new file mode 100644 index 0000000..de7dc54 --- /dev/null +++ b/src/app/admin/settings/settings-client.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useState } from 'react'; + +interface Settings { + require_auth: boolean; + signups_enabled: boolean; +} + +export default function AdminSettingsClient({ initial }: { initial: Settings }) { + const [settings, setSettings] = useState(initial); + const [saving, setSaving] = useState(null); + const [message, setMessage] = useState(null); + + const toggle = async (key: keyof Settings) => { + setSaving(key); + setMessage(null); + const next = { [key]: !settings[key] }; + const res = await fetch('/api/admin/settings', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(next), + }); + setSaving(null); + if (res.ok) { + const data = (await res.json()) as Settings; + setSettings(data); + setMessage('Saved.'); + setTimeout(() => setMessage(null), 2000); + } else { + setMessage('Failed to save.'); + } + }; + + return ( +
+

Site settings

+ {message &&

{message}

} + +
+

Access control

+
+
+

Require account to access site

+

Anonymous access is blocked. All visitors must sign in.

+
+ +
+ +
+
+

Allow new registrations

+

When off, the registration form returns 403. Existing accounts still work.

+
+ +
+
+
+ ); +} + +const s = { + heading: { fontFamily: 'var(--font-display)', fontSize: '1.25rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-8)' }, + toast: { padding: 'var(--space-2) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', fontSize: '0.875rem', color: 'var(--color-ink-muted)', marginBottom: 'var(--space-4)' }, + section: { marginBottom: 'var(--space-10)' }, + sectionHeading: { fontFamily: 'var(--font-display)', fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-ink-faint)', textTransform: 'uppercase' as const, letterSpacing: '0.08em', marginBottom: 'var(--space-4)' }, + row: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 'var(--space-6)', padding: 'var(--space-4) 0', borderBottom: '1px solid var(--color-border)' }, + label: { fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500, color: 'var(--color-ink)', margin: '0 0 var(--space-1)' }, + desc: { fontFamily: 'var(--font-body)', fontSize: '0.875rem', color: 'var(--color-ink-muted)', margin: 0 }, + toggle: { flexShrink: 0, padding: 'var(--space-2) var(--space-4)', border: '1px solid var(--color-border)', borderRadius: '4px', fontFamily: 'var(--font-display)', fontSize: '0.875rem', fontWeight: 500, cursor: 'pointer', background: 'var(--color-surface)', color: 'var(--color-ink-muted)' }, + toggleOn: { background: 'var(--color-accent)', color: '#fff', border: '1px solid var(--color-accent)' }, +} as const; diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..d23a2ad --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,16 @@ +import { requireAdmin } from '@/lib/auth-helpers'; +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +import { asc } from 'drizzle-orm'; +import AdminUsersClient from './users-client'; + +export default async function AdminUsersPage() { + await requireAdmin(); + + const rows = await db + .select({ id: users.id, name: users.name, email: users.email, role: users.role, createdAt: users.createdAt }) + .from(users) + .orderBy(asc(users.createdAt)); + + return ; +} diff --git a/src/app/admin/users/users-client.tsx b/src/app/admin/users/users-client.tsx new file mode 100644 index 0000000..fd2a207 --- /dev/null +++ b/src/app/admin/users/users-client.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +type UserRow = { id: string; name: string | null; email: string | null; role: 'learner' | 'admin' | 'suspended'; createdAt: Date }; + +export default function AdminUsersClient({ users }: { users: UserRow[] }) { + const router = useRouter(); + const [busy, setBusy] = useState(null); + + const action = async (id: string, body: object) => { + setBusy(id); + await fetch(`/api/admin/users/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + setBusy(null); + router.refresh(); + }; + + const del = async (id: string) => { + if (!confirm('Delete this user permanently?')) return; + setBusy(id); + await fetch(`/api/admin/users/${id}`, { method: 'DELETE' }); + setBusy(null); + router.refresh(); + }; + + return ( +
+

Users

+
+ + + + {['Email', 'Name', 'Role', 'Joined', 'Actions'].map((h) => ( + + ))} + + + + {users.map((u) => ( + + + + + + + + ))} + +
{h}
{u.email ?? '—'}{u.name ?? '—'}{u.role}{new Date(u.createdAt).toLocaleDateString()} + {u.role !== 'admin' && ( + + )} + {u.role !== 'suspended' && ( + + )} + {u.role === 'suspended' && ( + + )} + +
+
+
+ ); +} + +const s = { + heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-6)' }, + table: { width: '100%', borderCollapse: 'collapse' as const, fontFamily: 'var(--font-display)', fontSize: '0.875rem' }, + th: { textAlign: 'left' as const, padding: 'var(--space-2) var(--space-3)', borderBottom: '2px solid var(--color-border)', color: 'var(--color-ink-faint)', fontWeight: 600 }, + td: { padding: 'var(--space-2) var(--space-3)', borderBottom: '1px solid var(--color-border-subtle)', color: 'var(--color-ink)' }, + btn: { padding: 'var(--space-1) var(--space-3)', background: 'var(--color-accent-subtle)', color: 'var(--color-accent)', border: '1px solid var(--color-accent)', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8125rem' }, + dangerBtn: { padding: 'var(--space-1) var(--space-3)', background: 'var(--color-misconception-subtle)', color: 'var(--color-misconception)', border: '1px solid var(--color-misconception)', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8125rem' }, +} as const; diff --git a/src/app/api/admin/blueprints/[id]/__tests__/route.test.ts b/src/app/api/admin/blueprints/[id]/__tests__/route.test.ts new file mode 100644 index 0000000..b439ca3 --- /dev/null +++ b/src/app/api/admin/blueprints/[id]/__tests__/route.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db', () => ({ + db: { + update: vi.fn(), + }, +})); + +vi.mock('@/lib/db/schema', () => ({ + blueprints: { id: 'id' }, +})); + +vi.mock('drizzle-orm', () => ({ + eq: vi.fn().mockReturnValue('eq-condition'), +})); + +import { PATCH } from '../route'; +import { db } from '@/lib/db'; +import { requireAdmin } from '@/lib/auth-helpers'; + +function makeRequest(body: unknown, contentType = 'application/json'): NextRequest { + return new NextRequest('http://localhost/api/admin/blueprints/test-id', { + method: 'PATCH', + headers: { 'Content-Type': contentType }, + body: JSON.stringify(body), + }); +} + +function makeInvalidJsonRequest(): NextRequest { + return new NextRequest('http://localhost/api/admin/blueprints/test-id', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: '{invalid-json', + }); +} + +const mockWhere = vi.fn().mockResolvedValue([]); +const mockSet = vi.fn().mockReturnValue({ where: mockWhere }); +const mockUpdate = vi.fn().mockReturnValue({ set: mockSet }); + +describe('PATCH /api/admin/blueprints/[id]', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(db).update = mockUpdate; + mockUpdate.mockReturnValue({ set: mockSet }); + mockSet.mockReturnValue({ where: mockWhere }); + mockWhere.mockResolvedValue([]); + }); + + it('returns 200 with ok:true on valid status update', async () => { + const res = await PATCH(makeRequest({ status: 'published' }), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }); + + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + expect(mockUpdate).toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledWith({ status: 'published' }); + }); + + it('returns 200 with each valid status value', async () => { + const statuses = ['draft', 'verifying', 'published', 'flagged'] as const; + + for (const status of statuses) { + vi.clearAllMocks(); + vi.mocked(db).update = mockUpdate; + mockUpdate.mockReturnValue({ set: mockSet }); + mockSet.mockReturnValue({ where: mockWhere }); + mockWhere.mockResolvedValue([]); + + const res = await PATCH(makeRequest({ status }), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }); + + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + } + }); + + it('returns 400 on invalid status value', async () => { + const res = await PATCH(makeRequest({ status: 'invalid-status' }), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }); + + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + }); + + it('returns 400 on invalid JSON body', async () => { + const res = await PATCH(makeInvalidJsonRequest(), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }); + + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toEqual({ error: 'Invalid JSON' }); + }); + + it('returns 400 on extra unknown fields that fail schema', async () => { + // PatchSchema uses z.object with no strict — unknown fields are stripped, so + // an entirely missing-status (empty object) is still valid (status is optional). + // Test that a wrong type for status triggers validation failure. + const res = await PATCH(makeRequest({ status: 123 }), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }); + + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + }); + + it('calls requireAdmin for authorization', async () => { + await PATCH(makeRequest({ status: 'draft' }), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }); + + expect(vi.mocked(requireAdmin)).toHaveBeenCalled(); + }); + + it('returns 403 when requireAdmin throws', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('Forbidden')); + + await expect( + PATCH(makeRequest({ status: 'draft' }), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }) + ).rejects.toThrow('Forbidden'); + }); + + it('passes the correct id from params to the db update', async () => { + const targetId = 'specific-blueprint-id-123'; + + await PATCH(makeRequest({ status: 'flagged' }), { + params: Promise.resolve({ id: targetId }), + }); + + expect(mockUpdate).toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledWith({ status: 'flagged' }); + // eq() should have been called with the id + const { eq } = await import('drizzle-orm'); + expect(vi.mocked(eq)).toHaveBeenCalledWith(expect.anything(), targetId); + }); + + it('returns 200 with empty body (all fields optional)', async () => { + const res = await PATCH(makeRequest({}), { + params: Promise.resolve({ id: 'blueprint-uuid' }), + }); + + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + }); +}); diff --git a/src/app/api/admin/blueprints/[id]/route.ts b/src/app/api/admin/blueprints/[id]/route.ts new file mode 100644 index 0000000..56170c4 --- /dev/null +++ b/src/app/api/admin/blueprints/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { blueprints } from '@/lib/db/schema'; +import { requireAdmin } from '@/lib/auth-helpers'; + +const PatchSchema = z.object({ + status: z.enum(['draft', 'verifying', 'published', 'flagged']).optional(), +}); + +export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }): Promise { + await requireAdmin(); + const { id } = await params; + + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = PatchSchema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, { status: 400 }); + + await db.update(blueprints).set(parsed.data).where(eq(blueprints.id, id)); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/admin/blueprints/__tests__/route.test.ts b/src/app/api/admin/blueprints/__tests__/route.test.ts new file mode 100644 index 0000000..933c2d0 --- /dev/null +++ b/src/app/api/admin/blueprints/__tests__/route.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { mockSelect } = vi.hoisted(() => ({ mockSelect: vi.fn() })); + +vi.mock('@/lib/db', () => ({ db: { select: mockSelect } })); + +import { GET } from '../route'; +import { requireAdmin } from '@/lib/auth-helpers'; + +const fixtureBlueprintsRows = [ + { id: 'bp-1', intentKey: 'typescript-basics', title: 'TypeScript Basics', status: 'published', createdAt: new Date('2024-01-01') }, + { id: 'bp-2', intentKey: 'react-hooks', title: 'React Hooks', status: 'draft', createdAt: new Date('2024-01-02') }, +]; + +function mockChain(result: unknown) { + return { from: vi.fn().mockReturnValue({ orderBy: vi.fn().mockResolvedValue(result) }) }; +} + +describe('GET /api/admin/blueprints', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns blueprints list on happy path', async () => { + mockSelect.mockReturnValue(mockChain(fixtureBlueprintsRows).from()); + mockSelect.mockReturnValue(mockChain(fixtureBlueprintsRows)); + const res = await GET(); + const body = await res.json(); + expect(res.status).toBe(200); + expect(body.blueprints).toHaveLength(2); + }); + + it('returns empty array when no blueprints', async () => { + mockSelect.mockReturnValue(mockChain([])); + const res = await GET(); + const body = await res.json(); + expect(res.status).toBe(200); + expect(body.blueprints).toEqual([]); + }); + + it('calls requireAdmin', async () => { + mockSelect.mockReturnValue(mockChain([])); + await GET(); + expect(requireAdmin).toHaveBeenCalledOnce(); + }); + + it('propagates requireAdmin rejection', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('forbidden')); + await expect(GET()).rejects.toThrow('forbidden'); + }); + + it('propagates DB errors', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ orderBy: vi.fn().mockRejectedValue(new Error('DB error')) }), + }); + await expect(GET()).rejects.toThrow('DB error'); + }); +}); diff --git a/src/app/api/admin/blueprints/route.ts b/src/app/api/admin/blueprints/route.ts new file mode 100644 index 0000000..20bad6a --- /dev/null +++ b/src/app/api/admin/blueprints/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; +import { asc } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { blueprints } from '@/lib/db/schema'; +import { requireAdmin } from '@/lib/auth-helpers'; + +export async function GET(): Promise { + await requireAdmin(); + const rows = await db + .select({ id: blueprints.id, intentKey: blueprints.intentKey, title: blueprints.title, status: blueprints.status, createdAt: blueprints.createdAt }) + .from(blueprints) + .orderBy(asc(blueprints.createdAt)); + return NextResponse.json({ blueprints: rows }); +} diff --git a/src/app/api/admin/reports/[id]/__tests__/route.test.ts b/src/app/api/admin/reports/[id]/__tests__/route.test.ts new file mode 100644 index 0000000..5f4aa16 --- /dev/null +++ b/src/app/api/admin/reports/[id]/__tests__/route.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db', () => ({ + db: { + select: vi.fn(), + insert: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, +})); + +import { PATCH } from '../route'; +import { db } from '@/lib/db'; +import { requireAdmin } from '@/lib/auth-helpers'; + +function makeRequest(body: unknown): NextRequest { + return new NextRequest('http://localhost/api/admin/reports/test-id', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +function makeInvalidRequest(): NextRequest { + return new NextRequest('http://localhost/api/admin/reports/test-id', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: 'not-json{{{', + }); +} + +describe('PATCH /api/admin/reports/[id]', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 400 on invalid JSON', async () => { + const res = await PATCH(makeInvalidRequest(), { params: Promise.resolve({ id: 'test-id' }) }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBe('Invalid JSON'); + }); + + it('returns 400 on invalid action', async () => { + const res = await PATCH(makeRequest({ action: 'invalid-action' }), { params: Promise.resolve({ id: 'test-id' }) }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBeDefined(); + }); + + it('returns 400 when action is missing', async () => { + const res = await PATCH(makeRequest({}), { params: Promise.resolve({ id: 'test-id' }) }); + expect(res.status).toBe(400); + }); + + it('calls requireAdmin for auth guard', async () => { + vi.mocked(db).delete = vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }); + + await PATCH(makeRequest({ action: 'dismiss' }), { params: Promise.resolve({ id: 'test-id' }) }); + + expect(requireAdmin).toHaveBeenCalled(); + }); + + it('returns 401/403 when requireAdmin rejects', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('redirect')); + + await expect( + PATCH(makeRequest({ action: 'dismiss' }), { params: Promise.resolve({ id: 'test-id' }) }) + ).rejects.toThrow('redirect'); + }); + + describe('dismiss action', () => { + it('deletes the report and returns {ok: true}', async () => { + const mockWhere = vi.fn().mockResolvedValue([]); + vi.mocked(db).delete = vi.fn().mockReturnValue({ where: mockWhere }); + + const res = await PATCH(makeRequest({ action: 'dismiss' }), { params: Promise.resolve({ id: 'report-uuid' }) }); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ ok: true }); + expect(db.delete).toHaveBeenCalled(); + expect(mockWhere).toHaveBeenCalled(); + }); + + it('does not call select or update for dismiss', async () => { + const mockWhere = vi.fn().mockResolvedValue([]); + vi.mocked(db).delete = vi.fn().mockReturnValue({ where: mockWhere }); + + await PATCH(makeRequest({ action: 'dismiss' }), { params: Promise.resolve({ id: 'report-uuid' }) }); + + expect(db.select).not.toHaveBeenCalled(); + expect(db.update).not.toHaveBeenCalled(); + }); + }); + + describe('re-verify action', () => { + it('selects report, updates checkpoint to pending, returns {ok: true}', async () => { + const mockSelectWhere = vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([{ checkpointId: 'checkpoint-uuid' }]), + }); + vi.mocked(db).select = vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: mockSelectWhere, + }), + }); + + const mockUpdateWhere = vi.fn().mockResolvedValue([]); + vi.mocked(db).update = vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ where: mockUpdateWhere }), + }); + + const res = await PATCH(makeRequest({ action: 're-verify' }), { params: Promise.resolve({ id: 'report-uuid' }) }); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ ok: true }); + expect(db.select).toHaveBeenCalled(); + expect(db.update).toHaveBeenCalled(); + expect(mockUpdateWhere).toHaveBeenCalled(); + }); + + it('skips update when report is not found', async () => { + vi.mocked(db).select = vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + }); + + const res = await PATCH(makeRequest({ action: 're-verify' }), { params: Promise.resolve({ id: 'nonexistent-id' }) }); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ ok: true }); + expect(db.update).not.toHaveBeenCalled(); + }); + + it('does not call delete for re-verify', async () => { + vi.mocked(db).select = vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([{ checkpointId: 'checkpoint-uuid' }]), + }), + }), + }); + + vi.mocked(db).update = vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }), + }); + + await PATCH(makeRequest({ action: 're-verify' }), { params: Promise.resolve({ id: 'report-uuid' }) }); + + expect(db.delete).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/api/admin/reports/[id]/route.ts b/src/app/api/admin/reports/[id]/route.ts new file mode 100644 index 0000000..0b0beea --- /dev/null +++ b/src/app/api/admin/reports/[id]/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { contentReports, checkpoints } from '@/lib/db/schema'; +import { requireAdmin } from '@/lib/auth-helpers'; + +const PatchSchema = z.object({ + action: z.enum(['dismiss', 're-verify']), +}); + +export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }): Promise { + await requireAdmin(); + const { id } = await params; + + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = PatchSchema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, { status: 400 }); + + if (parsed.data.action === 'dismiss') { + await db.delete(contentReports).where(eq(contentReports.id, id)); + } else { + const [report] = await db.select({ checkpointId: contentReports.checkpointId }).from(contentReports).where(eq(contentReports.id, id)).limit(1); + if (report) { + await db.update(checkpoints).set({ verifyStatus: 'pending' }).where(eq(checkpoints.id, report.checkpointId)); + } + } + + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/admin/reports/__tests__/route.test.ts b/src/app/api/admin/reports/__tests__/route.test.ts new file mode 100644 index 0000000..fb99815 --- /dev/null +++ b/src/app/api/admin/reports/__tests__/route.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { mockSelect } = vi.hoisted(() => ({ mockSelect: vi.fn() })); + +vi.mock('@/lib/db', () => ({ db: { select: mockSelect } })); + +import { GET } from '../route'; +import { requireAdmin } from '@/lib/auth-helpers'; + +const sampleReports = [ + { id: 'r-1', checkpointId: 'cp-1', gradeId: null, reason: 'grade_wrong', notes: null, createdAt: new Date('2024-01-01') }, + { id: 'r-2', checkpointId: 'cp-2', gradeId: 'g-2', reason: 'content_error', notes: 'wrong info', createdAt: new Date('2024-01-02') }, +]; + +function mockChain(result: unknown) { + return { from: vi.fn().mockReturnValue({ orderBy: vi.fn().mockResolvedValue(result) }) }; +} + +describe('GET /api/admin/reports', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns reports list on happy path', async () => { + mockSelect.mockReturnValue(mockChain(sampleReports)); + const res = await GET(); + const body = await res.json(); + expect(res.status).toBe(200); + expect(body.reports).toHaveLength(2); + }); + + it('returns empty array when no reports', async () => { + mockSelect.mockReturnValue(mockChain([])); + const res = await GET(); + const body = await res.json(); + expect(res.status).toBe(200); + expect(body.reports).toEqual([]); + }); + + it('calls requireAdmin', async () => { + mockSelect.mockReturnValue(mockChain([])); + await GET(); + expect(requireAdmin).toHaveBeenCalledOnce(); + }); + + it('propagates requireAdmin rejection', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('forbidden')); + await expect(GET()).rejects.toThrow('forbidden'); + }); + + it('propagates DB errors', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ orderBy: vi.fn().mockRejectedValue(new Error('DB error')) }), + }); + await expect(GET()).rejects.toThrow('DB error'); + }); +}); diff --git a/src/app/api/admin/reports/route.ts b/src/app/api/admin/reports/route.ts new file mode 100644 index 0000000..f15d0bb --- /dev/null +++ b/src/app/api/admin/reports/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; +import { asc } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { contentReports } from '@/lib/db/schema'; +import { requireAdmin } from '@/lib/auth-helpers'; + +export async function GET(): Promise { + await requireAdmin(); + const rows = await db.select().from(contentReports).orderBy(asc(contentReports.createdAt)); + return NextResponse.json({ reports: rows }); +} diff --git a/src/app/api/admin/settings/route.ts b/src/app/api/admin/settings/route.ts new file mode 100644 index 0000000..e29dae8 --- /dev/null +++ b/src/app/api/admin/settings/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { requireAdmin } from '@/lib/auth-helpers'; +import { getAllSettings, setAuthRequired, setSignupsEnabled } from '@/lib/site-config'; + +const PatchSchema = z.object({ + require_auth: z.boolean().optional(), + signups_enabled: z.boolean().optional(), +}); + +export async function GET(): Promise { + await requireAdmin(); + const settings = await getAllSettings(); + return NextResponse.json(settings); +} + +export async function PATCH(req: NextRequest): Promise { + await requireAdmin(); + + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = PatchSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, { status: 400 }); + } + + const { require_auth, signups_enabled } = parsed.data; + await Promise.all([ + require_auth !== undefined ? setAuthRequired(require_auth) : Promise.resolve(), + signups_enabled !== undefined ? setSignupsEnabled(signups_enabled) : Promise.resolve(), + ]); + + const settings = await getAllSettings(); + return NextResponse.json(settings); +} diff --git a/src/app/api/admin/stats/__tests__/route.test.ts b/src/app/api/admin/stats/__tests__/route.test.ts new file mode 100644 index 0000000..585c920 --- /dev/null +++ b/src/app/api/admin/stats/__tests__/route.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { mockSelect } = vi.hoisted(() => ({ mockSelect: vi.fn() })); + +vi.mock('@/lib/db', () => ({ db: { select: mockSelect } })); + +import { GET } from '../route'; +import { requireAdmin } from '@/lib/auth-helpers'; + +describe('GET /api/admin/stats', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + function mockUserCount(total: number) { + mockSelect.mockReturnValueOnce({ from: vi.fn().mockResolvedValue([{ total }]) }); + } + + function mockSessionCount(active: number) { + mockSelect.mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([{ active }]) }), + }); + } + + it('returns totalUsers and activeToday', async () => { + mockUserCount(42); + mockSessionCount(7); + const res = await GET(); + const body = await res.json(); + expect(res.status).toBe(200); + expect(body).toEqual({ totalUsers: 42, activeToday: 7 }); + }); + + it('returns zero counts when empty', async () => { + mockUserCount(0); + mockSessionCount(0); + const res = await GET(); + const body = await res.json(); + expect(body).toEqual({ totalUsers: 0, activeToday: 0 }); + }); + + it('calls requireAdmin', async () => { + mockUserCount(1); + mockSessionCount(0); + await GET(); + expect(requireAdmin).toHaveBeenCalledOnce(); + }); + + it('propagates requireAdmin rejection', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('forbidden')); + await expect(GET()).rejects.toThrow('forbidden'); + }); + + it('calls select twice', async () => { + mockUserCount(5); + mockSessionCount(2); + await GET(); + expect(mockSelect).toHaveBeenCalledTimes(2); + }); + + it('propagates DB errors from user count query', async () => { + mockSelect.mockReturnValueOnce({ from: vi.fn().mockRejectedValue(new Error('DB error')) }); + await expect(GET()).rejects.toThrow('DB error'); + }); +}); diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts new file mode 100644 index 0000000..1a59edd --- /dev/null +++ b/src/app/api/admin/stats/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { users, sessions } from '@/lib/db/schema'; +import { requireAdmin } from '@/lib/auth-helpers'; +import { count, gte } from 'drizzle-orm'; + +export async function GET(): Promise { + await requireAdmin(); + + const [{ total }] = await db.select({ total: count() }).from(users); + + const dayAgo = new Date(Date.now() - 86_400_000); + const [{ active }] = await db + .select({ active: count() }) + .from(sessions) + .where(gte(sessions.expires, dayAgo)); + + return NextResponse.json({ totalUsers: total, activeToday: active }); +} diff --git a/src/app/api/admin/users/[id]/__tests__/route.test.ts b/src/app/api/admin/users/[id]/__tests__/route.test.ts new file mode 100644 index 0000000..f9df4d4 --- /dev/null +++ b/src/app/api/admin/users/[id]/__tests__/route.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db', () => ({ + db: { + update: vi.fn(), + delete: vi.fn(), + transaction: vi.fn(), + }, +})); + +import { db } from '@/lib/db'; +import { PATCH, DELETE } from '../route'; +import { requireAdmin } from '@/lib/auth-helpers'; + +function makeRequest(body?: unknown, method = 'PATCH'): NextRequest { + return new NextRequest('http://localhost/api/admin/users/some-uuid', { + method, + headers: { 'Content-Type': 'application/json' }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); +} + +const TEST_USER_ID = 'user-uuid-123'; + +describe('PATCH /api/admin/users/[id]', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(db).update = vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + }); + }); + + it('returns 200 with ok:true on valid role update', async () => { + const res = await PATCH(makeRequest({ role: 'admin' }), { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + }); + + it('calls db.update with the correct role', async () => { + await PATCH(makeRequest({ role: 'suspended' }), { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + expect(vi.mocked(db).update).toHaveBeenCalled(); + const setMock = vi.mocked(db).update({} as never).set; + expect(setMock).toHaveBeenCalledWith({ role: 'suspended' }); + }); + + it('returns 400 on invalid role value', async () => { + const res = await PATCH(makeRequest({ role: 'superuser' }), { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + }); + + it('returns 400 on invalid JSON', async () => { + const req = new NextRequest('http://localhost/api/admin/users/some-uuid', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: 'not-json{{{', + }); + const res = await PATCH(req, { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json.error).toBe('Invalid JSON'); + }); + + it('returns 403 when requireAdmin throws', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('Forbidden')); + await expect( + PATCH(makeRequest({ role: 'learner' }), { + params: Promise.resolve({ id: TEST_USER_ID }), + }) + ).rejects.toThrow('Forbidden'); + }); + + it('accepts learner as a valid role', async () => { + const res = await PATCH(makeRequest({ role: 'learner' }), { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + expect(res.status).toBe(200); + }); + + it('returns 200 when body has no role (optional field)', async () => { + const res = await PATCH(makeRequest({}), { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + expect(res.status).toBe(200); + }); +}); + +describe('DELETE /api/admin/users/[id]', () => { + let txMock: { + delete: ReturnType; + }; + + beforeEach(() => { + vi.clearAllMocks(); + + txMock = { + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + }; + + vi.mocked(db).transaction = vi.fn().mockImplementation(async (fn) => fn(txMock)); + }); + + it('returns 200 with ok:true on successful delete', async () => { + const res = await DELETE(makeRequest(undefined, 'DELETE'), { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + }); + + it('calls transaction and deletes responses, mastery, journeyInstances, users', async () => { + await DELETE(makeRequest(undefined, 'DELETE'), { + params: Promise.resolve({ id: TEST_USER_ID }), + }); + + expect(vi.mocked(db).transaction).toHaveBeenCalledOnce(); + expect(txMock.delete).toHaveBeenCalledTimes(4); + }); + + it('returns 403 when requireAdmin throws', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('Forbidden')); + await expect( + DELETE(makeRequest(undefined, 'DELETE'), { + params: Promise.resolve({ id: TEST_USER_ID }), + }) + ).rejects.toThrow('Forbidden'); + }); + + it('propagates transaction errors', async () => { + vi.mocked(db).transaction = vi.fn().mockRejectedValue(new Error('DB error')); + await expect( + DELETE(makeRequest(undefined, 'DELETE'), { + params: Promise.resolve({ id: TEST_USER_ID }), + }) + ).rejects.toThrow('DB error'); + }); +}); diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts new file mode 100644 index 0000000..5a96292 --- /dev/null +++ b/src/app/api/admin/users/[id]/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { users, responses, mastery, journeyInstances } from '@/lib/db/schema'; +import { requireAdmin } from '@/lib/auth-helpers'; + +const PatchSchema = z.object({ + role: z.enum(['learner', 'admin', 'suspended']).optional(), +}); + +export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }): Promise { + await requireAdmin(); + const { id } = await params; + + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = PatchSchema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, { status: 400 }); + + await db.update(users).set(parsed.data).where(eq(users.id, id)); + return NextResponse.json({ ok: true }); +} + +export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }): Promise { + await requireAdmin(); + const { id } = await params; + + await db.transaction(async (tx) => { + await tx.delete(responses).where(eq(responses.userId, id)); + await tx.delete(mastery).where(eq(mastery.userId, id)); + await tx.delete(journeyInstances).where(eq(journeyInstances.userId, id)); + await tx.delete(users).where(eq(users.id, id)); + }); + + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/admin/users/__tests__/route.test.ts b/src/app/api/admin/users/__tests__/route.test.ts new file mode 100644 index 0000000..42c5585 --- /dev/null +++ b/src/app/api/admin/users/__tests__/route.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +const { mockSelect } = vi.hoisted(() => ({ mockSelect: vi.fn() })); + +vi.mock('@/lib/db', () => ({ db: { select: mockSelect } })); + +import { GET } from '../route'; +import { requireAdmin } from '@/lib/auth-helpers'; + +const mockUsers = [ + { id: 'u-1', name: 'Alice', email: 'alice@example.com', role: 'learner', createdAt: new Date('2024-01-01') }, + { id: 'u-2', name: 'Bob', email: 'bob@example.com', role: 'admin', createdAt: new Date('2024-01-02') }, +]; + +function makeRequest(url = 'http://localhost/api/admin/users') { + return new NextRequest(url); +} + +function mockChain(result: unknown) { + return { + from: vi.fn().mockReturnValue({ + orderBy: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + offset: vi.fn().mockResolvedValue(result), + }), + }), + }), + }; +} + +describe('GET /api/admin/users', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSelect.mockReturnValue(mockChain(mockUsers)); + }); + + it('returns paginated users on happy path (page 1)', async () => { + const res = await GET(makeRequest('http://localhost/api/admin/users?page=1')); + const body = await res.json(); + expect(res.status).toBe(200); + expect(body.users).toHaveLength(mockUsers.length); + expect(body.users[0].id).toBe('u-1'); + expect(body.page).toBe(1); + }); + + it('defaults to page 1 when page param missing', async () => { + const res = await GET(makeRequest()); + const body = await res.json(); + expect(body.page).toBe(1); + }); + + it('returns correct page for page 2', async () => { + const res = await GET(makeRequest('http://localhost/api/admin/users?page=2')); + const body = await res.json(); + expect(body.page).toBe(2); + }); + + it('returns empty array when no users', async () => { + mockSelect.mockReturnValue(mockChain([])); + const res = await GET(makeRequest()); + const body = await res.json(); + expect(body.users).toEqual([]); + }); + + it('calls requireAdmin', async () => { + await GET(makeRequest()); + expect(requireAdmin).toHaveBeenCalledOnce(); + }); + + it('propagates requireAdmin rejection', async () => { + vi.mocked(requireAdmin).mockRejectedValueOnce(new Error('forbidden')); + await expect(GET(makeRequest())).rejects.toThrow('forbidden'); + }); + + it('propagates DB errors', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + orderBy: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ offset: vi.fn().mockRejectedValue(new Error('DB error')) }), + }), + }), + }); + await expect(GET(makeRequest())).rejects.toThrow('DB error'); + }); +}); diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..c06456c --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +import { requireAdmin } from '@/lib/auth-helpers'; +import { asc } from 'drizzle-orm'; + +export async function GET(req: NextRequest): Promise { + await requireAdmin(); + + const page = parseInt(req.nextUrl.searchParams.get('page') ?? '1', 10); + const limit = 50; + const offset = (page - 1) * limit; + + const rows = await db + .select({ id: users.id, name: users.name, email: users.email, role: users.role, createdAt: users.createdAt }) + .from(users) + .orderBy(asc(users.createdAt)) + .limit(limit) + .offset(offset); + + return NextResponse.json({ users: rows, page }); +} diff --git a/src/app/api/advance/__tests__/route.test.ts b/src/app/api/advance/__tests__/route.test.ts new file mode 100644 index 0000000..f500758 --- /dev/null +++ b/src/app/api/advance/__tests__/route.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db/queries', () => ({ + advanceJourneyPosition: vi.fn(), +})); + +import { POST } from '../route'; +import { advanceJourneyPosition } from '@/lib/db/queries'; + +const mockAdvance = vi.mocked(advanceJourneyPosition); + +const VALID_BODY = { + userId: '123e4567-e89b-12d3-a456-426614174000', + blueprintId: '223e4567-e89b-12d3-a456-426614174001', +}; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost:3000/api/advance', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('POST /api/advance', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returns 400 on malformed JSON', async () => { + const req = new NextRequest('http://localhost:3000/api/advance', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{not json', + }); + const res = await POST(req); + expect(res.status).toBe(400); + expect((await res.json()).error).toBe('Invalid JSON'); + }); + + it('returns 400 when userId missing', async () => { + const res = await POST(makeRequest({ blueprintId: VALID_BODY.blueprintId })); + expect(res.status).toBe(400); + }); + + it('returns 400 when blueprintId missing', async () => { + const res = await POST(makeRequest({ userId: VALID_BODY.userId })); + expect(res.status).toBe(400); + }); + + it('returns 400 when userId is not a UUID', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, userId: 'not-a-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when blueprintId is not a UUID', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, blueprintId: 'not-a-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 200 with position on success', async () => { + mockAdvance.mockResolvedValue(2); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.position).toBe(2); + }); + + it('calls advanceJourneyPosition with correct args', async () => { + mockAdvance.mockResolvedValue(1); + await POST(makeRequest(VALID_BODY)); + expect(mockAdvance).toHaveBeenCalledWith(VALID_BODY.userId, VALID_BODY.blueprintId); + }); +}); diff --git a/src/app/api/advance/route.ts b/src/app/api/advance/route.ts new file mode 100644 index 0000000..6bf3739 --- /dev/null +++ b/src/app/api/advance/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { advanceJourneyPosition } from '@/lib/db/queries'; +import { getOptionalSession } from '@/lib/auth-helpers'; + +const AdvanceBodySchema = z.object({ + userId: z.string().uuid().optional(), + blueprintId: z.string().uuid(), +}); + +/** + * POST /api/advance + * + * Advances the learner's journey position for a given blueprint. + * Called when all checkpoints in a lesson are mastered. + * Creates the journey instance on first call (anonymous-session-first). + */ +export async function POST(req: NextRequest) { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = AdvanceBodySchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json({ error: 'Missing userId or blueprintId' }, { status: 400 }); + } + + const session = await getOptionalSession(); + const userId = session?.user?.id ?? parsed.data.userId; + if (!userId) { + return NextResponse.json({ error: 'Missing userId or blueprintId' }, { status: 400 }); + } + const { blueprintId } = parsed.data; + + const position = await advanceJourneyPosition(userId, blueprintId); + return NextResponse.json({ position }); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..0a98352 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from '@/auth'; + +export const { GET, POST } = handlers; diff --git a/src/app/api/auth/forgot-password/route.ts b/src/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..b70eed0 --- /dev/null +++ b/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { randomBytes } from 'crypto'; +import { db } from '@/lib/db'; +import { users, passwordResetTokens } from '@/lib/db/schema'; +import { sendEmail } from '@/lib/email'; +import { passwordResetEmail } from '@/lib/email/templates'; + +const Schema = z.object({ email: z.string().email() }); + +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = Schema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: 'Valid email required' }, { status: 400 }); + + // Always return 200 — don't reveal whether email exists + const [user] = await db + .select({ id: users.id }) + .from(users) + .where(eq(users.email, parsed.data.email.toLowerCase())) + .limit(1); + + if (user) { + const token = randomBytes(32).toString('hex'); + const expires = new Date(Date.now() + 3_600_000); // 1 hour + + await db + .delete(passwordResetTokens) + .where(eq(passwordResetTokens.userId, user.id)); + + await db.insert(passwordResetTokens).values({ token, userId: user.id, expires }); + + const { subject, html, text } = passwordResetEmail(token); + void sendEmail({ to: parsed.data.email, subject, html, text }).catch(() => {}); + } + + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/auth/migrate-session/__tests__/route.test.ts b/src/app/api/auth/migrate-session/__tests__/route.test.ts new file mode 100644 index 0000000..61fc977 --- /dev/null +++ b/src/app/api/auth/migrate-session/__tests__/route.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +const { mockTransaction } = vi.hoisted(() => ({ mockTransaction: vi.fn() })); + +vi.mock('@/lib/db', () => ({ db: { transaction: mockTransaction } })); + +vi.mock('@/lib/cache/redis', () => ({ + getRedis: vi.fn().mockReturnValue({ + get: vi.fn().mockResolvedValue(null), + set: vi.fn().mockResolvedValue('OK'), + del: vi.fn().mockResolvedValue(1), + }), +})); + +vi.mock('@/lib/auth-helpers', () => ({ + getOptionalSession: vi.fn().mockResolvedValue({ + user: { id: '22222222-2222-2222-2222-222222222222' }, + }), + requireAuth: vi.fn(), + requireAdmin: vi.fn(), +})); + +import { POST } from '../route'; +import { getOptionalSession } from '@/lib/auth-helpers'; +import { getRedis } from '@/lib/cache/redis'; + +const ANON_ID = '11111111-1111-1111-1111-111111111111'; +const AUTH_ID = '22222222-2222-2222-2222-222222222222'; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost/api/auth/migrate-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +function setupTransaction() { + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { + const tx = { + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + }), + }; + return fn(tx); + }); +} + +describe('POST /api/auth/migrate-session', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getOptionalSession).mockResolvedValue({ user: { id: AUTH_ID } } as never); + setupTransaction(); + const redis = getRedis(); + vi.mocked(redis.get).mockResolvedValue(null); + }); + + it('returns 401 when no session', async () => { + vi.mocked(getOptionalSession).mockResolvedValueOnce(null); + const res = await POST(makeRequest({ anonUserId: ANON_ID })); + expect(res.status).toBe(401); + }); + + it('returns 401 when session has no user id', async () => { + vi.mocked(getOptionalSession).mockResolvedValueOnce({ user: {} } as never); + const res = await POST(makeRequest({ anonUserId: ANON_ID })); + expect(res.status).toBe(401); + }); + + it('returns 400 on bad JSON', async () => { + const req = new NextRequest('http://localhost/api/auth/migrate-session', { + method: 'POST', + body: '{not json', + headers: { 'Content-Type': 'application/json' }, + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it('returns 400 when anonUserId not UUID', async () => { + const res = await POST(makeRequest({ anonUserId: 'not-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 200 no-op when anonUserId equals authUserId', async () => { + const res = await POST(makeRequest({ anonUserId: AUTH_ID })); + expect(res.status).toBe(200); + expect(mockTransaction).not.toHaveBeenCalled(); + }); + + it('returns 200 {ok:true} and runs transaction on success', async () => { + const res = await POST(makeRequest({ anonUserId: ANON_ID })); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.ok).toBe(true); + expect(mockTransaction).toHaveBeenCalledOnce(); + }); + + it('renames Redis budget key when it exists', async () => { + const redis = getRedis(); + vi.mocked(redis.get).mockResolvedValueOnce('42'); + await POST(makeRequest({ anonUserId: ANON_ID })); + expect(redis.set).toHaveBeenCalledWith(`budget:${AUTH_ID}`, '42'); + expect(redis.del).toHaveBeenCalledWith(`budget:${ANON_ID}`); + }); + + it('skips Redis rename when anon key absent', async () => { + const redis = getRedis(); + vi.mocked(redis.get).mockResolvedValueOnce(null); + await POST(makeRequest({ anonUserId: ANON_ID })); + expect(redis.set).not.toHaveBeenCalled(); + }); + + it('returns 200 even when Redis throws (non-fatal)', async () => { + vi.mocked(getRedis).mockImplementationOnce(() => { throw new Error('Redis down'); }); + const res = await POST(makeRequest({ anonUserId: ANON_ID })); + expect(res.status).toBe(200); + }); +}); diff --git a/src/app/api/auth/migrate-session/route.ts b/src/app/api/auth/migrate-session/route.ts new file mode 100644 index 0000000..7420d5b --- /dev/null +++ b/src/app/api/auth/migrate-session/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { responses, mastery, journeyInstances } from '@/lib/db/schema'; +import { getOptionalSession } from '@/lib/auth-helpers'; +import { getRedis } from '@/lib/cache/redis'; + +const MigrateSchema = z.object({ + anonUserId: z.string().uuid(), +}); + +export async function POST(req: NextRequest): Promise { + const session = await getOptionalSession(); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = MigrateSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json({ error: 'anonUserId must be a UUID' }, { status: 400 }); + } + + const { anonUserId } = parsed.data; + const authUserId = session.user.id; + + if (anonUserId === authUserId) { + return NextResponse.json({ ok: true }); + } + + await db.transaction(async (tx) => { + await tx.update(responses).set({ userId: authUserId }).where(eq(responses.userId, anonUserId)); + await tx + .update(mastery) + .set({ userId: authUserId }) + .where(eq(mastery.userId, anonUserId)) + .catch(() => { + // Conflict if auth user already has mastery for same concept — skip + }); + await tx + .update(journeyInstances) + .set({ userId: authUserId }) + .where(eq(journeyInstances.userId, anonUserId)) + .catch(() => {}); + }); + + // Rename Redis budget key + try { + const r = getRedis(); + const anonKey = `budget:${anonUserId}`; + const authKey = `budget:${authUserId}`; + const val = await r.get(anonKey); + if (val !== null) { + await r.set(authKey, val); + await r.del(anonKey); + } + } catch { + // Redis optional; don't fail migration + } + + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/auth/register/__tests__/route.test.ts b/src/app/api/auth/register/__tests__/route.test.ts new file mode 100644 index 0000000..a9fb187 --- /dev/null +++ b/src/app/api/auth/register/__tests__/route.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +const { mockSelect, mockInsert } = vi.hoisted(() => ({ + mockSelect: vi.fn(), + mockInsert: vi.fn(), +})); + +vi.mock('@/lib/db', () => ({ db: { select: mockSelect, insert: mockInsert } })); +vi.mock('@/lib/site-config', () => ({ isSignupsEnabled: vi.fn().mockResolvedValue(true) })); + +vi.mock('bcryptjs', () => ({ + default: { hash: vi.fn().mockResolvedValue('hashed_pw'), compare: vi.fn() }, +})); + +import { POST } from '../route'; +import bcrypt from 'bcryptjs'; + +const VALID_BODY = { email: 'user@example.com', name: 'Test User', password: 'securepassword123' }; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +function setupDefault({ existingUser = false } = {}) { + mockSelect.mockImplementation((fields: Record | undefined) => { + if (fields && 'total' in fields) { + // count() query — first user, so total = 0 + return { from: vi.fn().mockResolvedValue([{ total: 0 }]) }; + } + // email duplicate check + const rows = existingUser ? [{ id: 'existing' }] : []; + return { from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue(rows) }) }) }; + }); + mockInsert.mockReturnValue({ + values: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([{ id: 'new-user-uuid' }]) }), + }); +} + +describe('POST /api/auth/register', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(bcrypt.hash).mockResolvedValue('hashed_pw' as never); + setupDefault(); + }); + + it('returns 400 on bad JSON', async () => { + const req = new NextRequest('http://localhost/api/auth/register', { + method: 'POST', + body: '{not json', + headers: { 'Content-Type': 'application/json' }, + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it('returns 400 when email missing', async () => { + const res = await POST(makeRequest({ name: 'Test', password: 'password123' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when email invalid', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, email: 'not-an-email' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when name missing', async () => { + const res = await POST(makeRequest({ email: 'a@b.com', password: 'password123' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when name empty', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, name: '' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when password too short', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, password: 'short' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when password over 128 chars', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, password: 'a'.repeat(129) })); + expect(res.status).toBe(400); + }); + + it('returns 409 when email already registered', async () => { + setupDefault({ existingUser: true }); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(409); + const body = await res.json(); + expect(body.error).toMatch(/already registered/i); + }); + + it('returns 201 with userId on success', async () => { + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(201); + const body = await res.json(); + expect(body.userId).toBe('new-user-uuid'); + }); + + it('hashes password with cost 12', async () => { + await POST(makeRequest(VALID_BODY)); + expect(bcrypt.hash).toHaveBeenCalledWith(VALID_BODY.password, 12); + }); + + it('stores hashed password not plaintext', async () => { + await POST(makeRequest(VALID_BODY)); + const valuesMock = vi.mocked(mockInsert).mock.results[0]?.value?.values; + expect(valuesMock).toHaveBeenCalledWith(expect.objectContaining({ passwordHash: 'hashed_pw' })); + }); + + it('normalizes email to lowercase', async () => { + await POST(makeRequest({ ...VALID_BODY, email: 'USER@EXAMPLE.COM' })); + const valuesMock = vi.mocked(mockInsert).mock.results[0]?.value?.values; + expect(valuesMock).toHaveBeenCalledWith(expect.objectContaining({ email: 'user@example.com' })); + }); +}); diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..3b2a715 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import bcrypt from 'bcryptjs'; +import { randomBytes } from 'crypto'; +import { eq, count } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { users, verificationTokens } from '@/lib/db/schema'; +import { sendEmail } from '@/lib/email'; +import { verificationEmail } from '@/lib/email/templates'; +import { isSignupsEnabled } from '@/lib/site-config'; + +const RegisterSchema = z.object({ + email: z.string().email(), + name: z.string().min(1).max(255), + password: z.string().min(8).max(128), +}); + +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const signupsEnabled = await isSignupsEnabled(); + if (!signupsEnabled) { + return NextResponse.json({ error: 'Signups are currently disabled' }, { status: 403 }); + } + + const parsed = RegisterSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues.map((i) => i.message).join('; ') }, + { status: 400 }, + ); + } + + const { email, name, password } = parsed.data; + const normalizedEmail = email.toLowerCase(); + + const [existing] = await db.select({ id: users.id }).from(users).where(eq(users.email, normalizedEmail)).limit(1); + if (existing) { + return NextResponse.json({ error: 'Email already registered' }, { status: 409 }); + } + + const [{ total }] = await db.select({ total: count() }).from(users); + const role = total === 0 ? 'admin' : 'learner'; + + const passwordHash = await bcrypt.hash(password, 12); + const [user] = await db + .insert(users) + .values({ email: normalizedEmail, name, passwordHash, role }) + .returning({ id: users.id }); + + const token = randomBytes(32).toString('hex'); + const expires = new Date(Date.now() + 86_400_000); // 24 hours + await db.insert(verificationTokens).values({ identifier: normalizedEmail, token, expires }); + const { subject, html, text } = verificationEmail(token); + void sendEmail({ to: normalizedEmail, subject, html, text }).catch(() => {}); + + return NextResponse.json({ userId: user.id }, { status: 201 }); +} diff --git a/src/app/api/auth/reset-password/route.ts b/src/app/api/auth/reset-password/route.ts new file mode 100644 index 0000000..f9fa2d0 --- /dev/null +++ b/src/app/api/auth/reset-password/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { and, eq, gt } from 'drizzle-orm'; +import bcrypt from 'bcryptjs'; +import { db } from '@/lib/db'; +import { users, passwordResetTokens } from '@/lib/db/schema'; + +const Schema = z.object({ + token: z.string().min(1), + password: z.string().min(8).max(128), +}); + +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = Schema.safeParse(body); + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, { status: 400 }); + } + + const { token, password } = parsed.data; + const now = new Date(); + + const [row] = await db + .select({ userId: passwordResetTokens.userId }) + .from(passwordResetTokens) + .where(and(eq(passwordResetTokens.token, token), gt(passwordResetTokens.expires, now))) + .limit(1); + + if (!row) { + return NextResponse.json({ error: 'Token invalid or expired' }, { status: 400 }); + } + + const passwordHash = await bcrypt.hash(password, 12); + await db.update(users).set({ passwordHash }).where(eq(users.id, row.userId)); + await db.delete(passwordResetTokens).where(eq(passwordResetTokens.token, token)); + + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/explain/__tests__/route.test.ts b/src/app/api/explain/__tests__/route.test.ts new file mode 100644 index 0000000..a9c45db --- /dev/null +++ b/src/app/api/explain/__tests__/route.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/generation/generate-explanation', () => ({ + generateExplanation: vi.fn(), +})); + +vi.mock('@/lib/budget', () => ({ + checkBudget: vi.fn().mockResolvedValue(undefined), + BudgetExceededError: class BudgetExceededError extends Error {}, +})); + +import { POST } from '../route'; +import { generateExplanation } from '@/lib/generation/generate-explanation'; +import { checkBudget, BudgetExceededError } from '@/lib/budget'; + +const mockGenerate = vi.mocked(generateExplanation); +const mockCheckBudget = vi.mocked(checkBudget); + +const VALID_BODY = { + checkpointId: '123e4567-e89b-12d3-a456-426614174000', + angle: 'explain with an analogy', + userId: 'user-abc', +}; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost:3000/api/explain', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('POST /api/explain', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockCheckBudget.mockResolvedValue(undefined); + }); + + it('returns 400 on malformed JSON', async () => { + const req = new NextRequest('http://localhost:3000/api/explain', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{not json', + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it('returns 400 when checkpointId missing', async () => { + const res = await POST(makeRequest({ angle: 'analogy', userId: 'u1' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when checkpointId is not a UUID', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, checkpointId: 'not-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when angle is too short', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, angle: 'x' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when userId is missing', async () => { + const res = await POST(makeRequest({ checkpointId: VALID_BODY.checkpointId, angle: VALID_BODY.angle })); + expect(res.status).toBe(400); + }); + + it('returns 200 with explanation on success', async () => { + mockGenerate.mockResolvedValue({ explanation: 'A closure is like a backpack...' }); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.explanation).toBe('A closure is like a backpack...'); + }); + + it('returns 429 when budget exceeded', async () => { + mockCheckBudget.mockRejectedValue(new BudgetExceededError('over budget')); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(429); + }); + + it('returns 500 on unexpected error', async () => { + mockGenerate.mockRejectedValue(new Error('LLM timeout')); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(500); + }); +}); diff --git a/src/app/api/explain/route.ts b/src/app/api/explain/route.ts new file mode 100644 index 0000000..8f6c616 --- /dev/null +++ b/src/app/api/explain/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { generateExplanation } from '@/lib/generation/generate-explanation'; +import { BudgetExceededError, checkBudget } from '@/lib/budget'; +import { getOptionalSession } from '@/lib/auth-helpers'; + +const ExplainRequestSchema = z.object({ + checkpointId: z.string().uuid('checkpointId must be a UUID'), + angle: z.string().min(3, 'angle too short').max(200, 'angle too long'), + userId: z.string().min(1).optional(), +}); + +/** + * POST /api/explain — generate a re-explanation from a different angle (§4 Adapt). + * + * Called when the grader returns feedback_directive: 're-explain:'. + * Uses the generator model and is budget-gated. + */ +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = ExplainRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues.map((i) => i.message).join('; ') }, + { status: 400 }, + ); + } + + const session = await getOptionalSession(); + const userId = session?.user?.id ?? parsed.data.userId; + if (!userId) { + return NextResponse.json({ error: 'userId required' }, { status: 400 }); + } + const { checkpointId, angle } = parsed.data; + + try { + await checkBudget(userId); + const result = await generateExplanation({ checkpointId, angle, userId }); + return NextResponse.json({ explanation: result.explanation }); + } catch (err) { + if (err instanceof BudgetExceededError) { + return NextResponse.json( + { error: 'Session limit reached. Start a new session to continue.' }, + { status: 429 }, + ); + } + console.error('[POST /api/explain]', err); + return NextResponse.json({ error: 'Failed to generate explanation' }, { status: 500 }); + } +} diff --git a/src/app/api/grade/__tests__/route.test.ts b/src/app/api/grade/__tests__/route.test.ts new file mode 100644 index 0000000..8f9fbb7 --- /dev/null +++ b/src/app/api/grade/__tests__/route.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/verification/grader', () => ({ + gradeResponse: vi.fn(), +})); + +import { POST } from '../route'; +import { gradeResponse } from '@/lib/verification/grader'; + +const mockGradeResponse = vi.mocked(gradeResponse); + +const VALID_BODY = { + checkpointId: '123e4567-e89b-12d3-a456-426614174000', + userId: 'user-abc', + responseText: 'A closure is a function that retains access to its outer scope.', +}; + +const MOCK_GRADE_RESULT = { + responseId: 'resp-1', + gradeId: 'grade-1', + grade: { + verdict: 'mastered' as const, + rubric_hits: ['r1'], + misconception_tag: 'none', + evidence_span: 'retains access to its outer scope', + feedback_directive: 'advance', + confidence: 0.9, + }, +}; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost:3000/api/grade', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('POST /api/grade', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 400 when checkpointId is missing', async () => { + const res = await POST(makeRequest({ userId: 'u', responseText: 'r' })); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toBeTruthy(); + }); + + it('returns 400 when checkpointId is not a UUID', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, checkpointId: 'not-a-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when responseText is empty', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, responseText: '' })); + expect(res.status).toBe(400); + }); + + it('returns 400 on malformed JSON', async () => { + const req = new NextRequest('http://localhost:3000/api/grade', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{not json', + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it('returns 200 with grade on success', async () => { + mockGradeResponse.mockResolvedValue(MOCK_GRADE_RESULT); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.grade.verdict).toBe('mastered'); + expect(body.responseId).toBe('resp-1'); + expect(body.gradeId).toBe('grade-1'); + }); + + it('returns 404 when checkpoint not found', async () => { + mockGradeResponse.mockRejectedValue( + new Error('Checkpoint not found: 123e4567-e89b-12d3-a456-426614174000'), + ); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(404); + }); + + it('returns 500 on unexpected grader error', async () => { + mockGradeResponse.mockRejectedValue(new Error('DB connection lost')); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(500); + const body = await res.json(); + expect(body.error).toBe('Grading failed'); + }); + + it('passes checkpointId, userId, responseText to gradeResponse', async () => { + mockGradeResponse.mockResolvedValue(MOCK_GRADE_RESULT); + await POST(makeRequest(VALID_BODY)); + expect(mockGradeResponse).toHaveBeenCalledWith( + expect.objectContaining({ + checkpointId: VALID_BODY.checkpointId, + userId: VALID_BODY.userId, + responseText: VALID_BODY.responseText, + }), + ); + }); + + it('passes selfConfidence when provided', async () => { + mockGradeResponse.mockResolvedValue(MOCK_GRADE_RESULT); + await POST(makeRequest({ ...VALID_BODY, selfConfidence: 3 })); + expect(mockGradeResponse).toHaveBeenCalledWith( + expect.objectContaining({ selfConfidence: 3 }), + ); + }); + + it('returns 400 when selfConfidence is out of range', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, selfConfidence: 5 })); + expect(res.status).toBe(400); + }); +}); diff --git a/src/app/api/grade/route.ts b/src/app/api/grade/route.ts new file mode 100644 index 0000000..8c48e03 --- /dev/null +++ b/src/app/api/grade/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { gradeResponse } from '@/lib/verification/grader'; +import { BudgetExceededError } from '@/lib/budget'; +import { autoFlagCheckpointIfNeeded } from '@/lib/db/queries'; +import { getOptionalSession } from '@/lib/auth-helpers'; + +const GradeRequestSchema = z.object({ + checkpointId: z.string().uuid('checkpointId must be a UUID'), + userId: z.string().min(1).optional(), + responseText: z.string().min(1, 'responseText required').max(4000), + selfConfidence: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(), +}); + +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = GradeRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues.map((i) => i.message).join('; ') }, + { status: 400 }, + ); + } + + const session = await getOptionalSession(); + const userId = session?.user?.id ?? parsed.data.userId; + if (!userId) { + return NextResponse.json({ error: 'userId required' }, { status: 400 }); + } + const { checkpointId, responseText, selfConfidence } = parsed.data; + + try { + const result = await gradeResponse({ checkpointId, userId, responseText, selfConfidence }); + // Auto-flag checkpoint if collective failure rate is high (§10.4, non-blocking) + void autoFlagCheckpointIfNeeded(checkpointId).catch(() => {}); + return NextResponse.json({ + responseId: result.responseId, + gradeId: result.gradeId, + grade: result.grade, + }); + } catch (err) { + if (err instanceof BudgetExceededError) { + return NextResponse.json( + { error: 'Session limit reached. Start a new session to continue.' }, + { status: 429 }, + ); + } + const message = err instanceof Error ? err.message : 'Internal error'; + if (message.startsWith('Checkpoint not found')) { + return NextResponse.json({ error: message }, { status: 404 }); + } + console.error('[POST /api/grade]', err); + return NextResponse.json({ error: 'Grading failed' }, { status: 500 }); + } +} diff --git a/src/app/api/health/__tests__/route.test.ts b/src/app/api/health/__tests__/route.test.ts new file mode 100644 index 0000000..0aba103 --- /dev/null +++ b/src/app/api/health/__tests__/route.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { mockQuery, mockPing, mockQuit } = vi.hoisted(() => ({ + mockQuery: vi.fn(), + mockPing: vi.fn(), + mockQuit: vi.fn(), +})); + +vi.mock('@/lib/db', () => ({ + pool: { query: mockQuery }, +})); + +vi.mock('ioredis', () => ({ + default: vi.fn().mockImplementation(() => ({ ping: mockPing, quit: mockQuit })), +})); + +import { GET } from '../route'; + +describe('GET /api/health', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockQuery.mockResolvedValue({}); + mockPing.mockResolvedValue('PONG'); + mockQuit.mockResolvedValue('OK'); + }); + + it('returns 200 ok when postgres and redis healthy', async () => { + const res = await GET(); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.status).toBe('ok'); + expect(body.checks.postgres).toBe('ok'); + expect(body.checks.redis).toBe('ok'); + }); + + it('returns 503 degraded when postgres fails', async () => { + mockQuery.mockRejectedValue(new Error('connection refused')); + const res = await GET(); + expect(res.status).toBe(503); + const body = await res.json(); + expect(body.status).toBe('degraded'); + expect(body.checks.postgres).toBe('error'); + expect(body.checks.redis).toBe('ok'); + }); + + it('returns 503 degraded when redis fails', async () => { + mockPing.mockRejectedValue(new Error('redis down')); + const res = await GET(); + expect(res.status).toBe(503); + const body = await res.json(); + expect(body.status).toBe('degraded'); + expect(body.checks.postgres).toBe('ok'); + expect(body.checks.redis).toBe('error'); + }); + + it('returns 503 when both fail', async () => { + mockQuery.mockRejectedValue(new Error('pg down')); + mockPing.mockRejectedValue(new Error('redis down')); + const res = await GET(); + expect(res.status).toBe(503); + const body = await res.json(); + expect(body.status).toBe('degraded'); + expect(body.checks.postgres).toBe('error'); + expect(body.checks.redis).toBe('error'); + }); +}); diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..49ff799 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server'; +import { pool } from '@/lib/db'; +import Redis from 'ioredis'; + +export async function GET() { + const checks: Record = {}; + + // Postgres + try { + await pool.query('SELECT 1'); + checks.postgres = 'ok'; + } catch { + checks.postgres = 'error'; + } + + // Redis + try { + const redis = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379'); + await redis.ping(); + await redis.quit(); + checks.redis = 'ok'; + } catch { + checks.redis = 'error'; + } + + const allOk = Object.values(checks).every((v) => v === 'ok'); + + return NextResponse.json( + { status: allOk ? 'ok' : 'degraded', checks }, + { status: allOk ? 200 : 503 }, + ); +} diff --git a/src/app/api/intent/__tests__/route.test.ts b/src/app/api/intent/__tests__/route.test.ts new file mode 100644 index 0000000..d624871 --- /dev/null +++ b/src/app/api/intent/__tests__/route.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/intent/normalize', () => ({ + normalizeIntent: vi.fn(), +})); + +vi.mock('@/lib/db/queries', () => ({ + getPersonalizationContext: vi.fn(), +})); + +vi.mock('@/lib/generation/generate-blueprint', () => ({ + generateBlueprint: vi.fn().mockResolvedValue('bp-new-uuid'), +})); + +import { POST } from '../route'; +import { normalizeIntent } from '@/lib/intent/normalize'; +import { getPersonalizationContext } from '@/lib/db/queries'; + +const mockNormalize = vi.mocked(normalizeIntent); +const mockPersonalize = vi.mocked(getPersonalizationContext); + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost:3000/api/intent', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('POST /api/intent', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returns 400 when input is missing', async () => { + const res = await POST(makeRequest({ userId: 'u1' })); + expect(res.status).toBe(400); + }); + + it('returns intentKey and isNew without userId', async () => { + mockNormalize.mockResolvedValue({ intentKey: 'javascript-closures', blueprintId: null, isNew: true }); + const res = await POST(makeRequest({ input: 'JavaScript closures' })); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.intentKey).toBe('javascript-closures'); + expect(body.isNew).toBe(true); + expect(body.personalizationHint).toBeUndefined(); + expect(mockPersonalize).not.toHaveBeenCalled(); + }); + + it('includes personalizationHint when userId provided and history exists', async () => { + mockNormalize.mockResolvedValue({ intentKey: 'closures', blueprintId: 'bp-1', isNew: false }); + mockPersonalize.mockResolvedValue({ + weakConcepts: ['Promises'], + recentMisconceptionTags: ['closure-is-object'], + }); + const res = await POST(makeRequest({ input: 'closures', userId: 'u1' })); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.personalizationHint).toContain('Promises'); + expect(body.personalizationHint).toContain('closure-is-object'); + }); + + it('omits personalizationHint when no history', async () => { + mockNormalize.mockResolvedValue({ intentKey: 'closures', blueprintId: null, isNew: true }); + mockPersonalize.mockResolvedValue({ weakConcepts: [], recentMisconceptionTags: [] }); + const res = await POST(makeRequest({ input: 'closures', userId: 'u1' })); + const body = await res.json(); + expect(body.personalizationHint).toBeUndefined(); + }); + + it('returns blueprintId when intent matches existing', async () => { + mockNormalize.mockResolvedValue({ intentKey: 'closures', blueprintId: 'bp-existing', isNew: false }); + mockPersonalize.mockResolvedValue({ weakConcepts: [], recentMisconceptionTags: [] }); + const res = await POST(makeRequest({ input: 'closures', userId: 'u1' })); + const body = await res.json(); + expect(body.blueprintId).toBe('bp-existing'); + expect(body.isNew).toBe(false); + }); +}); diff --git a/src/app/api/intent/route.ts b/src/app/api/intent/route.ts new file mode 100644 index 0000000..8c961b4 --- /dev/null +++ b/src/app/api/intent/route.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { normalizeIntent } from '@/lib/intent/normalize'; +import { generateBlueprint } from '@/lib/generation/generate-blueprint'; +import { getPersonalizationContext } from '@/lib/db/queries'; + +const IntentRequestSchema = z.object({ + input: z.string().min(1, 'input required').max(500), + userId: z.string().optional(), +}); + +export interface IntentResponse { + intentKey: string; + blueprintId: string | null; + isNew: boolean; + /** Only present when userId provided and learner has prior history. */ + personalizationHint?: string; +} + +function buildPersonalizationHint(ctx: { weakConcepts: string[]; recentMisconceptionTags: string[] }): string | undefined { + const parts: string[] = []; + if (ctx.weakConcepts.length > 0) { + parts.push(`Learner is weak on: ${ctx.weakConcepts.join(', ')}.`); + } + if (ctx.recentMisconceptionTags.length > 0) { + parts.push(`Recent misconception tags: ${ctx.recentMisconceptionTags.join(', ')}.`); + } + return parts.length > 0 ? parts.join(' ') : undefined; +} + +/** + * POST /api/intent + * Normalizes a learner's free-text intent into a canonical intent key, + * and returns personalization context if userId is provided. + */ +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = IntentRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues.map((i) => i.message).join('; ') }, + { status: 400 }, + ); + } + + const { input, userId } = parsed.data; + + const { intentKey, blueprintId: existingBlueprintId, isNew } = await normalizeIntent(input); + + let blueprintId = existingBlueprintId; + if (isNew) { + blueprintId = await generateBlueprint({ intentText: input, intentKey }); + } + + let personalizationHint: string | undefined; + if (userId) { + const ctx = await getPersonalizationContext(userId); + personalizationHint = buildPersonalizationHint(ctx); + } + + const response: IntentResponse = { intentKey, blueprintId, isNew }; + if (personalizationHint) response.personalizationHint = personalizationHint; + + return NextResponse.json(response); +} diff --git a/src/app/api/lessons/__tests__/route.test.ts b/src/app/api/lessons/__tests__/route.test.ts new file mode 100644 index 0000000..d304ca5 --- /dev/null +++ b/src/app/api/lessons/__tests__/route.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db/queries', () => ({ + getLessonResponse: vi.fn(), +})); + +vi.mock('@/lib/cache/lesson', () => ({ + getCachedLesson: vi.fn().mockResolvedValue(null), + setCachedLesson: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock('@/lib/jobs/queue', () => ({ + enqueueGenerateLesson: vi.fn().mockResolvedValue('job-id'), +})); + +vi.mock('@/lib/ratelimit', () => ({ + getColdStartRatelimit: vi.fn().mockReturnValue(null), +})); + +vi.mock('@/lib/db', () => ({ + db: { + select: vi.fn(), + insert: vi.fn(), + }, +})); + +// Verify generateLesson is never imported by the route module +vi.mock('@/lib/generation/generate-lesson', () => ({ + generateLesson: vi.fn(() => { + throw new Error('generateLesson must never be called from a route handler'); + }), +})); + +import { GET } from '../route'; +import { getLessonResponse } from '@/lib/db/queries'; +import { getCachedLesson, setCachedLesson } from '@/lib/cache/lesson'; +import { enqueueGenerateLesson } from '@/lib/jobs/queue'; +import { generateLesson } from '@/lib/generation/generate-lesson'; +import { db } from '@/lib/db'; + +const mockGetLesson = vi.mocked(getLessonResponse); +const mockGetCached = vi.mocked(getCachedLesson); +const mockSetCached = vi.mocked(setCachedLesson); +const mockEnqueue = vi.mocked(enqueueGenerateLesson); +const mockGenerateLesson = vi.mocked(generateLesson); + +function makeRequest(key?: string) { + const url = key + ? `http://localhost:3000/api/lessons?key=${key}` + : 'http://localhost:3000/api/lessons'; + return new NextRequest(url); +} + +const READY_RESPONSE = { + state: 'ready' as const, + blueprintId: 'bp-1', + blueprintTitle: 'JavaScript Closures', + lesson: { + id: 'lesson-1', + segments: [ + { + id: 'seg-1', + ord: 0, + body: 'A closure is a function that...', + checkpoint: { id: 'cp-1', prompt: 'Explain what a closure is.', kind: 'explain' as const }, + }, + ], + }, +}; + +const OUTLINE_RESPONSE = { + state: 'outline' as const, + blueprintId: 'bp-1', + blueprintTitle: 'JavaScript Closures', + outline: { + concepts: [{ name: 'What is a closure?', ord: 0 }], + estimatedMinutes: 10, + }, +}; + +function setupDbForOutline(existingJob = false) { + const selectChain = existingJob + ? Promise.resolve([{ id: 'job-1', status: 'pending' }]) + : Promise.resolve([]); + + vi.mocked(db.select).mockReturnValue({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: vi.fn(() => selectChain), + })), + })), + } as unknown as ReturnType); + + vi.mocked(db.insert).mockReturnValue({ + values: vi.fn(() => ({ + returning: vi.fn(() => Promise.resolve([{ id: 'new-job-id' }])), + })), + } as unknown as ReturnType); +} + +describe('GET /api/lessons', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetCached.mockResolvedValue(null); + }); + + it('returns 400 when key is missing', async () => { + const res = await GET(makeRequest()); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toContain('key'); + }); + + it('returns 404 when blueprint not found', async () => { + mockGetLesson.mockResolvedValue(null); + const res = await GET(makeRequest('unknown-topic')); + expect(res.status).toBe(404); + }); + + it('serves from Redis cache when available', async () => { + mockGetCached.mockResolvedValue(READY_RESPONSE); + const res = await GET(makeRequest('javascript-closures')); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.state).toBe('ready'); + expect(mockGetLesson).not.toHaveBeenCalled(); + }); + + it('returns 200 with ready state when lesson exists', async () => { + mockGetLesson.mockResolvedValue(READY_RESPONSE); + const res = await GET(makeRequest('javascript-closures')); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.state).toBe('ready'); + expect(body.lesson.segments).toHaveLength(1); + expect(mockSetCached).toHaveBeenCalledWith('javascript-closures', READY_RESPONSE); + }); + + it('returns 200 outline and enqueues job on cold start', async () => { + mockGetLesson.mockResolvedValue(OUTLINE_RESPONSE); + setupDbForOutline(false); + const res = await GET(makeRequest('javascript-closures')); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.state).toBe('outline'); + expect(mockEnqueue).toHaveBeenCalledOnce(); + }); + + it('does not enqueue when job already exists', async () => { + mockGetLesson.mockResolvedValue(OUTLINE_RESPONSE); + setupDbForOutline(true); + const res = await GET(makeRequest('javascript-closures')); + expect(res.status).toBe(200); + expect(mockEnqueue).not.toHaveBeenCalled(); + }); + + it('does NOT call generateLesson — no synchronous generation in request path', async () => { + mockGetLesson.mockResolvedValue(READY_RESPONSE); + await GET(makeRequest('javascript-closures')); + expect(mockGenerateLesson).not.toHaveBeenCalled(); + }); + + it('response never includes reference_answer', async () => { + mockGetLesson.mockResolvedValue(READY_RESPONSE); + const res = await GET(makeRequest('javascript-closures')); + const body = JSON.stringify(await res.json()); + expect(body).not.toContain('reference_answer'); + expect(body).not.toContain('rubric'); + }); +}); diff --git a/src/app/api/lessons/route.ts b/src/app/api/lessons/route.ts new file mode 100644 index 0000000..c3c7913 --- /dev/null +++ b/src/app/api/lessons/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getLessonResponse } from '@/lib/db/queries'; +import { getCachedLesson, setCachedLesson } from '@/lib/cache/lesson'; +import { enqueueGenerateLesson } from '@/lib/jobs/queue'; +import { getColdStartRatelimit } from '@/lib/ratelimit'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { jobs } from '@/lib/db/schema'; + +/** + * GET /api/lessons?key= + * + * Serve path: Redis → DB → outline (enqueue generate job on cold-start). + * Never generates content synchronously (invariant #4). + */ +export async function GET(req: NextRequest) { + const key = req.nextUrl.searchParams.get('key'); + + if (!key) { + return NextResponse.json({ error: 'Missing ?key parameter' }, { status: 400 }); + } + + // 1. Redis cache + const cached = await getCachedLesson(key); + if (cached?.state === 'ready') { + return NextResponse.json(cached); + } + + // 2. DB — pass uid for difficulty variant selection (§13) + const uid = req.nextUrl.searchParams.get('uid') ?? undefined; + const result = await getLessonResponse(key, uid); + + if (!result) { + return NextResponse.json({ error: 'Blueprint not found' }, { status: 404 }); + } + + // 3. Ready — warm cache and return + if (result.state === 'ready') { + setCachedLesson(key, result).catch(() => {}); + return NextResponse.json(result); + } + + // 4. Outline (cold-start) — rate-limit before enqueuing + const limiter = getColdStartRatelimit(); + if (limiter) { + const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'anonymous'; + const { success } = await limiter.limit(ip); + if (!success) { + return NextResponse.json({ error: 'Too many requests' }, { status: 429 }); + } + } + + const idempotencyKey = `generate-lesson:${result.blueprintId}:v1`; + + // Check if job already exists (pending or running) + const [existingJob] = await db + .select({ id: jobs.id, status: jobs.status }) + .from(jobs) + .where(eq(jobs.idempotencyKey, idempotencyKey)) + .limit(1); + + if (!existingJob) { + // Insert job ledger row first (durable state — invariant #8) + const [jobRow] = await db + .insert(jobs) + .values({ + type: 'generate-lesson', + idempotencyKey, + status: 'pending', + payloadJson: { blueprintId: result.blueprintId, intentKey: key }, + }) + .returning({ id: jobs.id }); + + // Enqueue BullMQ job (uses jobId = idempotencyKey to dedup) + await enqueueGenerateLesson({ + intentKey: key, + blueprintId: result.blueprintId, + idempotencyKey: jobRow.id, + }); + } + + return NextResponse.json(result); +} diff --git a/src/app/api/mastery/__tests__/route.test.ts b/src/app/api/mastery/__tests__/route.test.ts new file mode 100644 index 0000000..cc68eaf --- /dev/null +++ b/src/app/api/mastery/__tests__/route.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db/queries', () => ({ + getMasteryMap: vi.fn(), +})); + +import { GET } from '../route'; +import { getMasteryMap } from '@/lib/db/queries'; + +const mockGet = vi.mocked(getMasteryMap); + +const NOW = new Date('2026-06-21T10:00:00Z'); +const LATER = new Date('2026-06-28T10:00:00Z'); + +const ENTRIES = [ + { + conceptId: 'c1', + conceptName: 'Closures', + blueprintTitle: 'JavaScript', + score: 0.8, + lastSeen: NOW, + nextReview: LATER, + }, + { + conceptId: 'c2', + conceptName: 'Promises', + blueprintTitle: 'JavaScript', + score: 0.45, + lastSeen: NOW, + nextReview: NOW, + }, +]; + +function makeRequest(userId?: string) { + const url = userId + ? `http://localhost:3000/api/mastery?userId=${userId}` + : 'http://localhost:3000/api/mastery'; + return new NextRequest(url); +} + +describe('GET /api/mastery', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returns 400 when userId is missing', async () => { + const res = await GET(makeRequest()); + expect(res.status).toBe(400); + }); + + it('returns 200 with empty groups when no mastery', async () => { + mockGet.mockResolvedValue([]); + const res = await GET(makeRequest('u1')); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.groups).toHaveLength(0); + }); + + it('groups entries by blueprintTitle', async () => { + mockGet.mockResolvedValue(ENTRIES); + const res = await GET(makeRequest('u1')); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.groups).toHaveLength(1); + expect(body.groups[0].blueprintTitle).toBe('JavaScript'); + expect(body.groups[0].concepts).toHaveLength(2); + }); + + it('includes score in each concept entry', async () => { + mockGet.mockResolvedValue(ENTRIES); + const res = await GET(makeRequest('u1')); + const body = await res.json(); + const concepts = body.groups[0].concepts; + expect(concepts[0].score).toBe(0.8); + expect(concepts[1].score).toBe(0.45); + }); +}); diff --git a/src/app/api/mastery/route.ts b/src/app/api/mastery/route.ts new file mode 100644 index 0000000..ed0c993 --- /dev/null +++ b/src/app/api/mastery/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getMasteryMap } from '@/lib/db/queries'; +import { getOptionalSession } from '@/lib/auth-helpers'; + +/** + * GET /api/mastery?userId= + * Returns all mastery records for a user, grouped by blueprint. + */ +export async function GET(req: NextRequest): Promise { + const session = await getOptionalSession(); + const userId = session?.user?.id ?? req.nextUrl.searchParams.get('userId'); + + if (!userId) { + return NextResponse.json({ error: 'Missing ?userId parameter' }, { status: 400 }); + } + + const entries = await getMasteryMap(userId); + + // Group by blueprintTitle for the map view + const grouped: Record = {}; + for (const entry of entries) { + if (!grouped[entry.blueprintTitle]) { + grouped[entry.blueprintTitle] = { blueprintTitle: entry.blueprintTitle, concepts: [] }; + } + grouped[entry.blueprintTitle].concepts.push(entry); + } + + return NextResponse.json({ groups: Object.values(grouped) }); +} diff --git a/src/app/api/report/__tests__/route.test.ts b/src/app/api/report/__tests__/route.test.ts new file mode 100644 index 0000000..c9bfea6 --- /dev/null +++ b/src/app/api/report/__tests__/route.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db/queries', () => ({ + insertContentReport: vi.fn(), +})); + +import { POST } from '../route'; +import { insertContentReport } from '@/lib/db/queries'; + +const mockInsert = vi.mocked(insertContentReport); + +const VALID_BODY = { + checkpointId: '123e4567-e89b-12d3-a456-426614174000', + reason: 'grade_wrong' as const, +}; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost:3000/api/report', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('POST /api/report', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returns 400 on malformed JSON', async () => { + const req = new NextRequest('http://localhost:3000/api/report', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{not json', + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it('returns 400 when checkpointId missing', async () => { + const res = await POST(makeRequest({ reason: 'grade_wrong' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when checkpointId is not a UUID', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, checkpointId: 'not-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when reason is invalid', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, reason: 'bad_reason' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when reason missing', async () => { + const res = await POST(makeRequest({ checkpointId: VALID_BODY.checkpointId })); + expect(res.status).toBe(400); + }); + + it('returns 200 on success', async () => { + mockInsert.mockResolvedValue('report-id'); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.ok).toBe(true); + }); + + it('returns 200 even when DB insert throws (non-fatal)', async () => { + mockInsert.mockRejectedValue(new Error('DB error')); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.ok).toBe(true); + }); + + it('accepts optional gradeId and notes', async () => { + mockInsert.mockResolvedValue('report-id'); + const res = await POST(makeRequest({ + ...VALID_BODY, + gradeId: '223e4567-e89b-12d3-a456-426614174001', + notes: 'The grading was wrong because...', + })); + expect(res.status).toBe(200); + expect(mockInsert).toHaveBeenCalledWith(expect.objectContaining({ + gradeId: '223e4567-e89b-12d3-a456-426614174001', + notes: 'The grading was wrong because...', + })); + }); +}); diff --git a/src/app/api/report/route.ts b/src/app/api/report/route.ts new file mode 100644 index 0000000..635b9df --- /dev/null +++ b/src/app/api/report/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { insertContentReport } from '@/lib/db/queries'; + +const ReportRequestSchema = z.object({ + checkpointId: z.string().uuid('checkpointId must be a UUID'), + gradeId: z.string().uuid().optional(), + reason: z.enum(['grade_wrong', 'content_error', 'other']), + notes: z.string().max(500).optional(), +}); + +/** + * POST /api/report — learner-submitted content report (§10.4 user-signal outer loop). + * + * Accepts contested grades and content error flags. Stored for triage and + * potential re-verification. Always returns 200 (report is best-effort; + * learner should not see submission errors). + */ +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = ReportRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues.map((i) => i.message).join('; ') }, + { status: 400 }, + ); + } + + try { + await insertContentReport(parsed.data); + return NextResponse.json({ ok: true }); + } catch (err) { + // Non-fatal: log but don't surface to learner + console.error('[POST /api/report]', err); + return NextResponse.json({ ok: true }); // always succeed from learner's perspective + } +} diff --git a/src/app/api/review/__tests__/route.test.ts b/src/app/api/review/__tests__/route.test.ts new file mode 100644 index 0000000..e083b0b --- /dev/null +++ b/src/app/api/review/__tests__/route.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db/queries', () => ({ + getReviewCheckpoints: vi.fn(), +})); + +import { GET } from '../route'; +import { getReviewCheckpoints } from '@/lib/db/queries'; + +const mockGet = vi.mocked(getReviewCheckpoints); + +const REVIEW_ITEMS = [ + { + conceptId: 'c1', + conceptName: 'Closures', + score: 0.65, + checkpointId: 'cp1', + checkpointPrompt: 'Explain what a closure is.', + checkpointKind: 'explain' as const, + }, +]; + +function makeRequest(userId?: string) { + const url = userId + ? `http://localhost:3000/api/review?userId=${userId}` + : 'http://localhost:3000/api/review'; + return new NextRequest(url); +} + +describe('GET /api/review', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returns 400 when userId is missing', async () => { + const res = await GET(makeRequest()); + expect(res.status).toBe(400); + }); + + it('returns 200 with empty list when nothing due', async () => { + mockGet.mockResolvedValue([]); + const res = await GET(makeRequest('user-1')); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.checkpoints).toHaveLength(0); + }); + + it('returns 200 with due checkpoints', async () => { + mockGet.mockResolvedValue(REVIEW_ITEMS); + const res = await GET(makeRequest('user-1')); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.checkpoints).toHaveLength(1); + expect(body.checkpoints[0].conceptName).toBe('Closures'); + expect(body.checkpoints[0].score).toBe(0.65); + }); + + it('passes userId to getReviewCheckpoints', async () => { + mockGet.mockResolvedValue([]); + await GET(makeRequest('user-abc')); + expect(mockGet).toHaveBeenCalledWith('user-abc'); + }); +}); diff --git a/src/app/api/review/route.ts b/src/app/api/review/route.ts new file mode 100644 index 0000000..a24b02b --- /dev/null +++ b/src/app/api/review/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getReviewCheckpoints } from '@/lib/db/queries'; +import { getOptionalSession } from '@/lib/auth-helpers'; + +/** + * GET /api/review?userId= + * + * Returns due checkpoints for spaced-rep review, ordered by overdue-ness. + */ +export async function GET(req: NextRequest): Promise { + const session = await getOptionalSession(); + const userId = session?.user?.id ?? req.nextUrl.searchParams.get('userId'); + + if (!userId) { + return NextResponse.json({ error: 'Missing ?userId parameter' }, { status: 400 }); + } + + const checkpoints = await getReviewCheckpoints(userId); + return NextResponse.json({ checkpoints }); +} diff --git a/src/app/api/segments/[id]/sources/route.ts b/src/app/api/segments/[id]/sources/route.ts new file mode 100644 index 0000000..071947e --- /dev/null +++ b/src/app/api/segments/[id]/sources/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { eq, inArray } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { segments, sourceChunks } from '@/lib/db/schema'; + +/** + * GET /api/segments/:id/sources + * Returns source chunks for a segment — used for lazy citation display. + * Returns docRef + first 300 chars of text per chunk (enough for context). + */ +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ id: string }> }, +): Promise { + const { id } = await params; + + const [seg] = await db + .select({ sourceChunkIds: segments.sourceChunkIds }) + .from(segments) + .where(eq(segments.id, id)) + .limit(1); + + if (!seg) { + return NextResponse.json({ error: 'Segment not found' }, { status: 404 }); + } + + if (seg.sourceChunkIds.length === 0) { + return NextResponse.json({ sources: [] }); + } + + const chunkRows = await db + .select({ id: sourceChunks.id, docRef: sourceChunks.docRef, text: sourceChunks.text }) + .from(sourceChunks) + .where(inArray(sourceChunks.id, seg.sourceChunkIds)); + + return NextResponse.json({ + sources: chunkRows.map((c) => ({ + id: c.id, + docRef: c.docRef, + excerpt: c.text.slice(0, 300) + (c.text.length > 300 ? '…' : ''), + })), + }); +} diff --git a/src/app/api/segments/__tests__/sources.test.ts b/src/app/api/segments/__tests__/sources.test.ts new file mode 100644 index 0000000..6a30e41 --- /dev/null +++ b/src/app/api/segments/__tests__/sources.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db', () => ({ + db: { + select: vi.fn(), + }, +})); + +import { GET } from '../[id]/sources/route'; +import { db } from '@/lib/db'; + +function makeThenableChain(result: T) { + const p = Promise.resolve(result); + return Object.assign(p, { + where: vi.fn(() => Object.assign(p, { limit: vi.fn(() => p) })), + limit: vi.fn(() => p), + }); +} + +function makeRequest(id: string) { + return new NextRequest(`http://localhost:3000/api/segments/${id}/sources`); +} + +describe('GET /api/segments/:id/sources', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returns 404 when segment not found', async () => { + vi.mocked(db.select).mockReturnValue({ + from: vi.fn(() => ({ where: vi.fn(() => ({ limit: vi.fn(() => Promise.resolve([])) })) })), + } as unknown as ReturnType); + + const res = await GET(makeRequest('seg-1'), { params: Promise.resolve({ id: 'seg-1' }) }); + expect(res.status).toBe(404); + }); + + it('returns empty sources when segment has no source chunk IDs', async () => { + let callCount = 0; + vi.mocked(db.select).mockImplementation(() => { + callCount++; + if (callCount === 1) { + return { + from: vi.fn(() => ({ + where: vi.fn(() => ({ limit: vi.fn(() => Promise.resolve([{ sourceChunkIds: [] }])) })), + })), + } as unknown as ReturnType; + } + return { from: vi.fn(() => ({ where: vi.fn(() => makeThenableChain([])) })) } as unknown as ReturnType; + }); + + const res = await GET(makeRequest('seg-1'), { params: Promise.resolve({ id: 'seg-1' }) }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.sources).toHaveLength(0); + }); + + it('returns source excerpts truncated to 300 chars', async () => { + const longText = 'a'.repeat(400); + let callCount = 0; + vi.mocked(db.select).mockImplementation(() => { + callCount++; + if (callCount === 1) { + return { + from: vi.fn(() => ({ + where: vi.fn(() => ({ limit: vi.fn(() => Promise.resolve([{ sourceChunkIds: ['chunk-1'] }])) })), + })), + } as unknown as ReturnType; + } + return { + from: vi.fn(() => ({ + where: vi.fn(() => makeThenableChain([ + { id: 'chunk-1', docRef: 'doc/file.md', text: longText }, + ])), + })), + } as unknown as ReturnType; + }); + + const res = await GET(makeRequest('seg-1'), { params: Promise.resolve({ id: 'seg-1' }) }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.sources[0].excerpt).toHaveLength(301); // 300 + '…' + expect(body.sources[0].excerpt.endsWith('…')).toBe(true); + expect(body.sources[0].docRef).toBe('doc/file.md'); + }); +}); diff --git a/src/app/api/socratic/__tests__/route.test.ts b/src/app/api/socratic/__tests__/route.test.ts new file mode 100644 index 0000000..634ed46 --- /dev/null +++ b/src/app/api/socratic/__tests__/route.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/verification/socratic', () => ({ + runSocraticProbe: vi.fn(), +})); + +import { POST } from '../route'; +import { runSocraticProbe } from '@/lib/verification/socratic'; + +const mockProbe = vi.mocked(runSocraticProbe); + +const VALID_BODY = { + gradeId: '123e4567-e89b-12d3-a456-426614174000', + userId: 'user-abc', + followUpText: 'I meant that closures hold a reference to the outer scope, not a copy.', +}; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost:3000/api/socratic', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('POST /api/socratic', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returns 400 when gradeId is missing', async () => { + const res = await POST(makeRequest({ userId: 'u', followUpText: 'text' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when gradeId is not a UUID', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, gradeId: 'not-a-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when followUpText is empty', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, followUpText: '' })); + expect(res.status).toBe(400); + }); + + it('returns 200 with reply and upgradedVerdict on success', async () => { + mockProbe.mockResolvedValue({ reply: 'Yes, that is correct.', upgradedVerdict: 'mastered' }); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.reply).toBe('Yes, that is correct.'); + expect(body.upgradedVerdict).toBe('mastered'); + }); + + it('returns 404 when grade not found', async () => { + mockProbe.mockRejectedValue( + new Error(`Grade not found: ${VALID_BODY.gradeId}`), + ); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(404); + }); + + it('returns 500 on unexpected error', async () => { + mockProbe.mockRejectedValue(new Error('DB down')); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(500); + }); +}); diff --git a/src/app/api/socratic/route.ts b/src/app/api/socratic/route.ts new file mode 100644 index 0000000..73bd87d --- /dev/null +++ b/src/app/api/socratic/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { runSocraticProbe } from '@/lib/verification/socratic'; +import { BudgetExceededError } from '@/lib/budget'; +import { getOptionalSession } from '@/lib/auth-helpers'; + +const SocraticRequestSchema = z.object({ + gradeId: z.string().uuid('gradeId must be a UUID'), + userId: z.string().min(1).optional(), + followUpText: z.string().min(1, 'followUpText required').max(2000), +}); + +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = SocraticRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues.map((i) => i.message).join('; ') }, + { status: 400 }, + ); + } + + const session = await getOptionalSession(); + const userId = session?.user?.id ?? parsed.data.userId; + if (!userId) { + return NextResponse.json({ error: 'userId required' }, { status: 400 }); + } + const { gradeId, followUpText } = parsed.data; + + try { + const result = await runSocraticProbe({ gradeId, userId, followUpText }); + return NextResponse.json(result); + } catch (err) { + if (err instanceof BudgetExceededError) { + return NextResponse.json( + { error: 'Session limit reached. Start a new session to continue.' }, + { status: 429 }, + ); + } + const message = err instanceof Error ? err.message : 'Internal error'; + if (message.startsWith('Grade not found')) { + return NextResponse.json({ error: message }, { status: 404 }); + } + console.error('[POST /api/socratic]', err); + return NextResponse.json({ error: 'Socratic probe failed' }, { status: 500 }); + } +} diff --git a/src/app/api/tangent/__tests__/route.test.ts b/src/app/api/tangent/__tests__/route.test.ts new file mode 100644 index 0000000..11ffa3e --- /dev/null +++ b/src/app/api/tangent/__tests__/route.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/generation/generate-tangent', () => ({ + generateTangent: vi.fn(), +})); + +vi.mock('@/lib/budget', () => ({ + checkBudget: vi.fn().mockResolvedValue(undefined), + recordUsage: vi.fn().mockResolvedValue(undefined), + BudgetExceededError: class BudgetExceededError extends Error {}, +})); + +import { POST } from '../route'; +import { generateTangent } from '@/lib/generation/generate-tangent'; +import { checkBudget, BudgetExceededError } from '@/lib/budget'; + +const mockGenerate = vi.mocked(generateTangent); +const mockCheckBudget = vi.mocked(checkBudget); + +const VALID_BODY = { + segmentId: '123e4567-e89b-12d3-a456-426614174000', + question: 'Why does this work?', + userId: 'user-abc', +}; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost:3000/api/tangent', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('POST /api/tangent', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockCheckBudget.mockResolvedValue(undefined); + }); + + it('returns 400 on malformed JSON', async () => { + const req = new NextRequest('http://localhost:3000/api/tangent', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{not json', + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it('returns 400 when segmentId missing', async () => { + const res = await POST(makeRequest({ question: 'Why?', userId: 'u1' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when segmentId is not a UUID', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, segmentId: 'not-uuid' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when question is too short', async () => { + const res = await POST(makeRequest({ ...VALID_BODY, question: 'x' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when userId missing', async () => { + const res = await POST(makeRequest({ segmentId: VALID_BODY.segmentId, question: VALID_BODY.question })); + expect(res.status).toBe(400); + }); + + it('returns 200 with reply on success', async () => { + mockGenerate.mockResolvedValue({ reply: 'Great question! This works because...' }); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.reply).toBe('Great question! This works because...'); + }); + + it('calls generateTangent with correct args', async () => { + mockGenerate.mockResolvedValue({ reply: 'answer' }); + await POST(makeRequest(VALID_BODY)); + expect(mockGenerate).toHaveBeenCalledWith({ + segmentId: VALID_BODY.segmentId, + question: VALID_BODY.question, + userId: VALID_BODY.userId, + }); + }); + + it('returns 429 when budget exceeded', async () => { + mockCheckBudget.mockRejectedValue(new BudgetExceededError('over budget')); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(429); + }); + + it('returns 500 on unexpected error', async () => { + mockGenerate.mockRejectedValue(new Error('LLM timeout')); + const res = await POST(makeRequest(VALID_BODY)); + expect(res.status).toBe(500); + }); +}); diff --git a/src/app/api/tangent/route.ts b/src/app/api/tangent/route.ts new file mode 100644 index 0000000..a63ab99 --- /dev/null +++ b/src/app/api/tangent/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { generateTangent } from '@/lib/generation/generate-tangent'; +import { BudgetExceededError } from '@/lib/budget'; +import { checkBudget, recordUsage } from '@/lib/budget'; +import { getOptionalSession } from '@/lib/auth-helpers'; + +const TangentRequestSchema = z.object({ + segmentId: z.string().uuid('segmentId must be a UUID'), + question: z.string().min(3, 'question too short').max(500, 'question too long'), + userId: z.string().min(1).optional(), +}); + +export async function POST(req: NextRequest): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const parsed = TangentRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues.map((i) => i.message).join('; ') }, + { status: 400 }, + ); + } + + const session = await getOptionalSession(); + const userId = session?.user?.id ?? parsed.data.userId; + if (!userId) { + return NextResponse.json({ error: 'userId required' }, { status: 400 }); + } + const { segmentId, question } = parsed.data; + + try { + await checkBudget(userId); + const result = await generateTangent({ segmentId, question, userId }); + void recordUsage(userId, 200).catch(() => {}); // rough estimate; actual usage tracked in generateTangent trace + return NextResponse.json({ reply: result.reply }); + } catch (err) { + if (err instanceof BudgetExceededError) { + return NextResponse.json( + { error: 'Session limit reached. Start a new session to continue.' }, + { status: 429 }, + ); + } + console.error('[POST /api/tangent]', err); + return NextResponse.json({ error: 'Failed to generate tangent explanation' }, { status: 500 }); + } +} diff --git a/src/app/api/user/account/__tests__/route.test.ts b/src/app/api/user/account/__tests__/route.test.ts new file mode 100644 index 0000000..e982094 --- /dev/null +++ b/src/app/api/user/account/__tests__/route.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; +import { requireAuth } from '@/lib/auth-helpers'; + +const mockTx = { + delete: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }), +}; + +const mockDb = { + transaction: vi.fn().mockImplementation(async (fn: (tx: typeof mockTx) => Promise) => fn(mockTx)), +}; + +vi.mock('@/lib/db', () => ({ db: mockDb })); + +vi.mock('@/lib/db/schema', () => ({ + users: {}, + responses: {}, + mastery: {}, + journeyInstances: {}, +})); + +vi.mock('drizzle-orm', () => ({ + eq: vi.fn((col, val) => ({ col, val })), +})); + +function makeRequest(): NextRequest { + return new NextRequest('http://localhost/api/user/account', { method: 'DELETE' }); +} + +describe('DELETE /api/user/account', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockTx.delete.mockReturnValue({ where: vi.fn().mockResolvedValue([]) }); + mockDb.transaction.mockImplementation(async (fn: (tx: typeof mockTx) => Promise) => fn(mockTx)); + }); + + it('returns 200 {ok:true} on happy path', async () => { + const { DELETE } = await import('../route'); + const res = await DELETE(); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body).toEqual({ ok: true }); + }); + + it('runs transaction and deletes all four tables', async () => { + const { DELETE } = await import('../route'); + await DELETE(); + + expect(mockDb.transaction).toHaveBeenCalledOnce(); + expect(mockTx.delete).toHaveBeenCalledTimes(4); + }); + + it('returns 401 when requireAuth throws', async () => { + vi.mocked(requireAuth).mockRejectedValueOnce(new Error('redirect')); + + const { DELETE } = await import('../route'); + await expect(DELETE()).rejects.toThrow('redirect'); + }); + + it('propagates transaction errors', async () => { + mockDb.transaction.mockRejectedValueOnce(new Error('db error')); + + const { DELETE } = await import('../route'); + await expect(DELETE()).rejects.toThrow('db error'); + }); +}); diff --git a/src/app/api/user/account/route.ts b/src/app/api/user/account/route.ts new file mode 100644 index 0000000..8f711b0 --- /dev/null +++ b/src/app/api/user/account/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { users, responses, mastery, journeyInstances } from '@/lib/db/schema'; +import { requireAuth } from '@/lib/auth-helpers'; + +export async function DELETE(): Promise { + const session = await requireAuth(); + const userId = session.user!.id!; + + await db.transaction(async (tx) => { + await tx.delete(responses).where(eq(responses.userId, userId)); + await tx.delete(mastery).where(eq(mastery.userId, userId)); + await tx.delete(journeyInstances).where(eq(journeyInstances.userId, userId)); + // accounts and sessions ON DELETE CASCADE from users FK + await tx.delete(users).where(eq(users.id, userId)); + }); + + // Session cookies become invalid once the session row is gone (cascaded above). + // Client must call signOut() after this response. + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/user/change-password/__tests__/route.test.ts b/src/app/api/user/change-password/__tests__/route.test.ts new file mode 100644 index 0000000..94f2471 --- /dev/null +++ b/src/app/api/user/change-password/__tests__/route.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +const { mockSelect, mockUpdate } = vi.hoisted(() => ({ + mockSelect: vi.fn(), + mockUpdate: vi.fn(), +})); + +vi.mock('@/lib/db', () => ({ db: { select: mockSelect, update: mockUpdate } })); + +vi.mock('bcryptjs', () => ({ + default: { hash: vi.fn().mockResolvedValue('new_hash'), compare: vi.fn().mockResolvedValue(true) }, +})); + +import { POST } from '../route'; +import bcrypt from 'bcryptjs'; +import { requireAuth } from '@/lib/auth-helpers'; + +function makeRequest(body: unknown) { + return new NextRequest('http://localhost/api/user/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +function setupDefault() { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([{ passwordHash: 'existing_hash' }]) }), + }), + }); + mockUpdate.mockReturnValue({ set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }); +} + +describe('POST /api/user/change-password', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(bcrypt.hash).mockResolvedValue('new_hash' as never); + vi.mocked(bcrypt.compare).mockResolvedValue(true as never); + setupDefault(); + }); + + it('returns 200 {ok:true} on success', async () => { + const res = await POST(makeRequest({ oldPassword: 'old123456', newPassword: 'new123456' })); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ ok: true }); + }); + + it('calls bcrypt.compare with old password and existing hash', async () => { + await POST(makeRequest({ oldPassword: 'old123456', newPassword: 'new123456' })); + expect(bcrypt.compare).toHaveBeenCalledWith('old123456', 'existing_hash'); + }); + + it('calls bcrypt.hash on new password', async () => { + await POST(makeRequest({ oldPassword: 'old123456', newPassword: 'new123456' })); + expect(bcrypt.hash).toHaveBeenCalledWith('new123456', 12); + }); + + it('returns 400 on bad JSON', async () => { + const req = new NextRequest('http://localhost/api/user/change-password', { + method: 'POST', + body: '{bad', + headers: { 'Content-Type': 'application/json' }, + }); + expect((await POST(req)).status).toBe(400); + }); + + it('returns 400 when oldPassword missing', async () => { + expect((await POST(makeRequest({ newPassword: 'new123456' }))).status).toBe(400); + }); + + it('returns 400 when newPassword missing', async () => { + expect((await POST(makeRequest({ oldPassword: 'old123456' }))).status).toBe(400); + }); + + it('returns 400 when newPassword too short', async () => { + expect((await POST(makeRequest({ oldPassword: 'old123456', newPassword: 'short' }))).status).toBe(400); + }); + + it('returns 400 when user has no password (OAuth account)', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([{ passwordHash: null }]) }), + }), + }); + const res = await POST(makeRequest({ oldPassword: 'old123456', newPassword: 'new123456' })); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/no password/i); + }); + + it('returns 400 when user not found', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }), + }), + }); + const res = await POST(makeRequest({ oldPassword: 'old123456', newPassword: 'new123456' })); + expect(res.status).toBe(400); + }); + + it('returns 400 when old password wrong', async () => { + vi.mocked(bcrypt.compare).mockResolvedValueOnce(false as never); + const res = await POST(makeRequest({ oldPassword: 'wrong', newPassword: 'new123456' })); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/incorrect/i); + }); + + it('propagates requireAuth rejection', async () => { + vi.mocked(requireAuth).mockRejectedValueOnce(new Error('redirect')); + await expect(POST(makeRequest({ oldPassword: 'old', newPassword: 'new12345' }))).rejects.toThrow('redirect'); + }); +}); diff --git a/src/app/api/user/change-password/route.ts b/src/app/api/user/change-password/route.ts new file mode 100644 index 0000000..69d25b4 --- /dev/null +++ b/src/app/api/user/change-password/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import bcrypt from 'bcryptjs'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +import { requireAuth } from '@/lib/auth-helpers'; + +const Schema = z.object({ + oldPassword: z.string().min(1), + newPassword: z.string().min(8).max(128), +}); + +export async function POST(req: NextRequest): Promise { + const session = await requireAuth(); + const userId = session.user!.id!; + + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = Schema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, { status: 400 }); + + const [user] = await db.select({ passwordHash: users.passwordHash }).from(users).where(eq(users.id, userId)).limit(1); + if (!user?.passwordHash) return NextResponse.json({ error: 'No password set on this account' }, { status: 400 }); + + const valid = await bcrypt.compare(parsed.data.oldPassword, user.passwordHash); + if (!valid) return NextResponse.json({ error: 'Current password is incorrect' }, { status: 400 }); + + const newHash = await bcrypt.hash(parsed.data.newPassword, 12); + await db.update(users).set({ passwordHash: newHash }).where(eq(users.id, userId)); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/user/profile/__tests__/route.test.ts b/src/app/api/user/profile/__tests__/route.test.ts new file mode 100644 index 0000000..8da1bc8 --- /dev/null +++ b/src/app/api/user/profile/__tests__/route.test.ts @@ -0,0 +1,139 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; + +vi.mock('@/lib/db', () => ({ + db: { + update: vi.fn(), + }, +})); + +import { PATCH } from '../route'; +import { db } from '@/lib/db'; +import { requireAuth } from '@/lib/auth-helpers'; + +function makeRequest(body: unknown, options: RequestInit = {}): NextRequest { + return new NextRequest('http://localhost/api/user/profile', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + ...options, + }); +} + +function makeBadJsonRequest(): NextRequest { + return new NextRequest('http://localhost/api/user/profile', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: 'not valid json{{{', + }); +} + +function setupUpdateMock() { + vi.mocked(db).update = vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + }); +} + +describe('PATCH /api/user/profile', () => { + beforeEach(() => { + vi.clearAllMocks(); + setupUpdateMock(); + }); + + it('returns 200 with {ok:true} on valid name update', async () => { + const res = await PATCH(makeRequest({ name: 'New Name' })); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + }); + + it('returns 200 with {ok:true} on valid image update', async () => { + const res = await PATCH(makeRequest({ image: 'https://example.com/avatar.png' })); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + }); + + it('returns 200 with {ok:true} on updating both name and image', async () => { + const res = await PATCH(makeRequest({ name: 'Jane', image: 'https://example.com/jane.png' })); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ ok: true }); + }); + + it('calls db.update with the parsed data and correct user id', async () => { + const mockWhere = vi.fn().mockResolvedValue([]); + const mockSet = vi.fn().mockReturnValue({ where: mockWhere }); + vi.mocked(db).update = vi.fn().mockReturnValue({ set: mockSet }); + + await PATCH(makeRequest({ name: 'Alice' })); + + expect(vi.mocked(db).update).toHaveBeenCalledOnce(); + expect(mockSet).toHaveBeenCalledWith({ name: 'Alice' }); + expect(mockWhere).toHaveBeenCalledOnce(); + }); + + it('returns 400 on invalid JSON body', async () => { + const res = await PATCH(makeBadJsonRequest()); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + expect(json.error).toMatch(/invalid json/i); + }); + + it('returns 400 when name is empty string', async () => { + const res = await PATCH(makeRequest({ name: '' })); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + }); + + it('returns 400 when image is not a valid URL', async () => { + const res = await PATCH(makeRequest({ image: 'not-a-url' })); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + }); + + it('returns 400 when name exceeds max length', async () => { + const res = await PATCH(makeRequest({ name: 'a'.repeat(256) })); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + }); + + it('returns 400 when image URL exceeds max length', async () => { + const res = await PATCH(makeRequest({ image: 'https://example.com/' + 'a'.repeat(2048) })); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toHaveProperty('error'); + }); + + it('returns 401 when requireAuth throws (unauthenticated)', async () => { + vi.mocked(requireAuth).mockRejectedValueOnce(new Error('redirect')); + + await expect(PATCH(makeRequest({ name: 'Test' }))).rejects.toThrow('redirect'); + }); + + it('does not call db.update when validation fails', async () => { + await PATCH(makeRequest({ name: '' })); + expect(vi.mocked(db).update).not.toHaveBeenCalled(); + }); + + it('does not call db.update when JSON is invalid', async () => { + await PATCH(makeBadJsonRequest()); + expect(vi.mocked(db).update).not.toHaveBeenCalled(); + }); + + it('propagates db errors', async () => { + vi.mocked(db).update = vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockRejectedValue(new Error('DB failure')), + }), + }); + + await expect(PATCH(makeRequest({ name: 'Bob' }))).rejects.toThrow('DB failure'); + }); +}); diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts new file mode 100644 index 0000000..0661826 --- /dev/null +++ b/src/app/api/user/profile/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +import { requireAuth } from '@/lib/auth-helpers'; + +const ProfileSchema = z.object({ + name: z.string().min(1).max(255).optional(), + image: z.string().url().max(2048).optional(), +}); + +export async function PATCH(req: NextRequest): Promise { + const session = await requireAuth(); + const userId = session.user!.id!; + + let body: unknown; + try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } + + const parsed = ProfileSchema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, { status: 400 }); + + await db.update(users).set(parsed.data).where(eq(users.id, userId)); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/auth/error/page.tsx b/src/app/auth/error/page.tsx new file mode 100644 index 0000000..a9d6217 --- /dev/null +++ b/src/app/auth/error/page.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import Link from 'next/link'; + +const ERROR_MESSAGES: Record = { + Configuration: 'Server configuration error. Contact support.', + AccessDenied: 'Access denied.', + Verification: 'Verification link expired or already used. Request a new one.', + OAuthSignin: 'Could not connect to the OAuth provider. Try again.', + OAuthCallback: 'OAuth callback error. Try again.', + OAuthCreateAccount: 'Could not create account via OAuth.', + EmailCreateAccount: 'Could not create account with that email.', + Callback: 'Authentication callback error.', + OAuthAccountNotLinked: 'This email is already registered with a different sign-in method.', + EmailSignin: 'Email sign-in failed.', + CredentialsSignin: 'Invalid email or password.', + SessionRequired: 'You must be signed in to access that page.', + Default: 'An authentication error occurred.', +}; + +export default function AuthErrorPage() { + const params = useSearchParams(); + const code = params.get('error') ?? 'Default'; + const message = ERROR_MESSAGES[code] ?? ERROR_MESSAGES.Default; + + return ( +
+

+ Sign-in error +

+

{message}

+ + Back to sign in + +
+ ); +} diff --git a/src/app/auth/forgot-password/page.tsx b/src/app/auth/forgot-password/page.tsx new file mode 100644 index 0000000..20f8c69 --- /dev/null +++ b/src/app/auth/forgot-password/page.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; + +export default function ForgotPasswordPage() { + const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); + const [sent, setSent] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + const res = await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + setLoading(false); + if (res.ok) setSent(true); + else setError('Something went wrong. Try again.'); + }; + + if (sent) { + return ( +
+

Check your inbox

+

+ If an account exists for that email, we sent a reset link. It expires in 1 hour. +

+ Back to sign in +
+ ); + } + + return ( +
+

Reset password

+
+ + setEmail(e.target.value)} + required + autoComplete="email" + style={s.input} + /> + {error &&

{error}

} + +
+ Back to sign in +
+ ); +} + +const s = { + page: { display: 'flex', flexDirection: 'column' as const, alignItems: 'center', minHeight: '100svh', padding: 'var(--space-8)', paddingTop: 'var(--space-16)', gap: 'var(--space-4)' }, + heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-4)' }, + body: { color: 'var(--color-ink-muted)', maxWidth: '26rem', textAlign: 'center' as const, lineHeight: 'var(--leading-body)' }, + form: { width: '100%', maxWidth: '26rem', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-2)' }, + label: { fontFamily: 'var(--font-display)', fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-ink)', marginTop: 'var(--space-3)' }, + input: { padding: 'var(--space-3) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', fontFamily: 'var(--font-body)', fontSize: '1rem', color: 'var(--color-ink)', outline: 'none' }, + error: { color: 'var(--color-misconception)', fontSize: '0.875rem', margin: 0 }, + btn: { marginTop: 'var(--space-4)', padding: 'var(--space-3) var(--space-6)', background: 'var(--color-accent)', color: '#fff', border: 'none', borderRadius: '4px', fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500, cursor: 'pointer' }, + link: { color: 'var(--color-accent)', fontFamily: 'var(--font-display)', fontSize: '0.875rem', textDecoration: 'none' }, +} as const; diff --git a/src/app/auth/login/login-client.tsx b/src/app/auth/login/login-client.tsx new file mode 100644 index 0000000..0625172 --- /dev/null +++ b/src/app/auth/login/login-client.tsx @@ -0,0 +1,234 @@ +'use client'; + +import { useState } from 'react'; +import { signIn } from 'next-auth/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import Link from 'next/link'; + +interface Props { + hasOidc: boolean; + oidcLabel: string; + hasGoogle: boolean; + hasGitHub: boolean; +} + +export default function LoginClient({ hasOidc, oidcLabel, hasGoogle, hasGitHub }: Props) { + const router = useRouter(); + const params = useSearchParams(); + const callbackUrl = params.get('callbackUrl') ?? '/'; + const resetSuccess = params.get('reset') === '1'; + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleCredentials = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + const res = await signIn('credentials', { + email, + password, + redirect: false, + }); + setLoading(false); + if (res?.error) { + setError('Invalid email or password.'); + } else { + await migrateThenRedirect(callbackUrl, router); + } + }; + + return ( +
+

Sign in

+ {resetSuccess && ( +

Password updated. Sign in with your new password.

+ )} +
+ + setEmail(e.target.value)} + required + autoComplete="email" + style={styles.input} + /> + + setPassword(e.target.value)} + required + autoComplete="current-password" + style={styles.input} + /> + {error &&

{error}

} + + Forgot password? +
+ + {(hasGoogle || hasGitHub || hasOidc) &&
or
} + +
+ {hasGoogle && ( + + )} + {hasGitHub && ( + + )} + {hasOidc && ( + + )} +
+ +

+ No account?{' '} + Register +

+
+ ); +} + +async function migrateThenRedirect(callbackUrl: string, router: ReturnType) { + const anonId = sessionStorage.getItem('curio_uid'); + if (anonId) { + try { + await fetch('/api/auth/migrate-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ anonUserId: anonId }), + }); + sessionStorage.removeItem('curio_uid'); + } catch { + // non-blocking + } + } + router.push(callbackUrl); +} + +const styles = { + page: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + minHeight: '100svh', + padding: 'var(--space-8)', + paddingTop: 'var(--space-16)', + gap: 'var(--space-4)', + }, + heading: { + fontFamily: 'var(--font-display)', + fontSize: '1.5rem', + fontWeight: 500, + color: 'var(--color-ink)', + marginBottom: 'var(--space-4)', + }, + form: { + width: '100%', + maxWidth: '26rem', + display: 'flex', + flexDirection: 'column' as const, + gap: 'var(--space-2)', + }, + label: { + fontFamily: 'var(--font-display)', + fontSize: '0.875rem', + fontWeight: 500, + color: 'var(--color-ink)', + marginTop: 'var(--space-3)', + }, + input: { + width: '100%', + padding: 'var(--space-3) var(--space-4)', + background: 'var(--color-surface)', + border: '1px solid var(--color-border)', + borderRadius: '4px', + fontFamily: 'var(--font-body)', + fontSize: '1rem', + color: 'var(--color-ink)', + outline: 'none', + }, + error: { + color: 'var(--color-misconception)', + fontSize: '0.875rem', + margin: 0, + }, + btn: { + marginTop: 'var(--space-4)', + padding: 'var(--space-3) var(--space-6)', + background: 'var(--color-accent)', + color: '#fff', + border: 'none', + borderRadius: '4px', + fontFamily: 'var(--font-display)', + fontSize: '0.9375rem', + fontWeight: 500, + cursor: 'pointer', + }, + divider: { + width: '100%', + maxWidth: '26rem', + display: 'flex', + alignItems: 'center', + gap: 'var(--space-3)', + color: 'var(--color-ink-faint)', + fontSize: '0.875rem', + marginTop: 'var(--space-2)', + }, + oauthGroup: { + width: '100%', + maxWidth: '26rem', + display: 'flex', + flexDirection: 'column' as const, + gap: 'var(--space-2)', + }, + oauthBtn: { + width: '100%', + padding: 'var(--space-3) var(--space-4)', + background: 'var(--color-surface)', + border: '1px solid var(--color-border)', + borderRadius: '4px', + fontFamily: 'var(--font-display)', + fontSize: '0.9375rem', + color: 'var(--color-ink)', + cursor: 'pointer', + }, + footer: { + fontSize: '0.875rem', + color: 'var(--color-ink-muted)', + marginTop: 'var(--space-4)', + }, + link: { + color: 'var(--color-accent)', + textDecoration: 'none', + }, + forgotLink: { + color: 'var(--color-ink-muted)', + fontSize: '0.8125rem', + textDecoration: 'none', + textAlign: 'right' as const, + marginTop: 'var(--space-1)', + }, + success: { + width: '100%', + maxWidth: '26rem', + padding: 'var(--space-3) var(--space-4)', + background: 'var(--color-mastered-bg, #eaf5ea)', + color: 'var(--color-mastered, #1a6e2e)', + borderRadius: '4px', + fontSize: '0.875rem', + margin: 0, + }, +} as const; diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx new file mode 100644 index 0000000..530c626 --- /dev/null +++ b/src/app/auth/login/page.tsx @@ -0,0 +1,16 @@ +import LoginClient from './login-client'; + +export default function LoginPage() { + const hasOidc = Boolean(process.env.AUTH_OIDC_ISSUER); + const oidcLabel = process.env.AUTH_OIDC_DISPLAY_NAME ?? 'SSO'; + const hasGoogle = Boolean(process.env.AUTH_GOOGLE_ID); + const hasGitHub = Boolean(process.env.AUTH_GITHUB_ID); + return ( + + ); +} diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx new file mode 100644 index 0000000..2ef2dfb --- /dev/null +++ b/src/app/auth/register/page.tsx @@ -0,0 +1,189 @@ +'use client'; + +import { useState } from 'react'; +import { signIn } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +export default function RegisterPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirm) { + setError('Passwords do not match.'); + return; + } + setLoading(true); + setError(null); + + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, name, password }), + }); + + if (!res.ok) { + const data = (await res.json()) as { error?: string }; + setError(data.error ?? 'Registration failed.'); + setLoading(false); + return; + } + + const signInRes = await signIn('credentials', { email, password, redirect: false }); + if (signInRes?.error) { + setError('Account created but sign-in failed. Try signing in manually.'); + setLoading(false); + return; + } + + const anonId = sessionStorage.getItem('curio_uid'); + if (anonId) { + try { + await fetch('/api/auth/migrate-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ anonUserId: anonId }), + }); + sessionStorage.removeItem('curio_uid'); + } catch { + // non-blocking + } + } + + router.push('/'); + }; + + return ( +
+

Create account

+
+ + setName(e.target.value)} + required + autoComplete="name" + style={styles.input} + /> + + setEmail(e.target.value)} + required + autoComplete="email" + style={styles.input} + /> + + setPassword(e.target.value)} + required + minLength={8} + autoComplete="new-password" + style={styles.input} + /> + + setConfirm(e.target.value)} + required + autoComplete="new-password" + style={styles.input} + /> + {error &&

{error}

} + +
+

+ Already have an account?{' '} + Sign in +

+
+ ); +} + +const styles = { + page: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + minHeight: '100svh', + padding: 'var(--space-8)', + paddingTop: 'var(--space-16)', + gap: 'var(--space-4)', + }, + heading: { + fontFamily: 'var(--font-display)', + fontSize: '1.5rem', + fontWeight: 500, + color: 'var(--color-ink)', + marginBottom: 'var(--space-4)', + }, + form: { + width: '100%', + maxWidth: '26rem', + display: 'flex', + flexDirection: 'column' as const, + gap: 'var(--space-2)', + }, + label: { + fontFamily: 'var(--font-display)', + fontSize: '0.875rem', + fontWeight: 500, + color: 'var(--color-ink)', + marginTop: 'var(--space-3)', + }, + input: { + width: '100%', + padding: 'var(--space-3) var(--space-4)', + background: 'var(--color-surface)', + border: '1px solid var(--color-border)', + borderRadius: '4px', + fontFamily: 'var(--font-body)', + fontSize: '1rem', + color: 'var(--color-ink)', + outline: 'none', + }, + error: { + color: 'var(--color-misconception)', + fontSize: '0.875rem', + margin: 0, + }, + btn: { + marginTop: 'var(--space-4)', + padding: 'var(--space-3) var(--space-6)', + background: 'var(--color-accent)', + color: '#fff', + border: 'none', + borderRadius: '4px', + fontFamily: 'var(--font-display)', + fontSize: '0.9375rem', + fontWeight: 500, + cursor: 'pointer', + }, + footer: { + fontSize: '0.875rem', + color: 'var(--color-ink-muted)', + marginTop: 'var(--space-4)', + }, + link: { + color: 'var(--color-accent)', + textDecoration: 'none', + }, +} as const; diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx new file mode 100644 index 0000000..b621a12 --- /dev/null +++ b/src/app/auth/reset-password/page.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useState } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import Link from 'next/link'; + +export default function ResetPasswordPage() { + const params = useSearchParams(); + const token = params.get('token') ?? ''; + const router = useRouter(); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + if (!token) { + return ( +
+

Invalid reset link.

+ Request a new one +
+ ); + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirm) { setError('Passwords do not match.'); return; } + setLoading(true); + setError(null); + const res = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token, password }), + }); + setLoading(false); + if (res.ok) { + router.push('/auth/login?reset=1'); + } else { + const d = (await res.json()) as { error?: string }; + setError(d.error ?? 'Failed to reset password.'); + } + }; + + return ( +
+

Set new password

+
+ + setPassword(e.target.value)} required minLength={8} autoComplete="new-password" style={s.input} /> + + setConfirm(e.target.value)} required autoComplete="new-password" style={s.input} /> + {error &&

{error}

} + +
+
+ ); +} + +const s = { + page: { display: 'flex', flexDirection: 'column' as const, alignItems: 'center', minHeight: '100svh', padding: 'var(--space-8)', paddingTop: 'var(--space-16)', gap: 'var(--space-4)' }, + heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-4)' }, + form: { width: '100%', maxWidth: '26rem', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-2)' }, + label: { fontFamily: 'var(--font-display)', fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-ink)', marginTop: 'var(--space-3)' }, + input: { padding: 'var(--space-3) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', fontFamily: 'var(--font-body)', fontSize: '1rem', color: 'var(--color-ink)', outline: 'none' }, + error: { color: 'var(--color-misconception)', fontSize: '0.875rem', margin: 0 }, + btn: { marginTop: 'var(--space-4)', padding: 'var(--space-3) var(--space-6)', background: 'var(--color-accent)', color: '#fff', border: 'none', borderRadius: '4px', fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500, cursor: 'pointer' }, + link: { color: 'var(--color-accent)', fontFamily: 'var(--font-display)', fontSize: '0.875rem', textDecoration: 'none' }, +} as const; diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx new file mode 100644 index 0000000..23146c1 --- /dev/null +++ b/src/app/auth/verify-email/page.tsx @@ -0,0 +1,33 @@ +export default function VerifyEmailPage() { + return ( +
+

+ Check your inbox +

+

+ We sent a verification link to your email. Click it to confirm your address and activate your account. +

+

+ Didn't get it? Check spam, or try registering again. +

+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..45bea69 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,64 @@ +@import "tailwindcss"; +@import "../styles/tokens.css"; +@import "../styles/reading.css"; +@import "../styles/app.css"; + +@layer base { + *, + *::before, + *::after { + box-sizing: border-box; + } + + html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + } + + body { + background-color: var(--color-ground); + color: var(--color-ink); + font-family: var(--font-body); + font-size: 1.0625rem; /* ~17px — comfortable for sustained reading */ + line-height: var(--leading-body); + margin: 0; + transition: background-color var(--duration-base) var(--ease-out), + color var(--duration-base) var(--ease-out); + } + + /* Remove default margin from typography elements */ + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: var(--font-display); + line-height: var(--leading-display); + font-weight: 500; + margin: 0; + } + + p { + margin: 0; + } + + /* Visible focus — WCAG-AA required */ + :focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + border-radius: 2px; + } + + /* Reduced motion — honored everywhere */ + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..80c8d90 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,58 @@ +import type { Metadata } from 'next'; +import { Newsreader, Inter } from 'next/font/google'; +import { SessionProvider } from 'next-auth/react'; +import { redirect } from 'next/navigation'; +import { headers } from 'next/headers'; +import { auth } from '@/auth'; +import { isAuthRequired } from '@/lib/site-config'; +import { SiteHeader } from '@/components/site-header'; +import './globals.css'; + +// Set the persisted theme before first paint to avoid a flash. Inlined; no deps. +const THEME_BOOTSTRAP = `(function(){try{var t=localStorage.getItem('curio-theme');if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();`; + +const newsreader = Newsreader({ + subsets: ['latin'], + variable: '--font-body', + style: ['normal', 'italic'], + weight: ['300', '400', '500', '600'], + display: 'swap', +}); + +const inter = Inter({ + subsets: ['latin'], + variable: '--font-display', + display: 'swap', +}); + +export const metadata: Metadata = { + title: 'Curio', + description: 'A curiosity-led mastery tutor.', +}; + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const [session, authRequired] = await Promise.all([auth(), isAuthRequired()]); + + if (authRequired && !session?.user) { + const hdrs = await headers(); + const pathname = hdrs.get('x-pathname') ?? '/'; + // Skip redirect loop for auth pages + if (!pathname.startsWith('/auth/')) { + redirect('/auth/login'); + } + } + + return ( + + +