Pārlūkot izejas kodu

Loop prototype: Hono + Better Auth + D1, Hand DO with live Jev odds and the lever, mobile client, spec and day-one learnings

Claude 1 mēnesi atpakaļ
revīzija
6642db7375

+ 3 - 0
.dev.vars.example

@@ -0,0 +1,3 @@
+TYPESAFE_API_KEY=
+BETTER_AUTH_SECRET=
+OTP_MODE=console

+ 36 - 0
.gitignore

@@ -0,0 +1,36 @@
+# prod
+dist/
+
+# dev
+.yarn/
+!.yarn/releases
+.vscode/*
+!.vscode/launch.json
+!.vscode/*.code-snippets
+.idea/workspace.xml
+.idea/usage.statistics.xml
+.idea/shelf
+
+# deps
+node_modules/
+.wrangler
+
+# env
+.env
+.env.production
+.dev.vars
+
+# logs
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# misc
+.DS_Store
+.wrangler/
+worker-configuration.d.ts
+node_modules/

+ 11 - 0
.prettierignore

@@ -0,0 +1,11 @@
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+bun.lock
+bun.lockb
+.next
+.open-next
+node_modules
+dist
+build
+.wrangler

+ 9 - 0
.prettierrc

@@ -0,0 +1,9 @@
+{
+    "tabWidth": 4,
+    "printWidth": 120,
+    "semi": true,
+    "singleQuote": false,
+    "trailingComma": "es5",
+    "bracketSpacing": true,
+    "arrowParens": "avoid"
+}

+ 38 - 0
README.md

@@ -0,0 +1,38 @@
+# Plan Maker 2000 (decision-maker-2000) — loop prototype
+
+Spec: `docs/spec.md` (exported from the Claude Doc, which is the editable source). Learnings: `docs/blog/`.
+
+Hono + Better Auth (phone OTP + anonymous) + D1/Drizzle + one Durable Object per hand + Jev (TypeSafe System One) for live odds and the lever.
+
+## Dev loop
+
+```bash
+npm install
+cp .dev.vars.example .dev.vars        # then paste TYPESAFE_API_KEY + BETTER_AUTH_SECRET
+npm run db:migrate:dev                 # local D1 (miniflare)
+npm run dev                            # http://localhost:8787  — OTP codes print to this console
+```
+
+Open `/` on two phones on the same LAN (or `wrangler dev --ip 0.0.0.0`), "Jump in" with a name, create a hand, open `/h/<id>` on the other phone.
+
+### Scripts
+- `npm run dev` — wrangler dev, local D1 + DO, hot reload
+- `npm run cf-typegen` — regenerate `worker-configuration.d.ts` after editing wrangler.toml
+- `npm run auth:update` then `npm run db:generate` — after changing Better Auth plugins
+- `npm run db:migrate:dev` / `:prod`
+- `npm run deploy` — `wrangler deploy` (first time: `wrangler d1 create pm2000-db`, paste id into wrangler.toml, `wrangler secret put TYPESAFE_API_KEY`, `wrangler secret put BETTER_AUTH_SECRET`)
+- Preview deploys: `npx wrangler versions upload` gives a preview URL without touching prod
+
+### Where things are
+- `src/hand.ts` — the Hand DO: state, WebSocket fan-out, chips/veto/lock, debounced Jev odds, the lever (seeded sample), rounds
+- `src/jev.ts` — typed Jev client
+- `src/dealer.ts` — human-authored dealer line bank; Jev picks by tag
+- `src/auth/index.ts` — phone OTP (console in dev, Twilio when OTP_MODE=twilio) + anonymous
+- `public/index.html` — mobile client: felt table, chip stacks, live odds bars, lever, reels reveal with near-miss, rounds
+- `wrangler.toml` `[vars].PICK_INSTRUCTIONS` — the "semi-decision-makery" dial. Edit and hot-reload.
+
+### Driving it from the CLI
+`scripts/drive.sh` runs a full two-member hand against a running dev server (sign-in, chips, veto, lock, pull, again, accept) and prints odds, winner, reel order and dealer line.
+
+### Not here yet
+Options agent (screenshot intake / "More options" regen — the client prompts for labels), tables, ledger, rubber-banding, expiry alarms, run structure.

+ 5 - 0
docs/README.md

@@ -0,0 +1,5 @@
+# docs
+
+- `spec.md` — the living spec, exported from the Claude Doc (the editable source of truth). Re-export after material changes.
+- `concept-2026-09-13.md` — the original concept doc this started from, kept for the record.
+- `blog/` — dated learnings. One post per meaningful session; findings, not status.

+ 40 - 0
docs/blog/2026-09-19-day-one-jev-is-the-lever.md

@@ -0,0 +1,40 @@
+---
+title: "Day one: Jev is the lever"
+date: 2026-09-19
+---
+
+# Day one: Jev is the lever
+
+Started from a concept doc that said "no gambling", ended the day with a working slot machine for group decisions. Here's what changed and what we learned.
+
+## The reframe
+
+The concept doc treated "no money" and "no stakes" as the same constraint. They aren't. The regulatory line is value changing hands; the fun is odds, near-misses and a named winner. So chips came back, vetoes came back, and the lever pull became the ritual. Balatro structure: table (persistent group), run (a sequence of hands toward a goal), hand (one decision), round (one pass of options → chips → lever → posted decision).
+
+## Jev is not an LLM, and that's the whole point
+
+We'd sketched "Jev" as a text-generating negotiator. Then we read what it actually is: TypeSafe's System One model. State in, calibrated probabilities out, ~350 ms, no text, no hallucination. That shape fits a live odds engine far better than a chatbot does:
+
+- every chip drag is a state change → Jev re-answers a fixed question set → the odds bar moves
+- the lever samples Jev's distribution, seeded, so the reveal is replayable
+- every string a player sees is human-authored; Jev picks the line by tag from a bank
+
+## What the API told us (first contact, real key)
+
+| Finding | Evidence |
+| --- | --- |
+| Latency | 6 questions in one call: 320–420 ms. One question: ~200 ms. Debounce at 200 ms and ease the bar. |
+| Chips dominate | 9 vs 10 → 45/43. 8 concentrated beats 10 spread across four options (93 %). |
+| **The fairness dial is prose** | Same chips, Sam won the last four hands. No fairness clause → pizza 99 %. "Be fair across the ledger" → pizza 18 %. "Lean toward the others" → pizza 5 %. The instruction text is the tuning surface. It's a config var now. |
+| Classification wants features | "Which format?" over raw chips picked craps every time. Pre-compute `chip_agreement`, `live_option_count`, `spice` into the state and name them in the criteria → 97–100 % correct. |
+| Spice reads low | Contention 0.76, spice 0.27 on the same round. Chip position alone doesn't read as heat; needs velocity, vetoes, re-placements. |
+| Big banks are fine | 40 dealer tags in 301 ms. Duplicate tags split probability, so tags must be unique. |
+| Straight sampling punishes unanimity | Both members 10-on-Thai, Jev 78 %, the sample landed Ramen at 6 %. Fix: `one_tap` format confirms instead of sampling. |
+
+## What exists tonight
+
+Hono + Better Auth (phone OTP, anonymous) + D1 from the `better-auth-cloudflare` CLI; one Durable Object per hand with WebSocket fan-out, debounced Jev odds, seeded lever with the runner-up placed last in the reel order; a one-file mobile client with a CRT reels reveal and an UPSET banner. `scripts/drive.sh` ran a full two-member hand end to end. The dealer line for a round where Tom's veto sent Sam's chips home: "That veto did some heavy lifting."
+
+## Next
+
+Two phones. Does dragging a chip and watching the odds move feel like anything? Everything else waits on that.

+ 60 - 0
docs/concept-2026-09-13.md

@@ -0,0 +1,60 @@
+---
+tags: [project, idea, active]
+created: 2026-09-13
+status: 🟡 Concept — pre-validate, not committed
+type: side-project / product idea
+---
+
+# Plan Maker 2000 — decision-room app
+
+**One-line pitch:** a shareable, ephemeral "decision room" you boot from a group chat when a chat is stuck — options get enumerated, a hectic probability machine underneath resolves it, everyone moves on.
+
+**Origin moment (the problem it solves):** in a WhatsApp chat with your partner and you can't decide on something → move the convo to this. Also framed as "dinner decision maker" / "magic 8 ball". Language used: *"normal chat vibe on top but hectic probability machine underneath."*
+
+## Core insight (the wedge)
+Decisions don't have **state** in iMessage/WhatsApp — options, vetoes, constraints, who's paying, "is it actually decided?" all live in scrollback purgatory. A **decision-native surface** (tiny, social, multiplayer decision matrix) is the real product shape. The wedge insight from Kelpie's critique: **build the decision-room, not the chat-app** — a one-off room you boot on demand, not inbox #14.
+
+## Shape
+- Shareable link spins up a decision "room"; invites the relevant people; dies in ~24h
+- *A moment, not an inbox* — no expectation of network effects or daily DAUs
+- Chill, chat-like UI on top; the **reveal** is the payoff
+
+## Feature sketch
+- Option enumeration / fill-in (structured, not scrollback)
+- Expiry pressure: "must decide by 8pm or suffer default preset"
+- **Veto chips:** everyone gets N vetoes, spendable exactly once
+- Confidence-weighted / probability voting; bet on outcomes
+- Dice roll as the ritual tiebreaker ("the dice have spoken")
+- Animated probability-resolution reveal (meme-able, screenshot-able)
+- **Asset memory (retention engine):** recurring decisions, saved preferences, ledger of past resolutions ("we always fight about Friday dinner")
+
+## The Jev question
+- Jev as final decider = risky: "the AI picked" is a **worse** hard feeling than "the dice picked" (dice: nobody's fault; AI: someone's pick got validated)
+- Better role: **AI as negotiator** — propose ranked options from chat history, surface what each side cares about, force clarity ("is the no because of $ or Tuesday?")… then hand the final call to dice/veto. *AI prepares the gunfight, dice fire it.*
+
+## Validation next step
+Ship the smallest room (dice UI + enumeration + expiry + veto chips + reveal) to **one couple**; watch whether a WhatsApp group chain lights up on a Saturday night. Market signal = spontaneous re-share.
+
+## Open questions
+- Name: "Plan Maker 2000"? Dinner Decision Maker? Magic 8 Ball homage? (brandable room-name matters for share links)
+- Where does "move the convo" handoff live: paste-in import from WhatsApp, or manual entry?
+- Monetisation shape: free rooms + paid recurring/premium ledger? TBD later — don't block the wedge.
+- Jev integration: real-time model access? cost per room?
+
+## Log
+- 2026-09-13 Idea raised in chat (WhatsApp-chat hook; dinner/magic-8-ball framing); feedback loop done; direction locked: **decision-room, not chat app**. Note filed in Projects/.
+
+
+## Handover notes for engineering (prototype brief context) — 13 Sep 2026
+Context beyond the feature sketch that will otherwise cause rework:
+
+1. **NO money anywhere.** The concept mixes 'probability', 'voting' and 'betting' language — do NOT gamify with stakes/rewards/payouts. AU gambling regulation applies the moment value changes hands (even credits in some framings). Veto chips = spendable-once counters only, no purchases.
+2. **Identity is name-only.** No auth, no accounts, no profiles. A room link + a typed name is the entire identity system. Consequence: no cross-room user history in v1 (asset memory becomes per-device/localStorage until there's an account reason).
+3. **'Chat vibe' is a skin, not infrastructure.** Do not build messaging: no DMs, no message history, no notification backend, no thread persistence. It's a sequence of structured state screens (options → chips → resolution → reveal) dressed casually. If a screen looks like a conversation, it's the *layout* borrowing chat tropes, not a chat engine.
+4. **State machine is the real spec.** A room = exactly ONE decision with terminal states: expired (default preset applies) OR resolved (dice/veto outcome). Nothing reopens, nothing continues. The ephemerality is the product — resist persistent rooms.
+5. **WhatsApp handoff = paste-in.** No API integration (WhatsApp Business API is heavy and off-brand). The flow is: user copies a few messages / types the question and options manually. Design the entry form to make that fast (3 options max suggested, free-text allowed).
+6. **Mobile web first.** Share-sheet targets are phones (iOS/Android browser). No native app, no app store, no push. Expiry reminders are in-room only.
+7. **The reveal is the product moment.** Budget real animation effort here (probability resolve → ritual line 'the dice have spoken'). Screenshot-ability is a success metric — capture-safe layout.
+8. **Jev/LLM negotiator is a stretch goal, not v1.** Dice + enumeration + veto ships first. AI negotiator needs chat-history context that paste-in only partially provides.
+9. **Tone guardrail:** Australian-English, irreverent but warm. The 'hard feeling' risk (-bloc policing the method) is the failure state to avoid — veto chips exist FOR this.
+10. **Success metric for prototype:** an unsolicited share (someone forwarding the room link after losing the decision). If nobody re-shares, the wedge is wrong, not the UI.

+ 420 - 0
docs/spec.md

@@ -0,0 +1,420 @@
+# Plan Maker 2000 — Spec v0.1
+
+Living spec. Source of truth is the Claude Doc (see README); this file is exported from it. Last export 2026-09-19.
+
+## Summary
+
+Plan Maker 2000 is a mobile-web decision game you boot from a stuck group chat: screenshot the argument, an agent turns it into options, everyone places yes and veto chips, Jev pulls the lever, the decision posts, and the table either runs with it or goes another round. Balatro structure (table, run, hand, round), no messaging, no real money.
+
+The pitch line still holds: normal chat vibe on top, hectic probability machine underneath. The probability machine is Jev.
+
+## Use cases
+
+Design for dinner tonight first; the holiday run must fit the same primitives without a second model. Friday dinner falls out of the ledger for free.
+
+| Priority | Use case | People | Hands | Duration | What it stresses |
+| --- | --- | --- | --- | --- | --- |
+| 1 | Dinner tonight | 2–6 | 1 | minutes | intake speed, reveal, re-share |
+| 2 | Holiday planning | 2–6 | 3–10 over days | days | run structure, veto scarcity, expiry |
+| 3 | Recurring Friday dinner | 2–4 | 1 per week | weeks | ledger, streaks, rubber-banding |
+
+The couple case (2 people) is the hard-feelings test; the group case (4–6) is the re-share test. Both must work, but only the group produces the success metric.
+
+## Core model: table, run, hand
+
+Three objects, borrowed directly from Balatro's run/hand structure. Everything else in the product hangs off them.
+
+| Object | What it is | Lifetime | Holds |
+| --- | --- | --- | --- |
+| Table | A group of people (phone numbers) | persistent | members, ledger, veto allowance per run |
+| Run | An optional sequence of hands toward one goal ("Japan trip") | until last hand accepted, or 14 days idle | ordered hands, remaining vetoes |
+| Hand | One decision, looped through rounds until accepted | until accepted or expired (default 24 h) | question, rounds, final decision |
+| Round | One pass of options → chips → lever → posted decision | minutes | options, chip placements, Jev's odds, outcome |
+
+Rules:
+
+- The loop is: generate options → place yes and veto chips → Jev pulls the lever → decision posts → accept or go again. A hand is that loop until the table accepts.
+- A hand ends on accept or expiry and never reopens. Refinement happens in rounds before accept, not by reopening.
+- Every round carries a standing "More options" card. If it wins the lever, the options agent regenerates and a new round starts.
+- A single-hand decision is a run of length 1. No separate code path.
+- A table is created implicitly by the first hand shared to a new set of people. Nobody "makes a table".
+- Two-person tables are just tables. The couple case gets no special mode; rubber-banding covers it.
+
+## Identity
+
+A phone number is the whole identity. It exists for one reason: so the ledger follows a person across hands and tables.
+
+- Join flow: open hand link → enter phone → 6-digit SMS OTP → type a display name → you're at the table. Under 30 seconds, once per device.
+- Returning device: cookie/session, no OTP. Re-verify only when the session is gone.
+- No profile, no avatar upload, no username, no password, no email. Display name is per-table and editable.
+- Invite = the hand link, pasted back into the WhatsApp group. Whoever opens it and verifies is a member of that table.
+- Table membership is inferred from who joined the first hand. Later hands at the same table reuse the member list; new people can still join via link.
+- Phone number is never shown to other members. Only the display name is.
+
+Why phone and not name-only: the group already exists as phone numbers in WhatsApp, so it costs nothing to ask, and without it "you're paying next time" can't survive a hand.
+
+## Intake and the options agent
+
+One LLM agent generates options: from a screenshot at intake, and again mid-hand whenever "More options" wins a round. It is the only text-generating model in the product; Jev takes over the moment options exist.
+
+```mermaid
+flowchart LR
+  A[Screenshot or paste] --> B[Options agent<br/>question + 2-4 options]
+  B --> C{Ambiguous?}
+  C -- yes --> D[One clarifying<br/>question]
+  D --> B
+  C -- no --> E[Confirm / edit card]
+  E --> F[Round opens]
+  M[Manual entry] --> E
+  G[More options<br/>wins a round] --> B
+```
+
+At intake the agent sees the screenshot and the table's ledger and returns a question, 2–4 options, and any constraint it spotted ("Sam said not Tuesday"). The initiator sees a single editable card and taps share. On a regen it sees the previous round instead: which options had chips, which were vetoed, what Jev's odds were. That is its brief ("more like Thai, cheaper, not Tuesday"); vetoed options never come back.
+
+Rules:
+
+- Options cap at 4 per round, plus the standing "More options" card. More than that is two hands.
+- One clarifying question max before the card. Intake never becomes a conversation.
+- Regen rounds cap at 3 per hand. After that "More options" is greyed and the table picks from what's there.
+- Screenshots are processed and discarded. Only the extracted card persists.
+- Multiple screenshots (a long argument) are accepted in one intake.
+- Target: screenshot to open round in under 20 seconds; regen under 10.
+
+## Chips
+
+Everyone gets a fresh stack each round. Yes chips say what you'd be happy with and how much; veto chips kill an option outright. Chips don't set the odds directly — Jev reads the whole table, chips included, and sets them.
+
+**Yes chips (per round)**
+
+- Each member receives 10 yes chips at the start of a round (plus any rubber-band bonus).
+- Chips go on options in any split, including the "More options" card. Stacking is intensity: 8 on Thai says more than 3.
+- Placing is live and visible to the table. You can move your chips until you lock.
+- Jev's odds are shown live under the options and shift as chips move. That is the feedback loop: you watch your vote move the needle.
+- A member who places nothing by expiry is treated as abstaining. Jev sees that too.
+- Chips do not carry over. Unspent chips vanish at the pull. There is no balance.
+
+**Veto chips (per run)**
+
+- Each member gets 2 vetoes per run (a single-hand decision is a run, so 2 per hand in the dinner case).
+- A veto deletes an option from the hand. Chips already on it return to their owners for re-placement.
+- A veto cannot remove the last two options. Minimum viable hand is 2 options.
+- Vetoes are visible and attributed. "Sam vetoed Thai" is part of the fun and the accountability.
+
+**The lever**
+
+When everyone has locked or the round's time is up, Jev pulls the lever. Its calibrated probability over the options (the one it has been showing live) is the draw weights; the server samples from it, seeded and logged so the reveal can replay it. Sampling, not top-pick: the underdog can still win, so nobody's choice "got validated by the AI", and a 30 % option landing reads as luck.
+
+**Rubber-banding**
+
+- Members whose chips were mostly on losing options receive bonus chips next round or hand at the same table: +3 if less than 25 % of your chips were on the winner, +1 if 25–50 %.
+- Bonus is capped at +5 so a streak of losses doesn't make you a dictator.
+- This is what keeps the two-person table from souring. The loser has a structural edge next time without anyone conceding, and Jev is told about it in the state so it doesn't double-count.
+
+## Jev
+
+Jev is the probability machine. It is TypeSafe AI's System One model: it takes a state plus a set of typed questions and returns calibrated probabilities for all of them in parallel, in 70–500 ms, at roughly $0.04 per million input tokens with output free ([TypeSafe announcement](https://typesafe.ai/blog/introducing-system-one-models-and-jev), [LangChain guide](https://www.langchain.com/blog/building-a-harness-with-jev)). It does not generate text and cannot hallucinate; outputs are schema-guaranteed. Three question types: **choice** (probability per option + confidence), **score** (position on an ordered scale), **noul** (probability a statement is true). Cardinality up to 255 per choice.
+
+Jev is the decider, but a semi-decision-makery one: it weighs the chips heavily, along with constraints and the ledger, produces odds, and the lever samples them. It is not a majority counter and not an oracle. Every chip placement is a state change; Jev re-reads the round and re-answers a fixed question set fast enough that the odds move as you drag.
+
+**The hand state Jev sees**
+
+Question, options (including "More options"), current yes-chip placements per member, vetoes spent, constraints from intake, rubber-band bonuses in play, previous rounds of this hand (options, chips, what was posted), time to expiry, and the table's ledger (past outcomes, streaks, house rule). No phone numbers.
+
+**Question set, asked on every state change**
+
+| Question | Type | Used for |
+| --- | --- | --- |
+| Which option should this table go with? Yes chips are votes and weigh heavily; respect constraints; be fair over the ledger. | choice over options + More options | the odds shown live, and the lever's draw weights |
+| Is this round contentious? | noul | triggers the negotiator card when > 0.7 |
+| What is the disagreement about? | choice: cost / timing / distance / people / taste / other | picks which canned clarifying prompt to show |
+| Is any option effectively dead? | noul per option | greys the option, suggests a veto |
+| Is anyone sandbagging (chips spread to avoid commitment)? | noul per member | light nudge to commit |
+| How spicy is this round? | score: mild / warm / hot / volcanic | drives the UI escalation ramp |
+| Which dealer line fits? | choice over a bank of \~200 pre-written lines | the posted-decision line and ledger line |
+
+**Why sample instead of pick**
+
+If Jev picked its top option every time, it would be a weighted vote with extra steps and "the AI sided with Sam" becomes the hard feeling. Sampling from calibrated odds keeps the ritual: the lever is a real pull, the odds were on the screen before it, and a loss is bad luck. Jev's influence is in the odds, which everyone can see and argue with by moving chips.
+
+**Rules**
+
+- Jev decides by odds, not by top-pick. The lever always samples.
+- Jev never writes text. Every string a player sees from Jev is from a human-authored bank, selected by a choice question. Australian-English, irreverent, warm, never smug.
+- One Jev call per state change, all questions in parallel. Debounce chip drags to 200 ms. Budget: a busy hand is a few hundred calls; at Jev's pricing that is well under a cent.
+- Option generation stays on the LLM agent. Jev has no image input and no string output.
+- Jev's confidence is surfaced, not hidden. Low confidence on the odds shows as a wobbling bar, not a false number.
+
+## Ledger
+
+The ledger is the only thing that persists and the only thing that makes losing fun. It records outcomes and social debt, never chip balances.
+
+Per table, per resolved hand:
+
+| Field | Example |
+| --- | --- |
+| Question | Friday dinner |
+| Rounds | 2 (More options won round 1) |
+| Winner (option) | Thai |
+| Who backed it | Sam 8/10, Tom 2/10 |
+| Vetoes spent | Tom vetoed pizza |
+| Jev's odds at the pull | 62 % Thai |
+| Debt line | Tom pays; Sam picks next |
+| Dealer line | "Thai. Again. Tom, get your wallet." |
+
+Derived views the table sees:
+
+- Streaks: "Sam has won 4 Friday dinners in a row."
+- Pays-next / picks-next: set by a house rule chosen when the table is created (loser pays, winner picks, or none).
+- Most-vetoed option: the running joke.
+- Run summary: for a holiday run, the resolved hands in order as the itinerary.
+
+Not in the ledger: chip counts across hands, any score, any rank. Nothing accrues that could look like a currency.
+
+## UI escalation
+
+Calm to hectic is the emotional arc. The Balatro treatment is a ramp driven by Jev's "how spicy is this hand" score, not a theme applied from screen one. A unanimous hand never leaves the poll look; a contested one climbs to full CRT.
+
+| Stage | Look | Motion | Jev avatar |
+| --- | --- | --- | --- |
+| Intake / confirm | plain iMessage-style poll card, system font, flat | none | text bubble, no face |
+| Chips placing | felt table, chip stacks, Jev's odds bar live | chips slide, odds bar eases | low-res sprite, 16 px |
+| Lock / countdown | felt darkens, options become cards, lever arm appears | cards tilt, CRT scanlines fade in | sprite animates, 32 px |
+| Pull (reveal) | full CRT, screen shake, score-tick sound | lever cranks, reels spin on Jev's odds, winner card flips, confetti | full dealer sprite, speaks the line |
+| Posted | decision card, accept / go again / more options | none | dealer sprite idles |
+| Ledger | back to flat, receipt-style | none | text bubble |
+
+Reveal requirements:
+
+- Capture-safe: the final frame fits a 9:16 phone screenshot with question, winner, Jev's odds, dealer line and table name all visible. No overlays, no UI chrome in that frame.
+- Replayable: any member can re-watch the pull from the ledger. Same seed, same reels.
+- Under 6 seconds from pull to final frame. Skippable after 2.
+- Sound is on by default with a visible mute. Haptics on the lever crank and the winner flip.
+- Jev's odds are shown before the pull, so a 70 % favourite losing reads as bad luck, not a rigged app.
+
+## Engagement design: dopamine, serotonin, cortisol
+
+The lever is a slot machine with no money in it, so design it like one: variable reward, near-misses, named winners, a countdown, scarce vetoes, and a house (Jev) that everyone can blame. The one rule that keeps it a game rather than a fight is that every spike resolves inside the hand.
+
+**Dopamine: anticipation and variable reward**
+
+| Mechanic | Why it works | Build note |
+| --- | --- | --- |
+| The pull is a variable-ratio reward | Outcome uncertain, odds visible, action is yours (you crank the lever) | The crank is a drag gesture, not a tap. Effort before reward. |
+| Stretch the moment | Anticipation beats outcome for dopamine; slots spend 3–4 s on reels for a 200 ms decision | Reels spin on Jev's odds. 2 s minimum before the first reel stops, even if the draw is already known. |
+| Near-miss | Losing by a hair drives replay more than losing outright | Reels always pass through the runner-up (by chips) last before landing. If your option was runner-up, your card visibly almost lands. |
+| Upsets | A 20 % option landing is a story; a 70 % landing is Tuesday | Jev samples, so upsets happen naturally. When the winner was under 30 %, escalate: "UPSET" banner, double confetti, a ledger badge. |
+| Called it | Backing the winner heavily against the odds feels like skill | Ledger line: "Jem called it at 25 %". Titles for repeat callers. |
+| Live odds move as you drag | Action → visible consequence at 100 ms is the core slot loop | Jev call per drag, debounced 200 ms. The needle must move on every placement or the loop breaks. |
+| Ding ding ding | Sound anchors the reward | Escalating tick as reels stop, a bell on the winner, a groan on a veto. Mute visible, default on. |
+
+**Serotonin: status and being seen**
+
+| Mechanic | Why it works | Build note |
+| --- | --- | --- |
+| Winner is a person, not an option | "We're going with Jem's option" attributes the win | Whoever had the most chips on the winner gets their name on the decision card. Ties → earliest placement. |
+| Titles | Public, earned, in-group status | Pattern-derived by Jev over the ledger: Table Oracle (calls it), Chaos Agent (backs long shots), Serial Vetoer, The House Always Wins (Jev's favourite). Shown next to the name at the table. |
+| Streaks | Visible dominance is status; visible losing streaks trigger the rubber-band so nobody bottoms out | "Sam: 4 Friday dinners in a row" on the ledger. |
+| Being seen by Jev | Jev naming what you care about is validation | The negotiator card: "Jev reckons Tom's no is about money, not Thai." Human-authored line, Jev-selected. |
+| In-group language | Ritual lines become the table's slang | One fixed ritual line on every pull ("the lever has spoken") plus a rotating dealer line. The fixed line is what gets quoted back in WhatsApp. |
+| The screenshot | Social proof and bragging rights outside the app | Capture-safe final frame with the winner's name large. This is the acquisition loop. |
+
+**Cortisol: stakes and urgency**
+
+| Mechanic | Why it works | Build note |
+| --- | --- | --- |
+| Round countdown | Time pressure raises arousal; last-to-lock is social pressure | "Everyone's waiting on Tom" card, in-room only. No push. |
+| Vetoes are scarce, public, attributed | Spending one is a costly signal; watching someone reach for one is tension | Veto has a hold-to-confirm and a distinct sound. The table sees the hover. |
+| Chips vanish at the pull | Loss aversion: unspent is wasted, so place them | Unplaced chips fly off the table at the pull. |
+| Social debt | A consequence outside the app is real stakes | Loser pays / winner picks, written on the ledger and the screenshot. |
+| Comeback | Hope keeps the loser in | Rubber-band bonus is announced at the start of the next round: "Tom's got +3 this round." |
+| Spice ramp | The app visibly gets more intense as the table does; feedback amplifies | Jev's spice score drives the CRT ramp. Volcanic rounds shake. |
+
+**Every spike resolves in-hand.** Countdown → pull → named winner → dealer line → debt line → accept. Nothing is left hanging when the hand closes. A couple app that manufactures cortisol and doesn't discharge it is a churn machine and a breakup tool; the discharge is the product, not the spike.
+
+**Interpersonal dynamics**
+
+- Coalitions: chips are visible, so piling onto someone's option is an alliance and splitting is a hedge. Jev's sandbagging noul exposes fence-sitters with a nudge, not a callout.
+- Jev is the lightning rod. The lever decided, not Sam, so the loser's resentment goes to the machine. Give Jev enough personality to be ribbed collectively ("Jev's a Thai shill"). A shared antagonist bonds the table.
+- Reciprocity is mechanical: rubber-band, winner-picks-next, and Jev instructed to be fair over the ledger. Nobody has to concede; the system does it for them.
+- Vetoes are statements. "Tom vetoed pizza" is on the record and becomes table lore. Expect vetoes to be spent on people as much as options; that's fine, it's costly.
+- Two-person tables run hot because every hand is zero-sum. Three levers cool it: winner attributed to Jev's odds not the partner, rubber-band, and the negotiator card turning "no" into a reason.
+- Absence has a cost. A hand you skipped still resolves and the ledger says you abstained. No penalty beyond that; no streak-loss anxiety for being away.
+
+**Jev's natural form, used on purpose**
+
+- Calibrated odds are the drama. A 50/50 pull is a coin flip; a 90/10 upset is a legend. Show the number, always.
+- Confidence is tension. Low confidence wobbles the odds bar; high confidence locks it. "Jev's sure" and "Jev's torn" are two of the dealer states.
+- Parallel questions at 100 ms make Jev feel like it's watching. It reacts to every chip. That reactivity is the personality; there is no text.
+- Nouls are narrative triggers. Contentious, sandbagging, dead option, and per-member "who is this decision for" each fire a human-written callout. Cheap enough to run per member, per drag.
+- Score is the mood ring. Spice drives the UI; "how close is this" drives the reel duration.
+- **Jev picks the format.** One more choice question per round: "which game fits this state?" over a small bank of mechanics. Two options and low spice → a Tinder-style swipe, yes or no. Four options and split chips → the full craps table. Near-unanimous → a one-tap confirm with a tiny pull. Contentious and volcanic → reels with the long near-miss. The table never knows which game they're getting until the round opens; that unpredictability is itself the Reels feed. Each format ends in the same lever pull so the ritual is constant while the game varies.
+- No hallucination means Jev can be handed sharp lines safely. It picks the sting from a bank we wrote; it can never invent one. Cardinality 255 means the bank can be big and tagged.
+- Cost is a non-issue. A volcanic hand with 500 state changes is under a cent. Spend Jev calls freely on feel.
+
+**Where it stops being fun**
+
+- No mechanic may persist a negative state past the hand: no losing-streak badges, no "you owe 3 dinners" running tally beyond the last hand's debt line.
+- No urgency that reaches outside the room. No push, no SMS beyond OTP.
+- Nothing purchasable, ever. The moment chips can be bought the chemistry above becomes the thing regulators mean by gambling.
+
+## Hand state machine
+
+A hand loops through rounds and has two terminal states. Nothing reopens after accept or expiry.
+
+```mermaid
+stateDiagram-v2
+  [*] --> Draft: intake
+  Draft --> Open: initiator shares
+  Open --> Open: chips placed / vetoed
+  Open --> Pulled: all lock or round timer
+  Pulled --> Posted: Jev's odds sampled
+  Posted --> Open: refine, or More options<br/>won and agent regenerated
+  Posted --> Accepted: table accepts<br/>or quiet window passes
+  Open --> Expired: hand expiry, no chips
+  Accepted --> [*]
+  Expired --> [*]
+```
+
+| State | Who can act | Timer |
+| --- | --- | --- |
+| Draft | initiator only | 1 h then discarded |
+| Open | all members: place, move, veto, lock | round timer, default 2 h; hand expiry default 24 h, settable 1 h–7 d |
+| Pulled | nobody; draw runs within seconds | n/a |
+| Posted | all members: accept, or go again | quiet window 1 h → auto-accept |
+| Accepted | nobody; ledger written, reveal replayable | n/a |
+| Expired | nobody; default applies | n/a |
+
+Posted → Open is the GOTO. Any member can tap "go again" to start a new round with the same options (chips reset, vetoed options gone) or "more options" to send the agent back in. Accept needs a majority of members, or 1 h of silence. Expiry: if any chips were placed this round, expiry pulls the lever and the posted decision auto-accepts. If no chips were placed at all, the hand expires with the initiator's first-listed option and the ledger notes "decided by default". Reminders are in-room only (a countdown), no push.
+
+## Guardrails
+
+These are the lines that don't move. Each one exists because crossing it turns the product into something else (a chat app, a casino, a native app).
+
+- No purchasable currency, no cash-out, no chip balance that persists. Chips reset every hand. This is what keeps it clear of AU interactive gambling rules; the line is value changing hands, not the presence of odds.
+- No messaging infrastructure. No DMs, no threads, no message history, no notification backend. Reactions on options are the only in-hand expression.
+- No accounts beyond a phone number. No profiles, no friend graph, no discovery.
+- Mobile web only. No native app, no app store, no push. Share sheet and browser are the distribution.
+- A hand ends on accept or expiry and never reopens. Rounds are how you refine; reopening is not. Ephemerality is the product.
+- Jev decides by sampling its own calibrated odds, never by taking the top option outright, and never writes text. Every line it "says" is human-authored.
+
+## v1 scope
+
+**In**
+
+- Phone + OTP join, per-table display name
+- Options agent: screenshot intake, manual entry fallback, regen on "More options" (cap 3 per hand)
+- Rounds: 2–4 options + More options card, 10 yes chips, 2 veto chips, Jev's odds live
+- The lever: server-side sample of Jev's odds, seeded and replayable
+- Posted → accept / go again loop, majority accept or 1 h quiet window
+- Full pull animation, capture-safe final frame
+- Ledger: outcomes, rounds, debt line, streaks, Jev's odds at the pull, house rules (loser pays / winner picks)
+- Rubber-banding
+- Hand expiry with default
+- Runs (ordered hands under one goal) — thin: just a name and a list
+- Jev negotiator card: contention-triggered, canned clarifying prompt chosen by Jev, no free text
+- Spice-driven UI ramp and Jev-selected dealer lines from a \~200-line bank
+
+**Out**
+
+- Any free text shown as Jev; the options agent's output is only ever option cards
+- Any payment, premium tier, or purchase
+- Native app, push notifications
+- Reactions or emoji on options
+- Table admin (removing members, renaming)
+- Anything that reads like a chat
+
+**Stretch (only after an unsolicited share is observed)**
+
+- Jev question-set expansion: per-member "what does Tom actually want" reads, run-level predictions
+- Sound design beyond the reveal
+- Run templates ("plan a trip" pre-seeds where / when / stay hands)
+- Table-level house rules beyond the three
+
+## Validation
+
+Success is one unsolicited share: someone forwards a hand link or a reveal screenshot to a chat the initiator isn't in. If that doesn't happen in 4 weeks, the wedge is wrong, not the UI.
+
+- Seed two tables: one couple (hard-feelings test) and one friend group of 4–6 (re-share test). Run them in parallel for 4 weeks.
+- Watch: hands per table per week, time from intake to lock, reveal replays, screenshots of the reveal (detect via share-sheet events), new phone numbers joining a table that weren't invited by the initiator.
+- The couple table validates rubber-banding and tone. The group table is the only one that can produce the success metric.
+- Don't ask for feedback in week 1. Read the ledger and the join graph instead.
+- Kill criteria: fewer than 2 hands per table per week by week 3, or zero reveal screenshots by week 4.
+
+## Open questions
+
+- [ ] Name. "Plan Maker 2000" is a working title; the hand link domain is the brand people see in WhatsApp.
+- [ ] Chip numbers. 10 yes / 2 veto / +3 rubber-band are first guesses. Tune on the seed tables.
+- [ ] Lever sampling: straight sample from Jev's odds, or sharpened (temperature < 1) so a strong majority usually wins? Straight is more honest; sharpened is less annoying.
+- [ ] How heavily should the choice question's instructions tell Jev to weigh chips vs ledger fairness? This is the whole "semi-decision-makery" dial and needs playtesting.
+- [ ] Format bank for v1: swipe, craps table, one-tap, reels. Do all four ship, or two with the others behind the Jev choice question later?
+- [ ] Accept mechanics: majority tap, any single tap, or quiet window only?
+- [ ] Jev early access: confirm we can get an API key and what the rate limits are before building the live loop on it.
+- [ ] Dealer line bank: who writes 200 lines, and how do we tag them so Jev's choice question has enough signal?
+- [ ] Does a run need its own screen, or is it just the ledger filtered by goal?
+- [ ] Veto scarcity in a long run: 2 vetoes across 8 hands may be too tight. Consider +1 veto every 3 hands.
+- [ ] Seeded draw: publish the seed post-pull so anyone can verify it wasn't rigged?
+- [ ] Screenshot intake and privacy: the image contains other people's messages. State the discard policy in-app.
+
+## Jev findings (live API, 19 Sep)
+
+Tested against `POST https://api.typesafe.ai/v1/systemone` with `jev-latest`. Six hand states through the full question set, then targeted probes. All of this shapes the build.
+
+| Finding | Evidence | What it means for the build |
+| --- | --- | --- |
+| Latency is fine for a drag loop, not for 100 ms | 6 questions in one call: 320–420 ms. Single question: \~200 ms. | Debounce chip drags at 200 ms; expect the odds bar to move \~400 ms after the finger. Show it easing, not snapping. |
+| Chips dominate the odds by default | Tom 9 Thai / Sam 10 pizza → 45 / 43. Sam 8 concentrated vs Tom spread 3/3/2/2 → pizza 93 %. | Stacking is intensity, as specced. Spread chips read as weak. |
+| The fairness dial is the instruction text alone | Same chips (Sam 10 pizza, Tom 6 Thai / 4 ramen, Sam won last 4): no fairness clause → pizza 99 %. "Be fair across the ledger" → thai 54 %, pizza 18 %. "Lean toward the others" → thai 75 %, pizza 5 %. | `PICK_INSTRUCTIONS` is a config var, hot-reloadable. This is the whole "semi-decision-makery" tuning surface; no code changes needed to playtest it. |
+| "More options" works as a votable card | Both members put most chips on it → 94 %. | Regen trigger is reliable. |
+| Classification questions want derived features, not raw chips | Format question over raw chip state picked `craps` for everything, never `one_tap` or `reels`. With `chip_agreement`, `live_option_count` and `spice` pre-computed into the state and named in the criteria → one\_tap 97 %, swipe 99 %, craps 100 %, reels 72 %. | The DO computes features before every Jev call. Criteria reference state fields by name. |
+| Spice reads low from chips alone | Contentious 0.76 but spice 0.27 on the same round. | Spice needs signals beyond chip position: chip moves per minute, vetoes, re-placements. Or drive the ramp from contentious + format instead. |
+| Sandbagging noul is noisy | 0.6 for a member who put 7 chips on More options. | Nudge only, never a callout. Threshold ≥ 0.7. |
+| Large choice banks work | 40 dealer tags: 301 ms, correct pick. Duplicate tags split probability. | Bank can be big; tags must be unique and descriptive. |
+| Cost is nothing | \~750 in / \~215 out tokens per full call. | Hundreds of calls per hand is well under a cent. |
+| Straight sampling punishes unanimity | Local run: both members 10 chips on Thai → Jev 78 % Thai (history in state pulled it down from 99 %), sample landed Ramen at 6 %. | Design change: when format is `one_tap` the lever does not sample; it confirms. Sampling is for craps and reels. Sharpening (temperature < 1) still open for craps. |
+
+## Dev setup
+
+What exists after 19 Sep: a running loop prototype, driven end to end locally. this repo.
+
+Stack as built:
+
+- Hono on Cloudflare Workers, generated from `@better-auth-cloudflare/cli` (Hono template, D1, no KV/R2).
+- Better Auth 1.7 with the `phoneNumber` plugin (6-digit OTP, 5 min expiry, sign-up on verify) and `anonymous` as the zero-friction path. OTP prints to the wrangler console in dev; `OTP_MODE=twilio` sends SMS.
+- One Durable Object per hand (`src/hand.ts`): state, WebSocket fan-out via the hibernation API, chips/veto/lock, Jev odds recomputed on every state change with a 200 ms debounce, the lever as a seeded weighted sample with the runner-up placed last in the reel order, rounds (again / more / accept with majority).
+- Typed Jev client (`src/jev.ts`) with 429/529 backoff. Dealer bank (`src/dealer.ts`), 22 lines, Jev picks by tag.
+- Mobile web client in one HTML file: felt table, chip stacks, live odds bars that wobble under 50 % confidence, lever, CRT reels reveal with the near-miss hang and an UPSET banner under 30 %, posted → accept / go again / more options.
+- `scripts/drive.sh` runs a full two-member hand against a dev server for smoke testing.
+
+Dev loop:
+
+1. `npm install && cp .dev.vars.example .dev.vars`, paste keys.
+2. `npm run db:migrate:dev && npm run dev` — local D1 + DO in miniflare, hot reload, OTP codes in the console.
+3. Two phones on the LAN: `wrangler dev --ip 0.0.0.0`, open `/`, jump in, create a hand, share `/h/<id>`.
+4. Tune the pick instruction in `wrangler.toml` `[vars].PICK_INSTRUCTIONS`; it hot-reloads.
+5. Preview deploys: `npx wrangler versions upload` gives a URL without touching prod. First real deploy: `wrangler d1 create pm2000-db`, paste the id, `wrangler secret put TYPESAFE_API_KEY`, `wrangler secret put BETTER_AUTH_SECRET`, `npm run deploy`.
+
+Still to support the dev loop:
+
+- Options agent: one Worker route with a vision model for screenshot intake and "More options" regen. The DO already accepts new labels via the `more` op.
+- Expiry: DO alarms for round timer and hand expiry (currently manual pull only).
+- A Jev eval harness: `jev_test.py` / `jev_probe2.py` are the seed. Turn them into a fixture set of hand states with expected odds ranges so instruction changes can be regression-tested.
+- Sound and haptics beyond the one vibrate pattern.
+- Move `TYPESAFE_API_KEY` to `wrangler secret` before the first deploy and roll it.
+
+Temp Jev key lives in `.dev.vars` (gitignored) and in the Claude Doc, not in this repo. Roll it before first deploy.
+
+## Next steps
+
+Order matters: the whole product rests on the drag → odds → pull loop feeling good, and that loop rests on Jev. Prove those two before touching auth, tables or ledger.
+
+1. ~~Get Jev.~~ Done: key in hand, API probed, client written.
+2. ~~Loop prototype.~~ Built and driven locally. Next: put it on two phones and answer the only question — does dragging a chip and watching the odds move feel like anything?
+3. Write the choice question and 30 dealer lines. 22 lines exist; the pick instruction is a config var. Iterate both on the prototype.
+4. Reveal bake-off. Reels with near-miss exists. Add dice tumble and card flip variants in the same client; pick by what gets screenshotted.
+5. Format bank. The format question works with derived features; the client currently renders one layout. Add swipe and one-tap views; one-tap confirms without sampling.
+6. Options agent. Screenshot intake, then More options regen. One LLM route.
+7. Tables, ledger, rubber-banding, expiry alarms, runs.
+8. Seed two tables (couple + friend group of 4–6), run four weeks, watch the join graph and reveal screenshots. Kill criteria as in Validation.
+9. Name and domain, before step 8 goes out, because the link is the brand.
+
+Stack, given the shape: Cloudflare Workers + Durable Objects (one DO per hand holds the state and fans out odds over WebSocket), D1 or KV for tables and ledger, Pages for the mobile web client. The Jev call sits inside the DO so every state change is one round-trip.

+ 41 - 0
drizzle.config.ts

@@ -0,0 +1,41 @@
+import { defineConfig } from "drizzle-kit";
+import fs from "node:fs";
+import path from "node:path";
+
+function getLocalD1DB() {
+    try {
+        const basePath = path.resolve(".wrangler");
+        const dbFile = fs
+            .readdirSync(basePath, { encoding: "utf-8", recursive: true })
+            .find(f => f.endsWith(".sqlite"));
+
+        if (!dbFile) {
+            throw new Error(`.sqlite file not found in ${basePath}`);
+        }
+
+        const url = path.resolve(basePath, dbFile);
+        return url;
+    } catch (err) {
+        console.log(`Error  ${err}`);
+    }
+}
+
+export default defineConfig({
+    dialect: "sqlite",
+    schema: "./src/db/index.ts",
+    out: "./drizzle",
+    ...(process.env.NODE_ENV === "production"
+        ? {
+              driver: "d1-http",
+              dbCredentials: {
+                  accountId: process.env.CLOUDFLARE_D1_ACCOUNT_ID,
+                  databaseId: process.env.CLOUDFLARE_DATABASE_ID,
+                  token: process.env.CLOUDFLARE_D1_API_TOKEN,
+              },
+          }
+        : {
+              dbCredentials: {
+                  url: getLocalD1DB(),
+              },
+          }),
+});

+ 58 - 0
drizzle/0000_groovy_manta.sql

@@ -0,0 +1,58 @@
+CREATE TABLE `accounts` (
+	`id` text PRIMARY KEY NOT NULL,
+	`account_id` text NOT NULL,
+	`provider_id` text NOT NULL,
+	`user_id` text NOT NULL,
+	`access_token` text,
+	`refresh_token` text,
+	`id_token` text,
+	`access_token_expires_at` integer,
+	`refresh_token_expires_at` integer,
+	`scope` text,
+	`password` text,
+	`created_at` integer NOT NULL,
+	`updated_at` integer NOT NULL,
+	FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE TABLE `sessions` (
+	`id` text PRIMARY KEY NOT NULL,
+	`expires_at` integer NOT NULL,
+	`token` text NOT NULL,
+	`created_at` integer NOT NULL,
+	`updated_at` integer NOT NULL,
+	`ip_address` text,
+	`user_agent` text,
+	`user_id` text NOT NULL,
+	`timezone` text,
+	`city` text,
+	`country` text,
+	`region` text,
+	`region_code` text,
+	`colo` text,
+	`latitude` text,
+	`longitude` text,
+	FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint
+CREATE TABLE `users` (
+	`id` text PRIMARY KEY NOT NULL,
+	`name` text NOT NULL,
+	`email` text NOT NULL,
+	`email_verified` integer NOT NULL,
+	`image` text,
+	`created_at` integer NOT NULL,
+	`updated_at` integer NOT NULL,
+	`is_anonymous` integer
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
+CREATE TABLE `verifications` (
+	`id` text PRIMARY KEY NOT NULL,
+	`identifier` text NOT NULL,
+	`value` text NOT NULL,
+	`expires_at` integer NOT NULL,
+	`created_at` integer,
+	`updated_at` integer
+);

+ 76 - 0
drizzle/0001_furry_unicorn.sql

@@ -0,0 +1,76 @@
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_accounts` (
+	`id` text PRIMARY KEY NOT NULL,
+	`account_id` text NOT NULL,
+	`provider_id` text NOT NULL,
+	`user_id` text NOT NULL,
+	`access_token` text,
+	`refresh_token` text,
+	`id_token` text,
+	`access_token_expires_at` integer,
+	`refresh_token_expires_at` integer,
+	`scope` text,
+	`password` text,
+	`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
+	`updated_at` integer NOT NULL,
+	FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+INSERT INTO `__new_accounts`("id", "account_id", "provider_id", "user_id", "access_token", "refresh_token", "id_token", "access_token_expires_at", "refresh_token_expires_at", "scope", "password", "created_at", "updated_at") SELECT "id", "account_id", "provider_id", "user_id", "access_token", "refresh_token", "id_token", "access_token_expires_at", "refresh_token_expires_at", "scope", "password", "created_at", "updated_at" FROM `accounts`;--> statement-breakpoint
+DROP TABLE `accounts`;--> statement-breakpoint
+ALTER TABLE `__new_accounts` RENAME TO `accounts`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;--> statement-breakpoint
+CREATE INDEX `accounts_userId_idx` ON `accounts` (`user_id`);--> statement-breakpoint
+CREATE TABLE `__new_sessions` (
+	`id` text PRIMARY KEY NOT NULL,
+	`expires_at` integer NOT NULL,
+	`token` text NOT NULL,
+	`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
+	`updated_at` integer NOT NULL,
+	`ip_address` text,
+	`user_agent` text,
+	`user_id` text NOT NULL,
+	`timezone` text,
+	`city` text,
+	`country` text,
+	`region` text,
+	`region_code` text,
+	`colo` text,
+	`latitude` text,
+	`longitude` text,
+	FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+INSERT INTO `__new_sessions`("id", "expires_at", "token", "created_at", "updated_at", "ip_address", "user_agent", "user_id", "timezone", "city", "country", "region", "region_code", "colo", "latitude", "longitude") SELECT "id", "expires_at", "token", "created_at", "updated_at", "ip_address", "user_agent", "user_id", "timezone", "city", "country", "region", "region_code", "colo", "latitude", "longitude" FROM `sessions`;--> statement-breakpoint
+DROP TABLE `sessions`;--> statement-breakpoint
+ALTER TABLE `__new_sessions` RENAME TO `sessions`;--> statement-breakpoint
+CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint
+CREATE INDEX `sessions_userId_idx` ON `sessions` (`user_id`);--> statement-breakpoint
+CREATE TABLE `__new_users` (
+	`id` text PRIMARY KEY NOT NULL,
+	`name` text NOT NULL,
+	`email` text NOT NULL,
+	`email_verified` integer DEFAULT false NOT NULL,
+	`image` text,
+	`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
+	`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
+	`is_anonymous` integer DEFAULT false
+);
+--> statement-breakpoint
+INSERT INTO `__new_users`("id", "name", "email", "email_verified", "image", "created_at", "updated_at", "is_anonymous") SELECT "id", "name", "email", "email_verified", "image", "created_at", "updated_at", "is_anonymous" FROM `users`;--> statement-breakpoint
+DROP TABLE `users`;--> statement-breakpoint
+ALTER TABLE `__new_users` RENAME TO `users`;--> statement-breakpoint
+CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
+CREATE TABLE `__new_verifications` (
+	`id` text PRIMARY KEY NOT NULL,
+	`identifier` text NOT NULL,
+	`value` text NOT NULL,
+	`expires_at` integer NOT NULL,
+	`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
+	`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
+);
+--> statement-breakpoint
+INSERT INTO `__new_verifications`("id", "identifier", "value", "expires_at", "created_at", "updated_at") SELECT "id", "identifier", "value", "expires_at", "created_at", "updated_at" FROM `verifications`;--> statement-breakpoint
+DROP TABLE `verifications`;--> statement-breakpoint
+ALTER TABLE `__new_verifications` RENAME TO `verifications`;--> statement-breakpoint
+CREATE INDEX `verifications_identifier_idx` ON `verifications` (`identifier`);

+ 8 - 0
drizzle/0002_add_rate_limits.sql

@@ -0,0 +1,8 @@
+CREATE TABLE `rate_limits` (
+	`id` text PRIMARY KEY NOT NULL,
+	`key` text NOT NULL,
+	`count` integer NOT NULL,
+	`last_request` integer NOT NULL
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `rate_limits_key_unique` ON `rate_limits` (`key`);

+ 1 - 0
drizzle/0003_violet_vampiro.sql

@@ -0,0 +1 @@
+DROP TABLE `rate_limits`;

+ 11 - 0
drizzle/0004_quiet_lethal_legion.sql

@@ -0,0 +1,11 @@
+ALTER TABLE `users` ADD `phone_number` text;--> statement-breakpoint
+ALTER TABLE `users` ADD `phone_number_verified` integer;--> statement-breakpoint
+CREATE UNIQUE INDEX `users_phone_number_unique` ON `users` (`phone_number`);--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `timezone`;--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `city`;--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `country`;--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `region`;--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `region_code`;--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `colo`;--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `latitude`;--> statement-breakpoint
+ALTER TABLE `sessions` DROP COLUMN `longitude`;

+ 391 - 0
drizzle/meta/0000_snapshot.json

@@ -0,0 +1,391 @@
+{
+    "version": "6",
+    "dialect": "sqlite",
+    "id": "2705b294-dcb2-44a3-8c64-3e49c76c10a6",
+    "prevId": "00000000-0000-0000-0000-000000000000",
+    "tables": {
+        "accounts": {
+            "name": "accounts",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "account_id": {
+                    "name": "account_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "provider_id": {
+                    "name": "provider_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "user_id": {
+                    "name": "user_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "access_token": {
+                    "name": "access_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "refresh_token": {
+                    "name": "refresh_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "id_token": {
+                    "name": "id_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "access_token_expires_at": {
+                    "name": "access_token_expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "refresh_token_expires_at": {
+                    "name": "refresh_token_expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "scope": {
+                    "name": "scope",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "password": {
+                    "name": "password",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {},
+            "foreignKeys": {
+                "accounts_user_id_users_id_fk": {
+                    "name": "accounts_user_id_users_id_fk",
+                    "tableFrom": "accounts",
+                    "tableTo": "users",
+                    "columnsFrom": ["user_id"],
+                    "columnsTo": ["id"],
+                    "onDelete": "cascade",
+                    "onUpdate": "no action"
+                }
+            },
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "sessions": {
+            "name": "sessions",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "expires_at": {
+                    "name": "expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "token": {
+                    "name": "token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "ip_address": {
+                    "name": "ip_address",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "user_agent": {
+                    "name": "user_agent",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "user_id": {
+                    "name": "user_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "timezone": {
+                    "name": "timezone",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "city": {
+                    "name": "city",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "country": {
+                    "name": "country",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "region": {
+                    "name": "region",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "region_code": {
+                    "name": "region_code",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "colo": {
+                    "name": "colo",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "latitude": {
+                    "name": "latitude",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "longitude": {
+                    "name": "longitude",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {
+                "sessions_token_unique": {
+                    "name": "sessions_token_unique",
+                    "columns": ["token"],
+                    "isUnique": true
+                }
+            },
+            "foreignKeys": {
+                "sessions_user_id_users_id_fk": {
+                    "name": "sessions_user_id_users_id_fk",
+                    "tableFrom": "sessions",
+                    "tableTo": "users",
+                    "columnsFrom": ["user_id"],
+                    "columnsTo": ["id"],
+                    "onDelete": "cascade",
+                    "onUpdate": "no action"
+                }
+            },
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "users": {
+            "name": "users",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "name": {
+                    "name": "name",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "email": {
+                    "name": "email",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "email_verified": {
+                    "name": "email_verified",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "image": {
+                    "name": "image",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "is_anonymous": {
+                    "name": "is_anonymous",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {
+                "users_email_unique": {
+                    "name": "users_email_unique",
+                    "columns": ["email"],
+                    "isUnique": true
+                }
+            },
+            "foreignKeys": {},
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "verifications": {
+            "name": "verifications",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "identifier": {
+                    "name": "identifier",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "value": {
+                    "name": "value",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "expires_at": {
+                    "name": "expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {},
+            "foreignKeys": {},
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        }
+    },
+    "views": {},
+    "enums": {},
+    "_meta": {
+        "schemas": {},
+        "tables": {},
+        "columns": {}
+    },
+    "internal": {
+        "indexes": {}
+    }
+}

+ 416 - 0
drizzle/meta/0001_snapshot.json

@@ -0,0 +1,416 @@
+{
+    "version": "6",
+    "dialect": "sqlite",
+    "id": "a1eda014-4873-4fc6-9273-3f804eae195e",
+    "prevId": "2705b294-dcb2-44a3-8c64-3e49c76c10a6",
+    "tables": {
+        "accounts": {
+            "name": "accounts",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "account_id": {
+                    "name": "account_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "provider_id": {
+                    "name": "provider_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "user_id": {
+                    "name": "user_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "access_token": {
+                    "name": "access_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "refresh_token": {
+                    "name": "refresh_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "id_token": {
+                    "name": "id_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "access_token_expires_at": {
+                    "name": "access_token_expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "refresh_token_expires_at": {
+                    "name": "refresh_token_expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "scope": {
+                    "name": "scope",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "password": {
+                    "name": "password",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {
+                "accounts_userId_idx": {
+                    "name": "accounts_userId_idx",
+                    "columns": ["user_id"],
+                    "isUnique": false
+                }
+            },
+            "foreignKeys": {
+                "accounts_user_id_users_id_fk": {
+                    "name": "accounts_user_id_users_id_fk",
+                    "tableFrom": "accounts",
+                    "tableTo": "users",
+                    "columnsFrom": ["user_id"],
+                    "columnsTo": ["id"],
+                    "onDelete": "cascade",
+                    "onUpdate": "no action"
+                }
+            },
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "sessions": {
+            "name": "sessions",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "expires_at": {
+                    "name": "expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "token": {
+                    "name": "token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "ip_address": {
+                    "name": "ip_address",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "user_agent": {
+                    "name": "user_agent",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "user_id": {
+                    "name": "user_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "timezone": {
+                    "name": "timezone",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "city": {
+                    "name": "city",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "country": {
+                    "name": "country",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "region": {
+                    "name": "region",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "region_code": {
+                    "name": "region_code",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "colo": {
+                    "name": "colo",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "latitude": {
+                    "name": "latitude",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "longitude": {
+                    "name": "longitude",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {
+                "sessions_token_unique": {
+                    "name": "sessions_token_unique",
+                    "columns": ["token"],
+                    "isUnique": true
+                },
+                "sessions_userId_idx": {
+                    "name": "sessions_userId_idx",
+                    "columns": ["user_id"],
+                    "isUnique": false
+                }
+            },
+            "foreignKeys": {
+                "sessions_user_id_users_id_fk": {
+                    "name": "sessions_user_id_users_id_fk",
+                    "tableFrom": "sessions",
+                    "tableTo": "users",
+                    "columnsFrom": ["user_id"],
+                    "columnsTo": ["id"],
+                    "onDelete": "cascade",
+                    "onUpdate": "no action"
+                }
+            },
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "users": {
+            "name": "users",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "name": {
+                    "name": "name",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "email": {
+                    "name": "email",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "email_verified": {
+                    "name": "email_verified",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": false
+                },
+                "image": {
+                    "name": "image",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "is_anonymous": {
+                    "name": "is_anonymous",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false,
+                    "default": false
+                }
+            },
+            "indexes": {
+                "users_email_unique": {
+                    "name": "users_email_unique",
+                    "columns": ["email"],
+                    "isUnique": true
+                }
+            },
+            "foreignKeys": {},
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "verifications": {
+            "name": "verifications",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "identifier": {
+                    "name": "identifier",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "value": {
+                    "name": "value",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "expires_at": {
+                    "name": "expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                }
+            },
+            "indexes": {
+                "verifications_identifier_idx": {
+                    "name": "verifications_identifier_idx",
+                    "columns": ["identifier"],
+                    "isUnique": false
+                }
+            },
+            "foreignKeys": {},
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        }
+    },
+    "views": {},
+    "enums": {},
+    "_meta": {
+        "schemas": {},
+        "tables": {},
+        "columns": {}
+    },
+    "internal": {
+        "indexes": {}
+    }
+}

+ 460 - 0
drizzle/meta/0002_snapshot.json

@@ -0,0 +1,460 @@
+{
+    "version": "6",
+    "dialect": "sqlite",
+    "id": "0fc9fede-d457-420e-8796-05cb0170fc40",
+    "prevId": "a1eda014-4873-4fc6-9273-3f804eae195e",
+    "tables": {
+        "accounts": {
+            "name": "accounts",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "account_id": {
+                    "name": "account_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "provider_id": {
+                    "name": "provider_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "user_id": {
+                    "name": "user_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "access_token": {
+                    "name": "access_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "refresh_token": {
+                    "name": "refresh_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "id_token": {
+                    "name": "id_token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "access_token_expires_at": {
+                    "name": "access_token_expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "refresh_token_expires_at": {
+                    "name": "refresh_token_expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "scope": {
+                    "name": "scope",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "password": {
+                    "name": "password",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {
+                "accounts_userId_idx": {
+                    "name": "accounts_userId_idx",
+                    "columns": ["user_id"],
+                    "isUnique": false
+                }
+            },
+            "foreignKeys": {
+                "accounts_user_id_users_id_fk": {
+                    "name": "accounts_user_id_users_id_fk",
+                    "tableFrom": "accounts",
+                    "tableTo": "users",
+                    "columnsFrom": ["user_id"],
+                    "columnsTo": ["id"],
+                    "onDelete": "cascade",
+                    "onUpdate": "no action"
+                }
+            },
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "rate_limits": {
+            "name": "rate_limits",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "key": {
+                    "name": "key",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "count": {
+                    "name": "count",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "last_request": {
+                    "name": "last_request",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {
+                "rate_limits_key_unique": {
+                    "name": "rate_limits_key_unique",
+                    "columns": ["key"],
+                    "isUnique": true
+                }
+            },
+            "foreignKeys": {},
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "sessions": {
+            "name": "sessions",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "expires_at": {
+                    "name": "expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "token": {
+                    "name": "token",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "ip_address": {
+                    "name": "ip_address",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "user_agent": {
+                    "name": "user_agent",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "user_id": {
+                    "name": "user_id",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "timezone": {
+                    "name": "timezone",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "city": {
+                    "name": "city",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "country": {
+                    "name": "country",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "region": {
+                    "name": "region",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "region_code": {
+                    "name": "region_code",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "colo": {
+                    "name": "colo",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "latitude": {
+                    "name": "latitude",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "longitude": {
+                    "name": "longitude",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                }
+            },
+            "indexes": {
+                "sessions_token_unique": {
+                    "name": "sessions_token_unique",
+                    "columns": ["token"],
+                    "isUnique": true
+                },
+                "sessions_userId_idx": {
+                    "name": "sessions_userId_idx",
+                    "columns": ["user_id"],
+                    "isUnique": false
+                }
+            },
+            "foreignKeys": {
+                "sessions_user_id_users_id_fk": {
+                    "name": "sessions_user_id_users_id_fk",
+                    "tableFrom": "sessions",
+                    "tableTo": "users",
+                    "columnsFrom": ["user_id"],
+                    "columnsTo": ["id"],
+                    "onDelete": "cascade",
+                    "onUpdate": "no action"
+                }
+            },
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "users": {
+            "name": "users",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "name": {
+                    "name": "name",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "email": {
+                    "name": "email",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "email_verified": {
+                    "name": "email_verified",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": false
+                },
+                "image": {
+                    "name": "image",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "is_anonymous": {
+                    "name": "is_anonymous",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": false,
+                    "autoincrement": false,
+                    "default": false
+                }
+            },
+            "indexes": {
+                "users_email_unique": {
+                    "name": "users_email_unique",
+                    "columns": ["email"],
+                    "isUnique": true
+                }
+            },
+            "foreignKeys": {},
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        },
+        "verifications": {
+            "name": "verifications",
+            "columns": {
+                "id": {
+                    "name": "id",
+                    "type": "text",
+                    "primaryKey": true,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "identifier": {
+                    "name": "identifier",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "value": {
+                    "name": "value",
+                    "type": "text",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "expires_at": {
+                    "name": "expires_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false
+                },
+                "created_at": {
+                    "name": "created_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                },
+                "updated_at": {
+                    "name": "updated_at",
+                    "type": "integer",
+                    "primaryKey": false,
+                    "notNull": true,
+                    "autoincrement": false,
+                    "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+                }
+            },
+            "indexes": {
+                "verifications_identifier_idx": {
+                    "name": "verifications_identifier_idx",
+                    "columns": ["identifier"],
+                    "isUnique": false
+                }
+            },
+            "foreignKeys": {},
+            "compositePrimaryKeys": {},
+            "uniqueConstraints": {},
+            "checkConstraints": {}
+        }
+    },
+    "views": {},
+    "enums": {},
+    "_meta": {
+        "schemas": {},
+        "tables": {},
+        "columns": {}
+    },
+    "internal": {
+        "indexes": {}
+    }
+}

+ 434 - 0
drizzle/meta/0003_snapshot.json

@@ -0,0 +1,434 @@
+{
+  "version": "6",
+  "dialect": "sqlite",
+  "id": "a19d571a-1642-4464-aeff-7aaeadec3092",
+  "prevId": "0fc9fede-d457-420e-8796-05cb0170fc40",
+  "tables": {
+    "accounts": {
+      "name": "accounts",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "access_token": {
+          "name": "access_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "refresh_token": {
+          "name": "refresh_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "id_token": {
+          "name": "id_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "access_token_expires_at": {
+          "name": "access_token_expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "refresh_token_expires_at": {
+          "name": "refresh_token_expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "scope": {
+          "name": "scope",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "password": {
+          "name": "password",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "accounts_userId_idx": {
+          "name": "accounts_userId_idx",
+          "columns": [
+            "user_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {
+        "accounts_user_id_users_id_fk": {
+          "name": "accounts_user_id_users_id_fk",
+          "tableFrom": "accounts",
+          "tableTo": "users",
+          "columnsFrom": [
+            "user_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "sessions": {
+      "name": "sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "token": {
+          "name": "token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ip_address": {
+          "name": "ip_address",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "user_agent": {
+          "name": "user_agent",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "timezone": {
+          "name": "timezone",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "city": {
+          "name": "city",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "country": {
+          "name": "country",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "region": {
+          "name": "region",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "region_code": {
+          "name": "region_code",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "colo": {
+          "name": "colo",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "latitude": {
+          "name": "latitude",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "longitude": {
+          "name": "longitude",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "sessions_token_unique": {
+          "name": "sessions_token_unique",
+          "columns": [
+            "token"
+          ],
+          "isUnique": true
+        },
+        "sessions_userId_idx": {
+          "name": "sessions_userId_idx",
+          "columns": [
+            "user_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {
+        "sessions_user_id_users_id_fk": {
+          "name": "sessions_user_id_users_id_fk",
+          "tableFrom": "sessions",
+          "tableTo": "users",
+          "columnsFrom": [
+            "user_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "users": {
+      "name": "users",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "email": {
+          "name": "email",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "email_verified": {
+          "name": "email_verified",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "image": {
+          "name": "image",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "is_anonymous": {
+          "name": "is_anonymous",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false,
+          "default": false
+        }
+      },
+      "indexes": {
+        "users_email_unique": {
+          "name": "users_email_unique",
+          "columns": [
+            "email"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "verifications": {
+      "name": "verifications",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "identifier": {
+          "name": "identifier",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "value": {
+          "name": "value",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        }
+      },
+      "indexes": {
+        "verifications_identifier_idx": {
+          "name": "verifications_identifier_idx",
+          "columns": [
+            "identifier"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    }
+  },
+  "views": {},
+  "enums": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "indexes": {}
+  }
+}

+ 399 - 0
drizzle/meta/0004_snapshot.json

@@ -0,0 +1,399 @@
+{
+  "version": "6",
+  "dialect": "sqlite",
+  "id": "747924cd-f308-4e86-8330-8aee9a73881c",
+  "prevId": "a19d571a-1642-4464-aeff-7aaeadec3092",
+  "tables": {
+    "accounts": {
+      "name": "accounts",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "access_token": {
+          "name": "access_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "refresh_token": {
+          "name": "refresh_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "id_token": {
+          "name": "id_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "access_token_expires_at": {
+          "name": "access_token_expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "refresh_token_expires_at": {
+          "name": "refresh_token_expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "scope": {
+          "name": "scope",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "password": {
+          "name": "password",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "accounts_userId_idx": {
+          "name": "accounts_userId_idx",
+          "columns": [
+            "user_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {
+        "accounts_user_id_users_id_fk": {
+          "name": "accounts_user_id_users_id_fk",
+          "tableFrom": "accounts",
+          "tableTo": "users",
+          "columnsFrom": [
+            "user_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "sessions": {
+      "name": "sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "token": {
+          "name": "token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ip_address": {
+          "name": "ip_address",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "user_agent": {
+          "name": "user_agent",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "sessions_token_unique": {
+          "name": "sessions_token_unique",
+          "columns": [
+            "token"
+          ],
+          "isUnique": true
+        },
+        "sessions_userId_idx": {
+          "name": "sessions_userId_idx",
+          "columns": [
+            "user_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {
+        "sessions_user_id_users_id_fk": {
+          "name": "sessions_user_id_users_id_fk",
+          "tableFrom": "sessions",
+          "tableTo": "users",
+          "columnsFrom": [
+            "user_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "users": {
+      "name": "users",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "email": {
+          "name": "email",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "email_verified": {
+          "name": "email_verified",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "image": {
+          "name": "image",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "is_anonymous": {
+          "name": "is_anonymous",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false,
+          "default": false
+        },
+        "phone_number": {
+          "name": "phone_number",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "phone_number_verified": {
+          "name": "phone_number_verified",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "users_email_unique": {
+          "name": "users_email_unique",
+          "columns": [
+            "email"
+          ],
+          "isUnique": true
+        },
+        "users_phone_number_unique": {
+          "name": "users_phone_number_unique",
+          "columns": [
+            "phone_number"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "verifications": {
+      "name": "verifications",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "identifier": {
+          "name": "identifier",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "value": {
+          "name": "value",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
+        }
+      },
+      "indexes": {
+        "verifications_identifier_idx": {
+          "name": "verifications_identifier_idx",
+          "columns": [
+            "identifier"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    }
+  },
+  "views": {},
+  "enums": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "indexes": {}
+  }
+}

+ 41 - 0
drizzle/meta/_journal.json

@@ -0,0 +1,41 @@
+{
+  "version": "7",
+  "dialect": "sqlite",
+  "entries": [
+    {
+      "idx": 0,
+      "version": "6",
+      "when": 1749249948625,
+      "tag": "0000_groovy_manta",
+      "breakpoints": true
+    },
+    {
+      "idx": 1,
+      "version": "6",
+      "when": 1775400800584,
+      "tag": "0001_furry_unicorn",
+      "breakpoints": true
+    },
+    {
+      "idx": 2,
+      "version": "6",
+      "when": 1788704593842,
+      "tag": "0002_add_rate_limits",
+      "breakpoints": true
+    },
+    {
+      "idx": 3,
+      "version": "6",
+      "when": 1789794840870,
+      "tag": "0003_violet_vampiro",
+      "breakpoints": true
+    },
+    {
+      "idx": 4,
+      "version": "6",
+      "when": 1789795095782,
+      "tag": "0004_quiet_lethal_legion",
+      "breakpoints": true
+    }
+  ]
+}

+ 3645 - 0
package-lock.json

@@ -0,0 +1,3645 @@
+{
+  "name": "pm2000",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "pm2000",
+      "dependencies": {
+        "@better-auth/drizzle-adapter": "1.7.3",
+        "@cloudflare/workers-types": "^5.20260830.1",
+        "better-auth": "1.7.3",
+        "better-auth-cloudflare": "latest",
+        "drizzle-orm": "^0.45.0",
+        "hono": "4.13.5"
+      },
+      "devDependencies": {
+        "@types/node": "^22.15.30",
+        "drizzle-kit": "^0.31.4",
+        "typescript": "^5.8.3",
+        "wrangler": "4.127.1"
+      }
+    },
+    "node_modules/@better-auth/core": {
+      "version": "1.7.5",
+      "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.7.5.tgz",
+      "integrity": "sha512-kVlSu4H8OKQfjg4b/Zj5MOaospt83N0JbX38wsDzE58Yw95jzovFkU3pxzB1eUFYc4mkuhUMZD8iT1gpUjNMcQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "^1.41.1",
+        "@standard-schema/spec": "^1.1.0",
+        "zod": "^4.5.4"
+      },
+      "peerDependencies": {
+        "@better-auth/utils": "0.4.2",
+        "@better-fetch/fetch": "1.3.2",
+        "@opentelemetry/api": "^1.9.0",
+        "better-call": "1.4.0",
+        "jose": "^6.1.0",
+        "kysely": "^0.28.5 || ^0.29.0",
+        "nanostores": "^1.0.1"
+      },
+      "peerDependenciesMeta": {
+        "@opentelemetry/api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@better-auth/drizzle-adapter": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.7.3.tgz",
+      "integrity": "sha512-S+nQRlxbUhkR43LrSv8c98ZvOvmv3nrtOnHkiZXkdDkr60PWp7maC2cqgzZ2C9exCC1a+4TugJlx2jRL+r+/9A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@better-auth/core": "^1.7.3",
+        "@better-auth/utils": "0.4.2",
+        "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "drizzle-orm": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@better-auth/kysely-adapter": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.7.3.tgz",
+      "integrity": "sha512-UIsyJMIrjUnT+yTaS6dkCxYYmtPwxFHxwSJ8+CLty2II5w9BewlDxDA0/QzhoL/InYCPxQ5Y6xIgLHZG1dhwRA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@better-auth/core": "^1.7.3",
+        "@better-auth/utils": "0.4.2",
+        "kysely": "^0.28.17 || ^0.29.0"
+      },
+      "peerDependenciesMeta": {
+        "kysely": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@better-auth/memory-adapter": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.7.3.tgz",
+      "integrity": "sha512-WdLANFY/QWC3G351RCzxU+Y9YlW+BQ1oG9NwBTSOUWQw5rZ87ws+weU8tvAMO7sQ4C9gKIlkOKBKUKXrSt91Tw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@better-auth/core": "^1.7.3",
+        "@better-auth/utils": "0.4.2"
+      }
+    },
+    "node_modules/@better-auth/mongo-adapter": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.7.3.tgz",
+      "integrity": "sha512-YL9m01tNogmFmRvOWJ46M9WwE6HirCXHDell29mtsQB/Qs1TPNLrgj7ybGMMGhQuyj6S218+8Wbls8m9s+i8RQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@better-auth/core": "^1.7.3",
+        "@better-auth/utils": "0.4.2",
+        "mongodb": "^6.0.0 || ^7.0.0"
+      },
+      "peerDependenciesMeta": {
+        "mongodb": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@better-auth/prisma-adapter": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.7.3.tgz",
+      "integrity": "sha512-TJ/DhlU7oLzrC626/1wfYA1Pl+lVsXa/zXBZ8d7Rlc2YO5fGd5fsAYJdRrYMyA51iZEo+rWLXMhZ0cez1BamsQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@better-auth/core": "^1.7.3",
+        "@better-auth/utils": "0.4.2",
+        "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
+        "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@prisma/client": {
+          "optional": true
+        },
+        "prisma": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@better-auth/utils": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz",
+      "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "^2.0.1"
+      }
+    },
+    "node_modules/@better-fetch/fetch": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.2.tgz",
+      "integrity": "sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==",
+      "license": "MIT"
+    },
+    "node_modules/@cloudflare/kv-asset-handler": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
+      "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "engines": {
+        "node": ">=22.0.0"
+      }
+    },
+    "node_modules/@cloudflare/unenv-preset": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
+      "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "peerDependencies": {
+        "unenv": "2.0.0-rc.24",
+        "workerd": ">1.20260305.0 <2.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "workerd": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@cloudflare/workerd-darwin-64": {
+      "version": "1.20260828.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260828.1.tgz",
+      "integrity": "sha512-CVd+xPhqUESg8Xhq09TZx0wl4FSirfJGOzvbPz2yHhBIvmNHFFQkSN3rkd7wEwnhQQk37Xi0/aD6ykPLJbmGiQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-darwin-arm64": {
+      "version": "1.20260828.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260828.1.tgz",
+      "integrity": "sha512-5HDPXRM152vU5JveByGFk34X57TVyIsfp4cabepAf45DC0MKvm52ucJqAjW1h8bvW4X+zRw9GU35OHF9FEC9Ww==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-linux-64": {
+      "version": "1.20260828.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260828.1.tgz",
+      "integrity": "sha512-MQ1Ll9P7F72HHUKizbb7BlDfbY8fRoNMpbIpZoU6uKsSkneFICWSKv6UlgU9EQZ+w0i7TMa12iUgJ8l29eRI9A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-linux-arm64": {
+      "version": "1.20260828.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260828.1.tgz",
+      "integrity": "sha512-FBTaUQ1xcU9jcp4OyBPcH8x0QiFvc1iuZL2GkD8zp2q1WyTVHYOptRDQUU+cuHjt0rQ2EIKVPBjahPxfa0joBw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-windows-64": {
+      "version": "1.20260828.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260828.1.tgz",
+      "integrity": "sha512-yvr77hC7dUbvK5K+SCg062kkPq3sx+drV1PcgHslzHDYcJBtT0V3X80qLE49LW1vq2svaeNmsVQS+vHsqWu8cQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workers-types": {
+      "version": "5.20260919.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260919.1.tgz",
+      "integrity": "sha512-xUdKJoTHhNcXwnBrfrM1STkzm3oSUc9cRVzP8fO5NyJZ/UtnDnWZHDk9z8IkTnbC2WRWFAS87HICmqZhbHDvQw==",
+      "license": "MIT OR Apache-2.0"
+    },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.9"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@drizzle-team/brocli": {
+      "version": "0.10.2",
+      "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
+      "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
+      "devOptional": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.11.3",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+      "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
+      "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
+      "deprecated": "Merged into tsx: https://tsx.hirok.io",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.18.20",
+        "source-map-support": "^0.5.21"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+      "devOptional": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "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"
+      }
+    },
+    "node_modules/@esbuild-kit/esm-loader": {
+      "version": "2.6.5",
+      "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
+      "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
+      "deprecated": "Merged into tsx: https://tsx.hirok.io",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@esbuild-kit/core-utils": "^3.3.2",
+        "get-tsconfig": "^4.7.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/colour": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+      "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz",
+      "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz",
+      "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-freebsd-wasm32": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz",
+      "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "dependencies": {
+        "@img/sharp-wasm32": "0.35.2"
+      },
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz",
+      "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz",
+      "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz",
+      "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz",
+      "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz",
+      "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-riscv64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz",
+      "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz",
+      "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz",
+      "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz",
+      "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz",
+      "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz",
+      "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz",
+      "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz",
+      "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-riscv64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz",
+      "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-riscv64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz",
+      "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz",
+      "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz",
+      "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz",
+      "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.3.1"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz",
+      "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==",
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.11.1"
+      },
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-webcontainers-wasm32": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz",
+      "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "@img/sharp-wasm32": "0.35.2"
+      },
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz",
+      "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz",
+      "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz",
+      "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+      "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.0.3",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      }
+    },
+    "node_modules/@noble/ciphers": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.4.0.tgz",
+      "integrity": "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/hashes": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+      "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.43.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+      "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@poppinss/colors": {
+      "version": "4.1.6",
+      "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
+      "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^4.1.5"
+      }
+    },
+    "node_modules/@poppinss/dumper": {
+      "version": "0.6.5",
+      "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
+      "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@poppinss/colors": "^4.1.5",
+        "@sindresorhus/is": "^7.0.2",
+        "supports-color": "^10.0.0"
+      }
+    },
+    "node_modules/@poppinss/exception": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
+      "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@sindresorhus/is": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
+      "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/is?sponsor=1"
+      }
+    },
+    "node_modules/@speed-highlight/core": {
+      "version": "1.2.24",
+      "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz",
+      "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "22.20.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz",
+      "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/better-auth": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.7.3.tgz",
+      "integrity": "sha512-8xGp68JQ+l36kniDEgP8bP99TLi1GdEv0NTEUBkyqnYnus/cgFUZseUfqUHKzr2BAsg2O6aD88I9f0U68shanQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@better-auth/core": "1.7.3",
+        "@better-auth/drizzle-adapter": "1.7.3",
+        "@better-auth/kysely-adapter": "1.7.3",
+        "@better-auth/memory-adapter": "1.7.3",
+        "@better-auth/mongo-adapter": "1.7.3",
+        "@better-auth/prisma-adapter": "1.7.3",
+        "@better-auth/telemetry": "1.7.3",
+        "@better-auth/utils": "0.4.2",
+        "@better-fetch/fetch": "1.3.1",
+        "@noble/ciphers": "^2.2.0",
+        "@noble/hashes": "^2.2.0",
+        "better-call": "1.4.0",
+        "defu": "^6.1.4",
+        "jose": "^6.2.3",
+        "kysely": "^0.28.17 || ^0.29.0",
+        "nanostores": "^1.3.0",
+        "zod": "^4.5.4"
+      },
+      "peerDependencies": {
+        "@lynx-js/react": "*",
+        "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
+        "@sveltejs/kit": "^2.0.0",
+        "@tanstack/react-start": "^1.0.0",
+        "@tanstack/solid-start": "^1.0.0",
+        "better-sqlite3": "^12.0.0",
+        "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
+        "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
+        "mongodb": "^6.0.0 || ^7.0.0",
+        "mysql2": "^3.0.0",
+        "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
+        "pg": "^8.0.0",
+        "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0",
+        "solid-js": "^1.0.0",
+        "svelte": "^4.0.0 || ^5.0.0",
+        "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
+        "vue": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@lynx-js/react": {
+          "optional": true
+        },
+        "@prisma/client": {
+          "optional": true
+        },
+        "@sveltejs/kit": {
+          "optional": true
+        },
+        "@tanstack/react-start": {
+          "optional": true
+        },
+        "@tanstack/solid-start": {
+          "optional": true
+        },
+        "better-sqlite3": {
+          "optional": true
+        },
+        "drizzle-kit": {
+          "optional": true
+        },
+        "drizzle-orm": {
+          "optional": true
+        },
+        "mongodb": {
+          "optional": true
+        },
+        "mysql2": {
+          "optional": true
+        },
+        "next": {
+          "optional": true
+        },
+        "pg": {
+          "optional": true
+        },
+        "prisma": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        },
+        "solid-js": {
+          "optional": true
+        },
+        "svelte": {
+          "optional": true
+        },
+        "vitest": {
+          "optional": true
+        },
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/better-auth-cloudflare": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/better-auth-cloudflare/-/better-auth-cloudflare-0.3.1.tgz",
+      "integrity": "sha512-kvGYe1++fH4ODkb2o/WNkGWQtPYuWXjNPaKDAvPz8ChbaROW9TmZY0AIan9K52p+p+Pe53ZnQ7kKOWWZ2x0KrQ==",
+      "license": "MIT",
+      "dependencies": {
+        "drizzle-orm": "^0.45.0",
+        "mime": "^4.1.0",
+        "zod": "^4.3.0"
+      },
+      "peerDependencies": {
+        "@better-auth/drizzle-adapter": "^1.5.0",
+        "@cloudflare/workers-types": ">=4",
+        "better-auth": "^1.5.0"
+      }
+    },
+    "node_modules/better-auth/node_modules/@better-auth/core": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.7.3.tgz",
+      "integrity": "sha512-JdP7lOkyE83jgjn7RilJj1XvZ7n2JjRsErKJuaXchjyuNo6cf1iVd3GtbhAtiUyJJkWdk8yL+LaUBKT80H0zLA==",
+      "license": "MIT",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "^1.41.1",
+        "@standard-schema/spec": "^1.1.0",
+        "zod": "^4.5.4"
+      },
+      "peerDependencies": {
+        "@better-auth/utils": "0.4.2",
+        "@better-fetch/fetch": "1.3.1",
+        "@cloudflare/workers-types": ">=4",
+        "@opentelemetry/api": "^1.9.0",
+        "better-call": "1.4.0",
+        "jose": "^6.1.0",
+        "kysely": "^0.28.5 || ^0.29.0",
+        "nanostores": "^1.0.1"
+      },
+      "peerDependenciesMeta": {
+        "@cloudflare/workers-types": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/better-auth/node_modules/@better-auth/telemetry": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.7.3.tgz",
+      "integrity": "sha512-aixgHbJhGvS8PRczX/LR3murYyBnIvOGmJw37ZZBMg6ZtLR/UBJAkufbDi1HYn5THSuTJ3D5DtY85Ahh/ABQtw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@better-auth/core": "^1.7.3",
+        "@better-auth/utils": "0.4.2",
+        "@better-fetch/fetch": "1.3.1"
+      }
+    },
+    "node_modules/better-auth/node_modules/@better-fetch/fetch": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.1.tgz",
+      "integrity": "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==",
+      "license": "MIT"
+    },
+    "node_modules/better-call": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.4.0.tgz",
+      "integrity": "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==",
+      "license": "MIT",
+      "dependencies": {
+        "@better-auth/utils": "^0.5.0",
+        "@better-fetch/fetch": "^1.3.1",
+        "rou3": "^0.9.1",
+        "set-cookie-parser": "^3.1.2"
+      },
+      "peerDependencies": {
+        "zod": "^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/better-call/node_modules/@better-auth/utils": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.5.0.tgz",
+      "integrity": "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "^2.0.1"
+      }
+    },
+    "node_modules/blake3-wasm": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
+      "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/defu": {
+      "version": "6.1.7",
+      "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+      "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+      "license": "MIT"
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/drizzle-kit": {
+      "version": "0.31.10",
+      "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
+      "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@drizzle-team/brocli": "^0.10.2",
+        "@esbuild-kit/esm-loader": "^2.5.5",
+        "esbuild": "^0.25.4",
+        "tsx": "^4.21.0"
+      },
+      "bin": {
+        "drizzle-kit": "bin.cjs"
+      }
+    },
+    "node_modules/drizzle-orm": {
+      "version": "0.45.2",
+      "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
+      "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
+      "license": "Apache-2.0",
+      "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.13",
+        "@prisma/client": "*",
+        "@tidbcloud/serverless": "*",
+        "@types/better-sqlite3": "*",
+        "@types/pg": "*",
+        "@types/sql.js": "*",
+        "@upstash/redis": ">=1.34.7",
+        "@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",
+        "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
+        },
+        "@upstash/redis": {
+          "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
+        }
+      }
+    },
+    "node_modules/error-stack-parser-es": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+      "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "devOptional": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/get-tsconfig": {
+      "version": "4.14.3",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz",
+      "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
+    "node_modules/hono": {
+      "version": "4.13.5",
+      "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.5.tgz",
+      "integrity": "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.9.0"
+      }
+    },
+    "node_modules/jose": {
+      "version": "6.2.12",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
+      "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/kleur": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/kysely": {
+      "version": "0.29.6",
+      "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.6.tgz",
+      "integrity": "sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=22.0.0"
+      }
+    },
+    "node_modules/mime": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
+      "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
+      "funding": [
+        "https://github.com/sponsors/broofa"
+      ],
+      "license": "MIT",
+      "bin": {
+        "mime": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/miniflare": {
+      "version": "5.20260828.0-alpha",
+      "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260828.0-alpha.tgz",
+      "integrity": "sha512-6nbxhZEcz/UET3Y1OnYPsrAUjUmuFoib3ynUqteRdn1YnDxsLg8cwgZJZCk9QmtOmGzXwzXzgE/d/C0dJAPtVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@cspotcode/source-map-support": "0.8.1",
+        "sharp": "0.35.2",
+        "undici": "7.29.0",
+        "workerd": "1.20260828.1",
+        "ws": "8.21.0",
+        "youch": "4.1.0-beta.10"
+      },
+      "engines": {
+        "node": ">=22.0.0"
+      }
+    },
+    "node_modules/nanostores": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.5.3.tgz",
+      "integrity": "sha512-rQLB6eV4f2AW/n3L0JmwCROpaisYy9EDEADvEFSd1C/qG8hB6O5TPlh9A791JRbJr4CnMQBzptDcvD9OR1+6WA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": "^20.0.0 || >=22.0.0"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "devOptional": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
+    "node_modules/rou3": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz",
+      "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/set-cookie-parser": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
+      "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
+      "license": "MIT"
+    },
+    "node_modules/sharp": {
+      "version": "0.35.2",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz",
+      "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@img/colour": "^1.1.0",
+        "detect-libc": "^2.1.2",
+        "semver": "^7.8.4"
+      },
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.35.2",
+        "@img/sharp-darwin-x64": "0.35.2",
+        "@img/sharp-freebsd-wasm32": "0.35.2",
+        "@img/sharp-libvips-darwin-arm64": "1.3.1",
+        "@img/sharp-libvips-darwin-x64": "1.3.1",
+        "@img/sharp-libvips-linux-arm": "1.3.1",
+        "@img/sharp-libvips-linux-arm64": "1.3.1",
+        "@img/sharp-libvips-linux-ppc64": "1.3.1",
+        "@img/sharp-libvips-linux-riscv64": "1.3.1",
+        "@img/sharp-libvips-linux-s390x": "1.3.1",
+        "@img/sharp-libvips-linux-x64": "1.3.1",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.3.1",
+        "@img/sharp-libvips-linuxmusl-x64": "1.3.1",
+        "@img/sharp-linux-arm": "0.35.2",
+        "@img/sharp-linux-arm64": "0.35.2",
+        "@img/sharp-linux-ppc64": "0.35.2",
+        "@img/sharp-linux-riscv64": "0.35.2",
+        "@img/sharp-linux-s390x": "0.35.2",
+        "@img/sharp-linux-x64": "0.35.2",
+        "@img/sharp-linuxmusl-arm64": "0.35.2",
+        "@img/sharp-linuxmusl-x64": "0.35.2",
+        "@img/sharp-webcontainers-wasm32": "0.35.2",
+        "@img/sharp-win32-arm64": "0.35.2",
+        "@img/sharp-win32-ia32": "0.35.2",
+        "@img/sharp-win32-x64": "0.35.2"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "devOptional": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
+      "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "dev": true,
+      "license": "0BSD",
+      "optional": true
+    },
+    "node_modules/tsx": {
+      "version": "4.23.13",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
+      "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.28.0"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+      "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/android-arm": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+      "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+      "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/android-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+      "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+      "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+      "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+      "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+      "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+      "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+      "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+      "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+      "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+      "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+      "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+      "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+      "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+      "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+      "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+      "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+      "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+      "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+      "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+      "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tsx/node_modules/esbuild": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+      "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+      "devOptional": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.28.2",
+        "@esbuild/android-arm": "0.28.2",
+        "@esbuild/android-arm64": "0.28.2",
+        "@esbuild/android-x64": "0.28.2",
+        "@esbuild/darwin-arm64": "0.28.2",
+        "@esbuild/darwin-x64": "0.28.2",
+        "@esbuild/freebsd-arm64": "0.28.2",
+        "@esbuild/freebsd-x64": "0.28.2",
+        "@esbuild/linux-arm": "0.28.2",
+        "@esbuild/linux-arm64": "0.28.2",
+        "@esbuild/linux-ia32": "0.28.2",
+        "@esbuild/linux-loong64": "0.28.2",
+        "@esbuild/linux-mips64el": "0.28.2",
+        "@esbuild/linux-ppc64": "0.28.2",
+        "@esbuild/linux-riscv64": "0.28.2",
+        "@esbuild/linux-s390x": "0.28.2",
+        "@esbuild/linux-x64": "0.28.2",
+        "@esbuild/netbsd-arm64": "0.28.2",
+        "@esbuild/netbsd-x64": "0.28.2",
+        "@esbuild/openbsd-arm64": "0.28.2",
+        "@esbuild/openbsd-x64": "0.28.2",
+        "@esbuild/openharmony-arm64": "0.28.2",
+        "@esbuild/sunos-x64": "0.28.2",
+        "@esbuild/win32-arm64": "0.28.2",
+        "@esbuild/win32-ia32": "0.28.2",
+        "@esbuild/win32-x64": "0.28.2"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+      "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unenv": {
+      "version": "2.0.0-rc.24",
+      "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
+      "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/workerd": {
+      "version": "1.20260828.1",
+      "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260828.1.tgz",
+      "integrity": "sha512-pB9yvt0kkwZDAGZHmpY59r0o3hM0DzdW6BJERqwZOhunZ3ssOyDSgQxOQer2cSZW4YCFeOTIQYN1qwhK5wv/Cw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "workerd": "bin/workerd"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "optionalDependencies": {
+        "@cloudflare/workerd-darwin-64": "1.20260828.1",
+        "@cloudflare/workerd-darwin-arm64": "1.20260828.1",
+        "@cloudflare/workerd-linux-64": "1.20260828.1",
+        "@cloudflare/workerd-linux-arm64": "1.20260828.1",
+        "@cloudflare/workerd-windows-64": "1.20260828.1"
+      }
+    },
+    "node_modules/wrangler": {
+      "version": "4.127.1",
+      "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.127.1.tgz",
+      "integrity": "sha512-OzsiNgaI8i681L/+KnAKc+uEZ5D57xK5JuNvCOpRKICF4/5Q3Cu1oTGuUiT/f3GDUqQb3gzXNT0tfOHGMEtknw==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "dependencies": {
+        "@cloudflare/kv-asset-handler": "0.5.0",
+        "@cloudflare/unenv-preset": "2.16.1",
+        "blake3-wasm": "2.1.5",
+        "esbuild": "0.28.1",
+        "miniflare": "5.20260828.0-alpha",
+        "path-to-regexp": "6.3.0",
+        "unenv": "2.0.0-rc.24",
+        "workerd": "1.20260828.1"
+      },
+      "bin": {
+        "cf-wrangler": "bin/cf-wrangler.js",
+        "wrangler": "bin/wrangler.js",
+        "wrangler2": "bin/wrangler.js"
+      },
+      "engines": {
+        "node": ">=22.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.3"
+      },
+      "peerDependencies": {
+        "@cloudflare/workers-types": "^5.20260828.1"
+      },
+      "peerDependenciesMeta": {
+        "@cloudflare/workers-types": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+      "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/android-arm": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+      "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/android-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+      "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/android-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+      "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+      "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/darwin-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+      "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+      "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+      "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-arm": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+      "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+      "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-ia32": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+      "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-loong64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+      "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+      "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+      "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+      "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-s390x": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+      "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/linux-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+      "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+      "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+      "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+      "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+      "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+      "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/sunos-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+      "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/win32-arm64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+      "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/win32-ia32": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+      "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/@esbuild/win32-x64": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+      "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/wrangler/node_modules/esbuild": {
+      "version": "0.28.1",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+      "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "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"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.21.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+      "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/youch": {
+      "version": "4.1.0-beta.10",
+      "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
+      "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@poppinss/colors": "^4.1.5",
+        "@poppinss/dumper": "^0.6.4",
+        "@speed-highlight/core": "^1.2.7",
+        "cookie": "^1.0.2",
+        "youch-core": "^0.3.3"
+      }
+    },
+    "node_modules/youch-core": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
+      "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@poppinss/exception": "^1.2.2",
+        "error-stack-parser-es": "^1.0.5"
+      }
+    },
+    "node_modules/zod": {
+      "version": "4.6.5",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
+      "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    }
+  }
+}

+ 38 - 0
package.json

@@ -0,0 +1,38 @@
+{
+  "name": "pm2000",
+  "scripts": {
+    "dev": "wrangler dev",
+    "build": "npx tsc --noEmit",
+    "deploy": "wrangler deploy --minify",
+    "cf-typegen": "wrangler types",
+    "auth:generate": "npx --yes auth@1.7.3 generate --config src/auth/index.ts --output src/db/auth.schema.ts -y",
+    "auth:format": "npx --yes prettier --write src/db/auth.schema.ts",
+    "auth:update": "npm run auth:generate && npm run auth:format",
+    "db:generate": "drizzle-kit generate",
+    "db:migrate:dev": "wrangler d1 migrations apply DATABASE --local",
+    "db:migrate:prod": "wrangler d1 migrations apply DATABASE --remote",
+    "db:studio:dev": "drizzle-kit studio",
+    "db:studio:prod": "NODE_ENV=production drizzle-kit studio"
+  },
+  "dependencies": {
+    "@better-auth/drizzle-adapter": "1.7.3",
+    "@cloudflare/workers-types": "^5.20260830.1",
+    "better-auth": "1.7.3",
+    "better-auth-cloudflare": "latest",
+    "drizzle-orm": "^0.45.0",
+    "hono": "4.13.5"
+  },
+  "devDependencies": {
+    "@types/node": "^22.15.30",
+    "drizzle-kit": "^0.31.4",
+    "typescript": "^5.8.3",
+    "wrangler": "4.127.1"
+  },
+  "pnpm": {
+    "overrides": {
+      "@better-auth/utils": "0.4.2",
+      "defu": "6.1.5"
+    }
+  },
+  "packageManager": "pnpm@10.10.0"
+}

+ 262 - 0
public/index.html

@@ -0,0 +1,262 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
+<title>Plan Maker 2000</title>
+<style>
+  :root { --bg:#f6f6f6; --card:#fff; --ink:#111; --mute:#777; --line:#e5e5e5; --felt:#0d3b2e; --felt2:#0a2d23; --gold:#f2c14e; --chip:#e63946; --chip2:#ffd166; --jev:#7c5cff; }
+  * { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
+  body { margin:0; font-family:-apple-system, system-ui, sans-serif; background:var(--bg); color:var(--ink); transition: background .6s; }
+  body.felt { background:var(--felt); color:#fff; }
+  body.hot { background:var(--felt2); }
+  body.crt { background:#000; }
+  #app { max-width:480px; margin:0 auto; padding:16px; padding-bottom:120px; }
+  h1 { font-size:22px; margin:8px 0 4px; }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px; margin:10px 0; color:var(--ink); }
+  body.felt .card { background:rgba(255,255,255,.06); border-color:rgba(255,255,255,.12); color:#fff; }
+  .mute { color:var(--mute); font-size:13px; }
+  body.felt .mute { color:rgba(255,255,255,.6); }
+  input, button { font:inherit; }
+  input { width:100%; padding:12px; border:1px solid var(--line); border-radius:10px; margin:6px 0; background:#fff; color:#111; }
+  button { padding:12px 16px; border:0; border-radius:10px; background:#111; color:#fff; font-weight:600; width:100%; margin:6px 0; }
+  button.ghost { background:transparent; border:1px solid var(--line); color:inherit; }
+  button.small { width:auto; padding:8px 12px; margin:0 4px 0 0; font-size:14px; }
+  button:disabled { opacity:.4; }
+  .opt { display:flex; align-items:center; gap:10px; padding:12px; border-radius:12px; margin:8px 0; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.14); position:relative; overflow:hidden; }
+  .opt .label { flex:1; font-weight:600; }
+  .opt .odds { font-variant-numeric:tabular-nums; font-size:13px; min-width:44px; text-align:right; }
+  .opt .bar { position:absolute; left:0; bottom:0; height:3px; background:var(--jev); transition:width .35s ease; }
+  .opt .bar.wobble { animation:wob 1.2s infinite alternate ease-in-out; }
+  @keyframes wob { from { opacity:.5; transform:scaleX(.96);} to { opacity:1; transform:scaleX(1.04);} }
+  .opt.dead { opacity:.35; text-decoration:line-through; }
+  .opt.more { border-style:dashed; }
+  .chips { display:flex; gap:4px; flex-wrap:wrap; }
+  .chip { width:16px; height:16px; border-radius:50%; background:var(--chip); border:2px solid #fff3; }
+  .chip.other { background:var(--chip2); }
+  .stack { display:flex; gap:6px; align-items:center; }
+  .stack button { width:34px; height:34px; padding:0; border-radius:50%; background:#fff2; color:#fff; font-size:18px; margin:0; }
+  .bank { display:flex; gap:4px; justify-content:center; margin:10px 0; }
+  .bank .chip { width:22px; height:22px; }
+  .members { display:flex; gap:6px; flex-wrap:wrap; font-size:12px; }
+  .member { padding:4px 8px; border-radius:99px; background:rgba(255,255,255,.12); }
+  .member.locked { background:var(--gold); color:#111; }
+  .jev { display:flex; align-items:center; gap:8px; font-size:13px; margin-top:8px; }
+  .jev .face { width:20px; height:20px; background:var(--jev); border-radius:4px; image-rendering:pixelated; }
+  body.hot .jev .face { width:28px; height:28px; animation:bob .5s infinite alternate; }
+  @keyframes bob { from { transform:translateY(0);} to { transform:translateY(-3px);} }
+  .footer { position:fixed; left:0; right:0; bottom:0; padding:12px 16px calc(12px + env(safe-area-inset-bottom)); background:rgba(0,0,0,.35); backdrop-filter:blur(10px); }
+  .footer .inner { max-width:480px; margin:0 auto; display:flex; gap:8px; }
+  .lever { height:64px; font-size:20px; background:var(--gold); color:#111; border-radius:14px; }
+  /* reveal */
+  #reveal { position:fixed; inset:0; background:#000; color:#fff; display:none; flex-direction:column; align-items:center; justify-content:center; padding:24px; text-align:center; z-index:9; }
+  #reveal.on { display:flex; }
+  #reveal .scan { position:absolute; inset:0; background:repeating-linear-gradient(0deg, rgba(255,255,255,.04) 0 1px, transparent 1px 3px); pointer-events:none; }
+  .reel { font-size:34px; font-weight:800; min-height:48px; letter-spacing:.5px; }
+  .reel.shake { animation:shake .12s 4; }
+  @keyframes shake { 0%{transform:translate(2px,-1px)} 50%{transform:translate(-2px,1px)} 100%{transform:translate(0)} }
+  .winner { font-size:40px; font-weight:900; color:var(--gold); margin:10px 0; }
+  .upset { color:#ff4d6d; font-weight:900; letter-spacing:2px; }
+  .dealer { font-size:16px; opacity:.9; margin:8px 0 18px; max-width:340px; }
+  .odds-list { font-size:13px; opacity:.7; }
+  .reel-frame { border:2px solid var(--gold); border-radius:12px; padding:18px 24px; min-width:260px; }
+</style>
+</head>
+<body>
+<div id="app"></div>
+<div id="reveal"><div class="scan"></div><div id="reveal-body"></div></div>
+<script>
+const $ = s => document.querySelector(s);
+const handId = location.pathname.startsWith('/h/') ? location.pathname.split('/')[2] : null;
+let me = null, state = null, ws = null, revealed = null;
+const MORE = 'more_options';
+
+async function api(path, body) {
+  const r = await fetch(path, { method: body ? 'POST' : 'GET', credentials:'include', headers:{'Content-Type':'application/json'}, body: body ? JSON.stringify(body) : undefined });
+  if (!r.ok) { const t = await r.text(); toast(t); throw new Error(t); }
+  return r.json();
+}
+function toast(t){ const d=document.createElement('div'); d.textContent=t; d.style.cssText='position:fixed;top:12px;left:50%;transform:translateX(-50%);background:#111;color:#fff;padding:8px 14px;border-radius:99px;font-size:13px;z-index:20'; document.body.appendChild(d); setTimeout(()=>d.remove(),1800); }
+
+async function session() {
+  const r = await fetch('/api/auth/get-session', { credentials:'include' });
+  const t = await r.text(); if (!t) return null;
+  const j = JSON.parse(t); return j && j.user ? { id:j.user.id, name:j.user.name } : null;
+}
+
+// ---------- screens ----------
+function render() {
+  if (!me) return renderJoin();
+  if (!handId) return renderCreate();
+  if (!state) return $('#app').innerHTML = '<p class="mute">Loading hand…</p>';
+  renderHand();
+}
+
+function renderJoin() {
+  document.body.className = '';
+  $('#app').innerHTML = `
+    <h1>Plan Maker 2000</h1>
+    <div class="card">
+      <p class="mute">Who are you at this table?</p>
+      <input id="name" placeholder="Your name" autocomplete="off">
+      <button onclick="joinAnon()">Jump in</button>
+      <details><summary class="mute">Sign in with phone (OTP)</summary>
+        <input id="phone" placeholder="+61…" inputmode="tel">
+        <button class="ghost" onclick="sendOtp()">Send code</button>
+        <input id="code" placeholder="6-digit code" inputmode="numeric" style="display:none">
+        <button class="ghost" id="verify" onclick="verifyOtp()" style="display:none">Verify</button>
+      </details>
+    </div>`;
+}
+async function joinAnon() {
+  const name = $('#name').value.trim() || 'anon';
+  let s = await session();
+  if (!s) { await fetch('/api/auth/sign-in/anonymous', { method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body:'{}' }); }
+  await api('/api/me/name', { name });
+  me = await session(); boot();
+}
+async function sendOtp() {
+  await api('/api/auth/phone-number/send-otp', { phoneNumber: $('#phone').value.trim() });
+  $('#code').style.display = 'block'; $('#verify').style.display = 'block'; toast('Code sent (check wrangler console in dev)');
+}
+async function verifyOtp() {
+  await api('/api/auth/phone-number/verify', { phoneNumber: $('#phone').value.trim(), code: $('#code').value.trim() });
+  const name = $('#name').value.trim(); if (name) await api('/api/me/name', { name });
+  me = await session(); boot();
+}
+
+function renderCreate() {
+  document.body.className = '';
+  $('#app').innerHTML = `
+    <h1>New hand</h1>
+    <div class="card">
+      <input id="q" placeholder="Question (e.g. Friday dinner)">
+      <input class="o" placeholder="Option 1"><input class="o" placeholder="Option 2"><input class="o" placeholder="Option 3 (optional)"><input class="o" placeholder="Option 4 (optional)">
+      <input id="con" placeholder="Constraints, comma separated (optional)">
+      <button onclick="createHand()">Open the table</button>
+      <p class="mute">Screenshot intake comes later; type it for now.</p>
+    </div>`;
+}
+async function createHand() {
+  const options = [...document.querySelectorAll('.o')].map(i=>i.value.trim()).filter(Boolean);
+  const constraints = $('#con').value.split(',').map(s=>s.trim()).filter(Boolean);
+  const h = await api('/api/hands', { question: $('#q').value.trim(), options, constraints });
+  location.href = '/h/' + h.id;
+}
+
+function label(id){ return (state.round.options.find(o=>o.id===id)||{}).label || id; }
+function mine(){ return state.round.chips[me.id] || {}; }
+function spent(){ return Object.values(mine()).reduce((a,b)=>a+b,0); }
+function totals(){ const t={}; for (const c of Object.values(state.round.chips)) for (const [o,n] of Object.entries(c)) t[o]=(t[o]||0)+n; return t; }
+
+function renderHand() {
+  const r = state.round, odds = r.odds, live = new Set(state.live);
+  const spice = odds ? odds.spice : 0;
+  document.body.className = state.status==='open' ? (spice>=2 ? 'felt hot' : 'felt') : 'felt';
+  const meIn = state.members.find(m=>m.id===me.id);
+  const left = state.chipsPerMember - spent();
+  const vetoLeft = state.vetoesPerMember - (state.vetoesUsed[me.id]||0);
+  const t = totals();
+  const opts = r.options.map(o => {
+    const dead = !live.has(o.id);
+    const p = odds && odds.probabilities[o.id] != null ? odds.probabilities[o.id] : null;
+    const n = mine()[o.id]||0, others = (t[o.id]||0) - n;
+    return `<div class="opt ${dead?'dead':''} ${o.id===MORE?'more':''}">
+      <div style="flex:1">
+        <div class="label">${o.label}</div>
+        <div class="chips">${'<span class="chip"></span>'.repeat(n)}${'<span class="chip other"></span>'.repeat(Math.max(0,others))}</div>
+      </div>
+      ${state.status==='open' && !dead && meIn && !meIn.locked ? `<div class="stack"><button onclick="place('${o.id}',-1)">−</button><button onclick="place('${o.id}',1)">+</button></div>` : ''}
+      <div class="odds">${p==null?'—':Math.round(p*100)+'%'}</div>
+      ${state.status==='open' && !dead && o.id!==MORE && meIn && !meIn.locked && vetoLeft>0 ? `<button class="small ghost" style="color:#ff8fa3" onclick="veto('${o.id}')">veto</button>` : ''}
+      <div class="bar ${odds && odds.confidence<0.5?'wobble':''}" style="width:${p==null?0:Math.round(p*100)}%"></div>
+    </div>`;
+  }).join('');
+  const members = state.members.map(m=>`<span class="member ${m.locked?'locked':''}">${m.name}${m.locked?' ✓':''}${odds && odds.sandbagging[m.id]>0.6?' 🫥':''}</span>`).join('');
+  const jevLine = odds ? `Jev · ${Math.round(odds.confidence*100)}% sure · ${['mild','warm','hot','volcanic'][Math.round(odds.spice)]} · ${odds.contentious>0.7?'contentious ('+odds.about+')':'calm'} · ${odds.format} · ${odds.ms}ms` : 'Jev is waiting for chips';
+  let footer = '';
+  if (state.status==='open') footer = meIn && !meIn.locked
+    ? `<button class="ghost" onclick="act({op:'lock'})">Lock in (${left} left)</button><button class="lever" onclick="act({op:'pull'})">PULL</button>`
+    : `<button class="lever" onclick="act({op:'pull'})">PULL THE LEVER</button>`;
+  if (state.status==='posted') footer = `<button class="ghost" onclick="act({op:'again'})">Go again</button><button class="ghost" onclick="more()">More options</button><button onclick="act({op:'accept'})">Accept</button>`;
+  if (state.status==='accepted') footer = `<button onclick="location.href='/'">New hand</button>`;
+  $('#app').innerHTML = `
+    <div class="mute">Round ${r.n} · ${state.status}</div>
+    <h1>${state.question}</h1>
+    <div class="members">${members}</div>
+    ${!meIn ? `<button onclick="act({op:'join'})">Sit down as ${me.name}</button>` : ''}
+    <div class="bank">${'<span class="chip"></span>'.repeat(Math.max(0,left))}<span class="mute" style="margin-left:8px">${left} chips · ${vetoLeft} vetoes</span></div>
+    ${opts}
+    <div class="jev"><div class="face"></div><div>${jevLine}</div></div>
+    ${r.pull && state.status!=='open' ? `<div class="card"><b>${label(r.pull.winner)}</b> — ${Math.round(r.pull.odds[r.pull.winner]*100)}% at the pull<br><span class="mute">${r.pull.dealer}</span><br><button class="ghost small" onclick="playReveal(state.round.pull)">replay</button></div>` : ''}
+    ${state.rounds.length>1 ? `<div class="mute">Earlier: ${state.rounds.slice(0,-1).map(x=>x.pull?label(x.pull.winner):'?').join(' → ')}</div>` : ''}
+    <div class="footer"><div class="inner">${footer}</div></div>`;
+}
+
+async function act(body){ await api(`/api/hands/${handId}/act`, body); }
+async function place(o,n){ await act({op:'place', option:o, n}); }
+async function veto(o){ if (confirm('Veto '+label(o)+'? Everyone will see.')) await act({op:'veto', option:o}); }
+async function more(){ const l = prompt('Options agent not wired yet — type new options, comma separated'); if (!l) return; await act({op:'more', labels: l.split(',').map(s=>s.trim()).filter(Boolean)}); }
+
+// ---------- reveal: reels with the near-miss ----------
+function playReveal(pull) {
+  const el = $('#reveal'), body = $('#reveal-body');
+  el.classList.add('on'); document.body.classList.add('crt');
+  const seq = pull.order; // ..., runner-up, winner
+  const pw = pull.odds[pull.winner];
+  let i = 0;
+  body.innerHTML = `<div class="mute">The lever has spoken</div><div class="reel-frame"><div class="reel" id="reel">…</div></div>`;
+  const reel = $('#reel');
+  const step = () => {
+    if (i < seq.length) {
+      reel.textContent = label(seq[i]);
+      const isRunnerUp = seq[i]===pull.runnerUp && i===seq.length-2;
+      const delay = i===seq.length-1 ? 0 : isRunnerUp ? 900 : Math.max(120, 420 - i*60); // hang on the runner-up: the near-miss
+      if (isRunnerUp) reel.classList.add('shake');
+      i++; setTimeout(step, delay);
+    } else {
+      body.innerHTML = `${pw<0.3?'<div class="upset">UPSET</div>':''}<div class="mute">${state.question}</div><div class="winner">${label(pull.winner)}</div>
+        <div class="mute">${Math.round(pw*100)}% at the pull</div><div class="dealer">${pull.dealer}</div>
+        <div class="odds-list">${Object.entries(pull.odds).sort((a,b)=>b[1]-a[1]).map(([k,v])=>label(k)+' '+Math.round(v*100)+'%').join(' · ')}</div>
+        <button class="ghost" style="margin-top:20px;width:auto" onclick="closeReveal()">ok</button>`;
+      if (navigator.vibrate) navigator.vibrate([30,40,30,40,120]);
+    }
+  };
+  setTimeout(step, 600);
+}
+function closeReveal(){ $('#reveal').classList.remove('on'); document.body.classList.remove('crt'); render(); }
+
+// ---------- socket ----------
+function connect() {
+  const proto = location.protocol==='https:' ? 'wss' : 'ws';
+  ws = new WebSocket(`${proto}://${location.host}/api/hands/${handId}/ws`);
+  ws.onmessage = ev => {
+    if (ev.data==='pong') return;
+    const m = JSON.parse(ev.data);
+    if (m.type==='state') {
+      const prevPull = state && state.round && state.round.pull ? state.round.pull.at : null;
+      state = m.state;
+      const p = state.round.pull;
+      if (p && p.at !== prevPull && p.at !== revealed) { revealed = p.at; playReveal(p); return; }
+      render();
+    }
+    if (m.type==='odds') { if (state) { state.round.odds = m.odds; render(); } }
+    if (m.type==='error') toast(m.error.slice(0,120));
+  };
+  ws.onclose = () => setTimeout(connect, 1500);
+  setInterval(()=>{ try{ ws.send('ping'); }catch{} }, 25000);
+}
+
+async function boot() {
+  me = await session();
+  if (me && handId) {
+    state = await api(`/api/hands/${handId}`);
+    if (!state.members.find(m=>m.id===me.id)) await act({op:'join'});
+    if (!ws) connect();
+  }
+  render();
+}
+boot();
+</script>
+</body>
+</html>

+ 39 - 0
scripts/drive.sh

@@ -0,0 +1,39 @@
+set -e
+B=localhost:8787
+j(){ curl -s -b "$1" -c "$1" -H 'Content-Type: application/json' "${@:2}"; }
+# two members: anonymous sign-in, set names
+j /tmp/tom -X POST $B/api/auth/sign-in/anonymous -d '{}' >/dev/null; j /tmp/tom -X POST $B/api/me/name -d '{"name":"Tom"}' >/dev/null
+j /tmp/sam -X POST $B/api/auth/sign-in/anonymous -d '{}' >/dev/null; j /tmp/sam -X POST $B/api/me/name -d '{"name":"Sam"}' >/dev/null
+# phone OTP path smoke test
+j /tmp/phone -X POST $B/api/auth/phone-number/send-otp -d '{"phoneNumber":"+61400000000"}'; echo " <- send-otp"
+H=$(j /tmp/tom -X POST $B/api/hands -d '{"question":"Friday dinner","options":["Thai","Pizza","Ramen","Cook at home"],"constraints":["Sam is trying to spend less this month"]}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
+echo "hand $H"
+A(){ j $1 -X POST $B/api/hands/$H/act -d "$2" >/dev/null; }
+A /tmp/tom '{"op":"join"}'; A /tmp/sam '{"op":"join"}'
+A /tmp/tom '{"op":"place","option":"o1","n":6}'; A /tmp/tom '{"op":"place","option":"o3","n":4}'
+A /tmp/sam '{"op":"place","option":"o2","n":8}'; A /tmp/sam '{"op":"place","option":"o4","n":2}'
+A /tmp/tom '{"op":"veto","option":"o2"}'          # Tom vetoes pizza; Sam's 8 chips come back
+sleep 1.2
+curl -s $B/api/hands/$H | python3 -c '
+import sys,json; s=json.load(sys.stdin); r=s["round"]
+print("members",[m["name"] for m in s["members"]],"live",s["live"],"vetoed",r["vetoed"])
+print("chips",r["chips"])
+o=r["odds"]; print("odds", {k:round(v,2) for k,v in o["probabilities"].items()}, "conf",round(o["confidence"],2),"spice",round(o["spice"],2),"format",o["format"],"about",o["about"], o["ms"],"ms")'
+A /tmp/sam '{"op":"place","option":"o4","n":8}'   # Sam goes all in on cooking
+sleep 0.6
+A /tmp/tom '{"op":"lock"}'; A /tmp/sam '{"op":"lock"}'   # both locked -> lever pulls
+sleep 1.5
+curl -s $B/api/hands/$H | python3 -c '
+import sys,json; s=json.load(sys.stdin); r=s["round"]; p=r["pull"]
+lab=lambda i: next(o["label"] for o in r["options"] if o["id"]==i)
+print("status",s["status"]); print("odds at pull",{lab(k):round(v,2) for k,v in p["odds"].items()})
+print("WINNER",lab(p["winner"]),"| runner-up",lab(p["runnerUp"]),"| reel order",[lab(x) for x in p["order"]],"| seed",p["seed"])
+print("dealer:",p["dealer"])'
+A /tmp/tom '{"op":"again"}'; sleep 0.3
+A /tmp/tom '{"op":"place","option":"o1","n":10}'; A /tmp/sam '{"op":"place","option":"o1","n":10}'; sleep 1
+A /tmp/tom '{"op":"pull"}'; sleep 1.5
+A /tmp/tom '{"op":"accept"}'; A /tmp/sam '{"op":"accept"}'
+curl -s $B/api/hands/$H | python3 -c '
+import sys,json; s=json.load(sys.stdin); r=s["round"]; p=r["pull"]
+lab=lambda i: next(o["label"] for o in r["options"] if o["id"]==i)
+print("round",r["n"],"status",s["status"],"winner",lab(p["winner"]),{lab(k):round(v,2) for k,v in p["odds"].items()},"format",r["odds"]["format"]); print("dealer:",p["dealer"])'

+ 60 - 0
src/auth/index.ts

@@ -0,0 +1,60 @@
+type IncomingRequestCfProperties = Record<string, unknown>;
+import { betterAuth } from "better-auth";
+import { withCloudflare } from "better-auth-cloudflare";
+import { anonymous, phoneNumber } from "better-auth/plugins";
+import { drizzleAdapter } from "@better-auth/drizzle-adapter";
+import { drizzle } from "drizzle-orm/d1";
+import { schema } from "../db";
+import type { CloudflareBindings } from "../env";
+
+// Identity = phone number. OTP over SMS in prod; logged to the console in dev.
+// Anonymous stays enabled as the zero-friction path for loop prototyping.
+async function sendOTP(env: CloudflareBindings | undefined, phone: string, code: string) {
+    if (!env || env.OTP_MODE !== "twilio") {
+        console.log(`[OTP] ${phone} -> ${code}`);
+        return;
+    }
+    const body = new URLSearchParams({ To: phone, From: env.TWILIO_FROM!, Body: `Your Plan Maker code is ${code}` });
+    await fetch(`https://api.twilio.com/2010-04-01/Accounts/${env.TWILIO_ACCOUNT_SID}/Messages.json`, {
+        method: "POST",
+        headers: { Authorization: "Basic " + btoa(`${env.TWILIO_ACCOUNT_SID}:${env.TWILIO_AUTH_TOKEN}`) },
+        body,
+    });
+}
+
+function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties, baseURL?: string) {
+    const db = env ? drizzle(env.DATABASE, { schema }) : ({} as any);
+
+    return betterAuth({
+        baseURL,
+        secret: env?.BETTER_AUTH_SECRET,
+        ...withCloudflare(
+            {
+                autoDetectIpAddress: true,
+                geolocationTracking: false,
+                cf: cf || {},
+                d1: env ? { db, options: { usePlural: true } } : undefined,
+            },
+            {
+                emailAndPassword: { enabled: false },
+                plugins: [
+                    anonymous(),
+                    phoneNumber({
+                        sendOTP: ({ phoneNumber, code }) => sendOTP(env, phoneNumber, code),
+                        otpLength: 6,
+                        expiresIn: 300,
+                        signUpOnVerification: {
+                            getTempEmail: phone => `${phone.replace(/\D/g, "")}@phone.pm2000.local`,
+                            getTempName: phone => phone,
+                        },
+                    }),
+                ],
+                rateLimit: { enabled: true, window: 60, max: 100 },
+            }
+        ),
+        ...(env ? {} : { database: drizzleAdapter({} as any, { provider: "sqlite", usePlural: true }) }),
+    });
+}
+
+export const auth = createAuth();
+export { createAuth };

+ 108 - 0
src/db/auth.schema.ts

@@ -0,0 +1,108 @@
+import { relations, sql } from "drizzle-orm";
+import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
+
+export const users = sqliteTable("users", {
+    id: text("id").primaryKey(),
+    name: text("name").notNull(),
+    email: text("email").notNull().unique(),
+    emailVerified: integer("email_verified", { mode: "boolean" }).default(false).notNull(),
+    image: text("image"),
+    createdAt: integer("created_at", { mode: "timestamp_ms" })
+        .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
+        .notNull(),
+    updatedAt: integer("updated_at", { mode: "timestamp_ms" })
+        .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
+        .$onUpdate(() => /* @__PURE__ */ new Date())
+        .notNull(),
+    isAnonymous: integer("is_anonymous", { mode: "boolean" }).default(false),
+    phoneNumber: text("phone_number").unique(),
+    phoneNumberVerified: integer("phone_number_verified", { mode: "boolean" }),
+});
+
+export const sessions = sqliteTable(
+    "sessions",
+    {
+        id: text("id").primaryKey(),
+        expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
+        token: text("token").notNull().unique(),
+        createdAt: integer("created_at", { mode: "timestamp_ms" })
+            .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
+            .notNull(),
+        updatedAt: integer("updated_at", { mode: "timestamp_ms" })
+            .$onUpdate(() => /* @__PURE__ */ new Date())
+            .notNull(),
+        ipAddress: text("ip_address"),
+        userAgent: text("user_agent"),
+        userId: text("user_id")
+            .notNull()
+            .references(() => users.id, { onDelete: "cascade" }),
+    },
+    table => [index("sessions_userId_idx").on(table.userId)]
+);
+
+export const accounts = sqliteTable(
+    "accounts",
+    {
+        id: text("id").primaryKey(),
+        accountId: text("account_id").notNull(),
+        providerId: text("provider_id").notNull(),
+        userId: text("user_id")
+            .notNull()
+            .references(() => users.id, { onDelete: "cascade" }),
+        accessToken: text("access_token"),
+        refreshToken: text("refresh_token"),
+        idToken: text("id_token"),
+        accessTokenExpiresAt: integer("access_token_expires_at", {
+            mode: "timestamp_ms",
+        }),
+        refreshTokenExpiresAt: integer("refresh_token_expires_at", {
+            mode: "timestamp_ms",
+        }),
+        scope: text("scope"),
+        password: text("password"),
+        createdAt: integer("created_at", { mode: "timestamp_ms" })
+            .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
+            .notNull(),
+        updatedAt: integer("updated_at", { mode: "timestamp_ms" })
+            .$onUpdate(() => /* @__PURE__ */ new Date())
+            .notNull(),
+    },
+    table => [index("accounts_userId_idx").on(table.userId)]
+);
+
+export const verifications = sqliteTable(
+    "verifications",
+    {
+        id: text("id").primaryKey(),
+        identifier: text("identifier").notNull(),
+        value: text("value").notNull(),
+        expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
+        createdAt: integer("created_at", { mode: "timestamp_ms" })
+            .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
+            .notNull(),
+        updatedAt: integer("updated_at", { mode: "timestamp_ms" })
+            .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
+            .$onUpdate(() => /* @__PURE__ */ new Date())
+            .notNull(),
+    },
+    table => [index("verifications_identifier_idx").on(table.identifier)]
+);
+
+export const usersRelations = relations(users, ({ many }) => ({
+    sessions: many(sessions),
+    accounts: many(accounts),
+}));
+
+export const sessionsRelations = relations(sessions, ({ one }) => ({
+    user: one(users, {
+        fields: [sessions.userId],
+        references: [users.id],
+    }),
+}));
+
+export const accountsRelations = relations(accounts, ({ one }) => ({
+    user: one(users, {
+        fields: [accounts.userId],
+        references: [users.id],
+    }),
+}));

+ 8 - 0
src/db/index.ts

@@ -0,0 +1,8 @@
+import { schema } from "./schema";
+
+// Re-export the drizzle-orm types and utilities from here for convenience
+export * from "drizzle-orm";
+
+// Re-export the feature schemas for use in other files
+export * from "./auth.schema"; // Export individual tables for drizzle-kit
+export * from "./schema";

+ 7 - 0
src/db/schema.ts

@@ -0,0 +1,7 @@
+import * as authSchema from "./auth.schema"; // This will be generated in a later step
+
+// Combine all schemas here for migrations
+export const schema = {
+    ...authSchema,
+    // ... your other application schemas
+} as const;

+ 25 - 0
src/dealer.ts

@@ -0,0 +1,25 @@
+// Human-authored dealer lines. Jev picks; it never writes. Keys are the tag Jev sees; values are what the table sees.
+export const DEALER_LINES: Record<string, string> = {
+    "favourite won, smug": "The lever has spoken. It was always going to.",
+    "favourite won, unanimous": "Unanimous. Boring. Go eat.",
+    "upset, winner under 30%": "UPSET. Nobody saw that coming. Especially not Jev.",
+    "upset, winner backed by one person": "One person. One dream. The lever respects conviction.",
+    "close call, near coin flip": "That was a coin flip with extra steps.",
+    "loser has lost several in a row": "Better luck next time. Again.",
+    "same option won as last time": "Same again. Creatures of habit.",
+    "more options won, nobody happy": "None of the above. The agent's on its way.",
+    "two person table, close": "Couples counselling is cheaper than this. Marginally.",
+    "someone vetoed the runner-up": "That veto did some heavy lifting.",
+    "late night hand": "It's late. The lever doesn't care.",
+    "winner had the most chips by far": "Chips talk. Loudly.",
+    "winner had low confidence odds": "Jev wasn't sure either. Nobody was.",
+    "quick hand, resolved in one round": "In and out. Efficient.",
+    "long hand, many rounds": "Three rounds. Was it worth it? The lever thinks so.",
+    "one member abstained": "Someone didn't show. Their loss.",
+    "constraints respected, cheap option won": "Wallet-friendly. Sam will be pleased.",
+    "constraints ignored, expensive option won": "Expensive. Somebody's shouting.",
+    "generic cheerful": "Ding ding ding. We have a winner.",
+    "generic deadpan": "Decided.",
+    "generic mock sympathy": "Oh no. Anyway.",
+    "generic hype": "LET'S GO.",
+};

+ 8 - 0
src/env.d.ts

@@ -0,0 +1,8 @@
+// Bindings come from worker-configuration.d.ts (run `npm run cf-typegen`). Secrets from .dev.vars / wrangler secret.
+export type CloudflareBindings = Env & {
+    TYPESAFE_API_KEY: string;
+    BETTER_AUTH_SECRET: string;
+    TWILIO_ACCOUNT_SID?: string;
+    TWILIO_AUTH_TOKEN?: string;
+    TWILIO_FROM?: string;
+};

+ 416 - 0
src/hand.ts

@@ -0,0 +1,416 @@
+// One Durable Object per hand. Holds the round state, fans out over WebSocket,
+// recomputes Jev odds on every state change (debounced), and pulls the lever.
+import { DurableObject } from "cloudflare:workers";
+import { jev, type Choice } from "./jev";
+import { DEALER_LINES } from "./dealer";
+import type { CloudflareBindings } from "./env";
+
+export type Member = { id: string; name: string; locked: boolean };
+export type Option = { id: string; label: string };
+export type Chips = Record<string, Record<string, number>>; // memberId -> optionId -> count
+
+export type Odds = {
+    probabilities: Record<string, number>;
+    confidence: number;
+    contentious: number;
+    about: string;
+    spice: number; // 0..3 mild..volcanic
+    format: string;
+    sandbagging: Record<string, number>;
+    ms: number;
+    at: number;
+};
+
+export type Pull = {
+    seed: number;
+    odds: Record<string, number>;
+    winner: string;
+    runnerUp: string;
+    order: string[]; // reel order for the animation: runner-up is last before winner
+    dealer: string;
+    at: number;
+};
+
+export type Round = {
+    n: number;
+    options: Option[];
+    vetoed: { by: string; option: string }[];
+    chips: Chips;
+    odds: Odds | null;
+    pull: Pull | null;
+    openedAt: number;
+};
+
+export type HandState = {
+    id: string;
+    question: string;
+    status: "draft" | "open" | "pulled" | "posted" | "accepted" | "expired";
+    members: Member[];
+    rounds: Round[];
+    constraints: string[];
+    accepts: string[];
+    chipsPerMember: number;
+    vetoesPerMember: number;
+    createdAt: number;
+};
+
+const MORE = "more_options";
+
+function mulberry32(seed: number) {
+    return () => {
+        seed |= 0;
+        seed = (seed + 0x6d2b79f5) | 0;
+        let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
+        t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
+        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+    };
+}
+
+export class Hand extends DurableObject<CloudflareBindings> {
+    private state: HandState | null = null;
+    private oddsTimer: ReturnType<typeof setTimeout> | null = null;
+    private oddsPending = false;
+
+    private async load(): Promise<HandState | null> {
+        if (!this.state) this.state = (await this.ctx.storage.get<HandState>("hand")) ?? null;
+        return this.state;
+    }
+    private async save() {
+        if (this.state) await this.ctx.storage.put("hand", this.state);
+    }
+
+    // ---- HTTP entry (from the Worker) ----
+    async fetch(req: Request): Promise<Response> {
+        const url = new URL(req.url);
+        if (url.pathname.endsWith("/ws")) {
+            const pair = new WebSocketPair();
+            this.ctx.acceptWebSocket(pair[1]);
+            const s = await this.load();
+            if (s) pair[1].send(JSON.stringify({ type: "state", state: this.publicState(s) }));
+            return new Response(null, { status: 101, webSocket: pair[0] });
+        }
+        if (url.pathname.endsWith("/init") && req.method === "POST") {
+            const body = (await req.json()) as { id: string; question: string; options: string[]; constraints?: string[] };
+            this.state = {
+                id: body.id,
+                question: body.question,
+                status: "open",
+                members: [],
+                rounds: [this.newRound(1, body.options.map((label, i) => ({ id: `o${i + 1}`, label })))],
+                constraints: body.constraints ?? [],
+                accepts: [],
+                chipsPerMember: 10,
+                vetoesPerMember: 2,
+                createdAt: Date.now(),
+            };
+            await this.save();
+            return Response.json(this.publicState(this.state));
+        }
+        if (url.pathname.endsWith("/state")) {
+            const s = await this.load();
+            return s ? Response.json(this.publicState(s)) : new Response("not found", { status: 404 });
+        }
+        if (url.pathname.endsWith("/act") && req.method === "POST") {
+            const body = (await req.json()) as { member: { id: string; name: string }; op: string; option?: string; n?: number };
+            const s = await this.load();
+            if (!s) return new Response("not found", { status: 404 });
+            const err = await this.act(s, body);
+            if (err) return new Response(err, { status: 400 });
+            return Response.json(this.publicState(s));
+        }
+        return new Response("no", { status: 404 });
+    }
+
+    private newRound(n: number, options: Option[]): Round {
+        return { n, options: [...options, { id: MORE, label: "More options" }], vetoed: [], chips: {}, odds: null, pull: null, openedAt: Date.now() };
+    }
+
+    private round(s: HandState) {
+        return s.rounds[s.rounds.length - 1];
+    }
+    private liveOptions(r: Round) {
+        const dead = new Set(r.vetoed.map(v => v.option));
+        return r.options.filter(o => !dead.has(o.id));
+    }
+    private spent(r: Round, memberId: string) {
+        return Object.values(r.chips[memberId] ?? {}).reduce((a, b) => a + b, 0);
+    }
+    private vetoesUsed(s: HandState, memberId: string) {
+        return s.rounds.flatMap(r => r.vetoed).filter(v => v.by === memberId).length;
+    }
+
+    // ---- actions ----
+    private async act(s: HandState, a: { member: { id: string; name: string }; op: string; option?: string; n?: number }): Promise<string | null> {
+        const r = this.round(s);
+        const m = s.members.find(x => x.id === a.member.id);
+        switch (a.op) {
+            case "join": {
+                if (!m) s.members.push({ id: a.member.id, name: a.member.name, locked: false });
+                else m.name = a.member.name;
+                break;
+            }
+            case "place": {
+                if (!m || s.status !== "open" || m.locked) return "can't place";
+                const opt = this.liveOptions(r).find(o => o.id === a.option);
+                if (!opt) return "no such option";
+                const n = a.n ?? 1;
+                const mine = (r.chips[m.id] ??= {});
+                const cur = mine[opt.id] ?? 0;
+                if (n > 0 && this.spent(r, m.id) + n > s.chipsPerMember) return "no chips left";
+                if (cur + n < 0) return "none there";
+                mine[opt.id] = cur + n;
+                if (mine[opt.id] === 0) delete mine[opt.id];
+                this.scheduleOdds();
+                break;
+            }
+            case "veto": {
+                if (!m || s.status !== "open") return "can't veto";
+                if (a.option === MORE) return "can't veto that";
+                if (this.vetoesUsed(s, m.id) >= s.vetoesPerMember) return "no vetoes left";
+                const live = this.liveOptions(r).filter(o => o.id !== MORE);
+                if (live.length <= 2) return "minimum two options";
+                if (!live.find(o => o.id === a.option)) return "no such option";
+                r.vetoed.push({ by: m.id, option: a.option! });
+                for (const c of Object.values(r.chips)) delete c[a.option!]; // chips on it return to their owners
+                this.scheduleOdds();
+                break;
+            }
+            case "lock": {
+                if (!m || s.status !== "open") return "can't lock";
+                m.locked = true;
+                if (s.members.every(x => x.locked)) await this.pull(s);
+                break;
+            }
+            case "pull": {
+                if (s.status !== "open") return "not open";
+                await this.pull(s);
+                break;
+            }
+            case "again": {
+                if (s.status !== "posted") return "nothing posted";
+                const live = this.liveOptions(r).filter(o => o.id !== MORE);
+                s.rounds.push(this.newRound(r.n + 1, live));
+                s.members.forEach(x => (x.locked = false));
+                s.accepts = [];
+                s.status = "open";
+                this.scheduleOdds();
+                break;
+            }
+            case "more": {
+                // Options agent hook: for now the client supplies new labels. Later: vision/LLM regen.
+                if (s.status !== "posted") return "nothing posted";
+                if (s.rounds.length >= 3) return "regen cap";
+                const labels = ((a as any).labels as string[] | undefined) ?? [];
+                const live = this.liveOptions(r).filter(o => o.id !== MORE);
+                const fresh = labels.map((label, i) => ({ id: `o${r.n + 1}_${i + 1}`, label }));
+                s.rounds.push(this.newRound(r.n + 1, [...live, ...fresh].slice(0, 4)));
+                s.members.forEach(x => (x.locked = false));
+                s.accepts = [];
+                s.status = "open";
+                this.scheduleOdds();
+                break;
+            }
+            case "accept": {
+                if (!m || s.status !== "posted") return "nothing posted";
+                if (!s.accepts.includes(m.id)) s.accepts.push(m.id);
+                if (s.accepts.length * 2 > s.members.length) s.status = "accepted";
+                break;
+            }
+            default:
+                return "unknown op";
+        }
+        await this.save();
+        this.broadcast({ type: "state", state: this.publicState(s) });
+        return null;
+    }
+
+    // ---- Jev odds, debounced 200 ms ----
+    private scheduleOdds() {
+        this.oddsPending = true;
+        if (this.oddsTimer) return;
+        this.oddsTimer = setTimeout(() => {
+            this.oddsTimer = null;
+            if (this.oddsPending) {
+                this.oddsPending = false;
+                this.ctx.waitUntil(this.recomputeOdds());
+            }
+        }, 200);
+    }
+
+    private features(s: HandState, r: Round) {
+        const live = this.liveOptions(r);
+        const totals: Record<string, number> = {};
+        for (const c of Object.values(r.chips)) for (const [o, n] of Object.entries(c)) totals[o] = (totals[o] ?? 0) + n;
+        const placed = Object.values(totals).reduce((a, b) => a + b, 0);
+        const top = Math.max(0, ...Object.values(totals));
+        const agreement = placed === 0 ? "nothing placed" : top / placed > 0.85 ? "unanimous" : top / placed > 0.6 ? "leaning" : "split";
+        return { live, totals, placed, agreement, liveCount: live.filter(o => o.id !== MORE).length };
+    }
+
+    async recomputeOdds() {
+        const s = await this.load();
+        if (!s || s.status !== "open") return;
+        const r = this.round(s);
+        const f = this.features(s, r);
+        const nameOf = (id: string) => s.members.find(m => m.id === id)?.name ?? id;
+        const chipsByName: Record<string, Record<string, number>> = {};
+        for (const [mid, c] of Object.entries(r.chips)) {
+            chipsByName[nameOf(mid)] = Object.fromEntries(Object.entries(c).map(([o, n]) => [r.options.find(x => x.id === o)?.label ?? o, n]));
+        }
+        const state = {
+            question: s.question,
+            options: f.live.map(o => o.label),
+            yes_chips: chipsByName,
+            chips_per_member: s.chipsPerMember,
+            members_not_yet_placed: s.members.filter(m => !r.chips[m.id]).map(m => m.name),
+            vetoes: r.vetoed.map(v => ({ by: nameOf(v.by), option: r.options.find(x => x.id === v.option)?.label })),
+            constraints: s.constraints,
+            previous_rounds: s.rounds.slice(0, -1).map(pr => ({
+                n: pr.n,
+                posted: pr.pull ? r.options.find(x => x.id === pr.pull!.winner)?.label ?? pr.pull.winner : null,
+            })),
+            live_option_count: f.liveCount,
+            chip_agreement: f.agreement,
+            round_number: r.n,
+        };
+        const criteria: Record<string, string | null> = {};
+        for (const o of f.live) criteria[o.id] = o.id === MORE ? "Nobody is happy with what's here; generate new options" : o.label;
+        const sandbag: Record<string, Choice | any> = {};
+        for (const m of s.members) {
+            sandbag[`sb_${m.id}`] = { type: "noul", instructions: `Is ${m.name} spreading chips thinly to avoid committing?` };
+        }
+        try {
+            const res = await jev(this.env.TYPESAFE_API_KEY, state, {
+                pick: {
+                    type: "choice",
+                    instructions: this.env.PICK_INSTRUCTIONS ?? "Which option should this table go with? Yes chips are votes and weigh heavily. Respect stated constraints.",
+                    criteria,
+                },
+                contentious: { type: "noul", instructions: "Is this round contentious — are members split with strong preferences?" },
+                about: {
+                    type: "choice",
+                    instructions: "What is the disagreement mainly about?",
+                    criteria: { cost: null, timing: null, distance: null, people: null, taste: null, none: "No real disagreement" },
+                },
+                spice: { type: "score", instructions: "How heated is this round?", criteria: ["mild", "warm", "hot", "volcanic"] },
+                format: {
+                    type: "choice",
+                    instructions: "Pick the round format. Use the smallest format that fits.",
+                    criteria: {
+                        one_tap: "chip_agreement is 'unanimous' — everyone backs the same option",
+                        swipe: "live_option_count is exactly 2",
+                        craps: "live_option_count is 3 or more and chips are split or leaning",
+                        reels: "the table is fighting — contentious and heated",
+                    },
+                },
+                ...sandbag,
+            } as any);
+            const a: any = res.answers;
+            const sandbagging: Record<string, number> = {};
+            for (const m of s.members) sandbagging[m.id] = a[`sb_${m.id}`]?.noul ?? 0;
+            r.odds = {
+                probabilities: a.pick.probabilities,
+                confidence: a.pick.confidence,
+                contentious: a.contentious.noul,
+                about: a.about.choice,
+                spice: a.spice.score,
+                format: a.format.choice,
+                sandbagging,
+                ms: res.ms,
+                at: Date.now(),
+            };
+            await this.save();
+            this.broadcast({ type: "odds", odds: r.odds });
+        } catch (e) {
+            this.broadcast({ type: "error", error: String(e) });
+        }
+    }
+
+    // ---- the lever ----
+    private async pull(s: HandState) {
+        const r = this.round(s);
+        if (!r.odds || Date.now() - r.odds.at > 5000 || this.oddsPending) await this.recomputeOdds();
+        const p = r.odds?.probabilities;
+        const live = this.liveOptions(r);
+        // Fallback to chip proportions if Jev is unavailable
+        const weights: Record<string, number> = {};
+        if (p) for (const o of live) weights[o.id] = p[o.id] ?? 0;
+        else {
+            const f = this.features(s, r);
+            for (const o of live) weights[o.id] = (f.totals[o.id] ?? 0) + 0.01;
+        }
+        const total = Object.values(weights).reduce((a, b) => a + b, 0) || 1;
+        const seed = (Math.random() * 2 ** 32) >>> 0;
+        const rand = mulberry32(seed);
+        let u = rand() * total;
+        let winner = live[0].id;
+        for (const o of live) {
+            u -= weights[o.id];
+            if (u <= 0) {
+                winner = o.id;
+                break;
+            }
+        }
+        const ranked = [...live].sort((a, b) => weights[b.id] - weights[a.id]).map(o => o.id);
+        const runnerUp = ranked.find(id => id !== winner) ?? winner;
+        const rest = ranked.filter(id => id !== winner && id !== runnerUp);
+        const order = [...rest, runnerUp, winner]; // near-miss: runner-up is always the last card before the winner
+        const dealer = await this.dealerLine(s, r, winner, weights, total);
+        r.pull = { seed, odds: Object.fromEntries(Object.entries(weights).map(([k, v]) => [k, v / total])), winner, runnerUp, order, dealer, at: Date.now() };
+        s.status = winner === MORE ? "posted" : "posted";
+        await this.save();
+    }
+
+    private async dealerLine(s: HandState, r: Round, winner: string, w: Record<string, number>, total: number) {
+        const pw = (w[winner] ?? 0) / total;
+        const label = (id: string) => r.options.find(o => o.id === id)?.label ?? id;
+        const backers = Object.entries(r.chips).filter(([, c]) => (c[winner] ?? 0) > 0).length;
+        const state = {
+            question: s.question,
+            winner: label(winner),
+            winner_probability: Number(pw.toFixed(2)),
+            winner_was_favourite: pw >= 0.5,
+            winner_was_upset: pw < 0.3,
+            members_backing_winner: backers,
+            member_count: s.members.length,
+            two_person_table: s.members.length === 2,
+            round_number: r.n,
+            more_options_won: winner === MORE,
+            odds_confidence: r.odds?.confidence ?? null,
+            vetoes_this_round: r.vetoed.length,
+            constraints: s.constraints,
+            hour_local: new Date().getUTCHours(),
+        };
+        try {
+            const res = await jev(this.env.TYPESAFE_API_KEY, state, {
+                line: { type: "choice", instructions: "Which dealer line fits this outcome?", criteria: Object.fromEntries(Object.keys(DEALER_LINES).map(k => [k, null])) },
+            });
+            return DEALER_LINES[res.answers.line.choice] ?? DEALER_LINES["generic deadpan"];
+        } catch {
+            return DEALER_LINES["generic deadpan"];
+        }
+    }
+
+    // ---- fan-out ----
+    private broadcast(msg: unknown) {
+        const data = JSON.stringify(msg);
+        for (const ws of this.ctx.getWebSockets()) {
+            try {
+                ws.send(data);
+            } catch {}
+        }
+    }
+    async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
+        if (message === "ping") ws.send("pong");
+    }
+    async webSocketClose(ws: WebSocket) {
+        try {
+            ws.close();
+        } catch {}
+    }
+
+    publicState(s: HandState) {
+        const r = this.round(s);
+        return { ...s, round: r, live: this.liveOptions(r).map(o => o.id), vetoesUsed: Object.fromEntries(s.members.map(m => [m.id, this.vetoesUsed(s, m.id)])) };
+    }
+}

+ 63 - 0
src/index.ts

@@ -0,0 +1,63 @@
+import { Hono } from "hono";
+import { createAuth } from "./auth";
+import type { CloudflareBindings } from "./env";
+export { Hand } from "./hand";
+
+type Variables = { auth: ReturnType<typeof createAuth>; me: { id: string; name: string } | null };
+const app = new Hono<{ Bindings: CloudflareBindings; Variables: Variables }>();
+
+app.use("*", async (c, next) => {
+    const auth = createAuth(c.env, (c.req.raw as any).cf || {}, new URL(c.req.url).origin);
+    c.set("auth", auth);
+    const session = await auth.api.getSession({ headers: c.req.raw.headers }).catch(() => null);
+    c.set("me", session ? { id: session.user.id, name: session.user.name || "anon" } : null);
+    await next();
+});
+
+app.all("/api/auth/*", c => c.get("auth").handler(c.req.raw));
+
+// Name is per-table display name; stored on the user for now.
+app.post("/api/me/name", async c => {
+    const me = c.get("me");
+    if (!me) return c.text("sign in first", 401);
+    const { name } = (await c.req.json()) as { name: string };
+    await c.get("auth").api.updateUser({ headers: c.req.raw.headers, body: { name } });
+    return c.json({ ok: true });
+});
+
+// Create a hand (draft -> open in one step for the prototype)
+app.post("/api/hands", async c => {
+    const me = c.get("me");
+    if (!me) return c.text("sign in first", 401);
+    const body = (await c.req.json()) as { question: string; options: string[]; constraints?: string[] };
+    if (!body.question || !body.options || body.options.length < 2 || body.options.length > 4) return c.text("2–4 options", 400);
+    const id = crypto.randomUUID().slice(0, 8);
+    const stub = c.env.HAND.get(c.env.HAND.idFromName(id));
+    const res = await stub.fetch("https://hand/init", { method: "POST", body: JSON.stringify({ id, ...body }) });
+    return c.json(await res.json());
+});
+
+app.get("/api/hands/:id", async c => {
+    const stub = c.env.HAND.get(c.env.HAND.idFromName(c.req.param("id")));
+    return stub.fetch("https://hand/state");
+});
+
+app.post("/api/hands/:id/act", async c => {
+    const me = c.get("me");
+    if (!me) return c.text("sign in first", 401);
+    const body = await c.req.json();
+    const stub = c.env.HAND.get(c.env.HAND.idFromName(c.req.param("id")));
+    return stub.fetch("https://hand/act", { method: "POST", body: JSON.stringify({ ...body, member: me }) });
+});
+
+app.get("/api/hands/:id/ws", async c => {
+    const stub = c.env.HAND.get(c.env.HAND.idFromName(c.req.param("id")));
+    return stub.fetch("https://hand/ws", { headers: c.req.raw.headers });
+});
+
+app.get("/health", c => c.json({ ok: true, at: new Date().toISOString() }));
+
+// Mobile web client: same page for / and /h/:id
+app.get("/h/:id", c => c.env.ASSETS.fetch(new Request(new URL("/index.html", c.req.url))));
+
+export default app;

+ 47 - 0
src/jev.ts

@@ -0,0 +1,47 @@
+// Jev client — TypeSafe System One API. One call, many typed questions, calibrated probabilities.
+export type Noul = { type: "noul"; instructions: string; criteria?: { true?: string; false?: string } };
+export type Choice = { type: "choice"; instructions: string; criteria: Record<string, string | null> };
+export type Score = { type: "score"; instructions: string; criteria: string[] };
+export type Question = Noul | Choice | Score;
+
+export type NoulAnswer = { type: "noul"; noul: number };
+export type ChoiceAnswer = { type: "choice"; choice: string; probabilities: Record<string, number>; confidence: number };
+export type ScoreAnswer = { type: "score"; score: number; legend: Record<string, string>; probabilities: Record<string, number>; confidence: number };
+export type Answer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
+
+export type JevResponse<Q extends Record<string, Question>> = {
+    model: string;
+    answers: { [K in keyof Q]: Q[K] extends Noul ? NoulAnswer : Q[K] extends Choice ? ChoiceAnswer : ScoreAnswer };
+    usage: { input_tokens: number; output_tokens: number };
+    ms: number;
+};
+
+export async function jev<Q extends Record<string, Question>>(
+    apiKey: string,
+    state: unknown,
+    questions: Q,
+    opts: { model?: string; retries?: number } = {}
+): Promise<JevResponse<Q>> {
+    const model = opts.model ?? "jev-latest";
+    const retries = opts.retries ?? 2;
+    let lastErr: unknown;
+    for (let attempt = 0; attempt <= retries; attempt++) {
+        const t = Date.now();
+        const res = await fetch("https://api.typesafe.ai/v1/systemone", {
+            method: "POST",
+            headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
+            body: JSON.stringify({ state, model, questions }),
+        });
+        if (res.ok) {
+            const body = (await res.json()) as Omit<JevResponse<Q>, "ms">;
+            return { ...body, ms: Date.now() - t };
+        }
+        lastErr = new Error(`jev ${res.status}: ${await res.text()}`);
+        if (res.status === 429 || res.status === 529) {
+            await new Promise(r => setTimeout(r, 150 * 2 ** attempt));
+            continue;
+        }
+        throw lastErr;
+    }
+    throw lastErr;
+}

+ 14 - 0
tsconfig.json

@@ -0,0 +1,14 @@
+{
+    "compilerOptions": {
+        "target": "ESNext",
+        "module": "ESNext",
+        "moduleResolution": "Bundler",
+        "strict": true,
+        "skipLibCheck": true,
+        "lib": ["ESNext"],
+        "types": ["./worker-configuration.d.ts", "@types/node"],
+        "jsx": "react-jsx",
+        "jsxImportSource": "hono/jsx"
+    },
+    "include": ["src/**/*.ts", "worker-configuration.d.ts"]
+}

+ 30 - 0
wrangler.toml

@@ -0,0 +1,30 @@
+name = "pm2000"
+main = "src/index.ts"
+compatibility_date = "2025-03-01"
+compatibility_flags = ["nodejs_compat"]
+
+[observability]
+enabled = true
+
+[assets]
+directory = "./public"
+binding = "ASSETS"
+run_worker_first = ["/api/*", "/h/*", "/health"]
+
+[[d1_databases]]
+binding = "DATABASE"
+database_name = "pm2000-db"
+database_id = "your-d1-database-id-here"
+migrations_dir = "drizzle"
+
+[[durable_objects.bindings]]
+name = "HAND"
+class_name = "Hand"
+
+[[migrations]]
+tag = "v1"
+new_sqlite_classes = ["Hand"]
+
+[vars]
+OTP_MODE = "console"
+PICK_INSTRUCTIONS = "Which option should this table go with? Yes chips are votes and weigh heavily. Respect stated constraints."