Compare commits

..

22 commits

Author SHA1 Message Date
lila
6539d3e346 fix: prevent deployment when quality checks fail
All checks were successful
Build and Deploy / quality (push) Successful in 1m47s
Build and Deploy / build-and-deploy (push) Successful in 1m0s
2026-05-02 13:22:35 +02:00
lila
531da98c24 fix: initialize Resend lazily to prevent test failures when API key is absent
All checks were successful
Build and Deploy / quality (push) Successful in 1m46s
Build and Deploy / build-and-deploy (push) Successful in 1m37s
2026-05-02 13:18:00 +02:00
lila
90b0890263 Merge branch 'dev'
Some checks failed
Build and Deploy / quality (push) Failing after 1m47s
Build and Deploy / build-and-deploy (push) Successful in 2m8s
2026-05-02 13:09:19 +02:00
lila
ccfd83d16c feat: email/password auth + email verification + password reset via Resend 2026-05-02 13:05:43 +02:00
lila
4ae2c568c6 fix: resolve ESLint config file ignores and project service coverage 2026-05-02 12:15:23 +02:00
lila
6ca6fc4e09 fix: correct dotenv path in packages/db/src/index.ts for compiled dist output 2026-05-02 11:23:10 +02:00
lila
e1c4fb5744 refactoring 2026-05-02 11:22:54 +02:00
lila
dc11213cb5 feat: replace login route with auth modal
- Add AuthModal to root layout driven by ?modal=auth search param
- Update multiplayer and play beforeLoad redirects to use modal
- Update NavAuth and Hero links to use modal
- Delete login route and NavLogin component
2026-04-30 19:46:45 +02:00
lila
32ee1edf80 feat: add AuthModal component with login, register and social tabs
- Add AuthModal with login/register tabs and social buttons
- Add forgot-password and reset-password routes
- Add Sonner toaster to root layout
- Add auth search schemas to @lila/shared
- Add ESLint overrides for TanStack Router generics
2026-04-30 19:38:43 +02:00
lila
6297dff399 feat: add email/password auth backend + forgot/reset password routes
- Configure Better Auth emailAndPassword plugin with Resend
- Add email verification and password reset email sending
- Create forgot-password and reset-password frontend routes
- Add auth schemas to @lila/shared
2026-04-30 18:30:20 +02:00
lila
690e1ab72e updating status
All checks were successful
Build and Deploy / quality (push) Successful in 1m41s
Build and Deploy / build-and-deploy (push) Successful in 1m1s
2026-04-30 02:17:35 +02:00
lila
349107fa6f revert: remove registry login from deploy step
All checks were successful
Build and Deploy / quality (push) Successful in 1m41s
Build and Deploy / build-and-deploy (push) Successful in 34s
2026-04-30 02:02:58 +02:00
lila
14d1837ee9 fix: login to registry in deploy step to bypass gpg passphrase prompt
Some checks failed
Build and Deploy / quality (push) Successful in 1m40s
Build and Deploy / build-and-deploy (push) Failing after 1m1s
2026-04-30 01:52:21 +02:00
lila
5f553930c2 chore: skip husky in production installs
Some checks failed
Build and Deploy / quality (push) Successful in 1m43s
Build and Deploy / build-and-deploy (push) Failing after 1m56s
2026-04-30 01:36:07 +02:00
lila
47a0becc6e chore: fix typecheck script to use per-package tsc --noEmit
Some checks failed
Build and Deploy / quality (push) Successful in 1m41s
Build and Deploy / build-and-deploy (push) Failing after 1m2s
2026-04-30 01:29:19 +02:00
lila
89e559a4db adding pnpm-store
Some checks failed
Build and Deploy / quality (push) Failing after 1m31s
Build and Deploy / build-and-deploy (push) Failing after 48s
2026-04-30 01:20:59 +02:00
lila
4f47e18ad9 formatting 2026-04-30 01:20:12 +02:00
lila
35e54014b3 chore: add husky pre-commit and pre-push hooks
Some checks failed
Build and Deploy / quality (push) Failing after 1m53s
Build and Deploy / build-and-deploy (push) Failing after 1m3s
2026-04-30 01:15:14 +02:00
lila
4d64d50598 removing comment that tested pre-commit/pre-push hook 2026-04-30 01:14:53 +02:00
lila
1bfc0606c3 test: verify pre-commit hook 2026-04-30 01:13:01 +02:00
lila
8a121442a3 adding missing variables 2026-04-30 00:38:08 +02:00
lila
57d2190549 adding task to prompts 2026-04-30 00:36:28 +02:00
37 changed files with 1080 additions and 258 deletions

View file

@ -1,4 +1,5 @@
DATABASE_URL=postgres://postgres:mypassword@db-host:5432/databasename DATABASE_URL=postgres://postgres:mypassword@db-host:5432/databasename
DATABASE_URL_LOCAL=postgres://postgres:mypassword@localhost:5432/databasename
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
@ -10,3 +11,11 @@ GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET= GITHUB_CLIENT_SECRET=
VITE_WS_URL=
UID=1000
GID=1000
RESEND_API_KEY=
EMAIL_FROM=mail@example.com

View file

@ -5,8 +5,31 @@ on:
branches: [main] branches: [main]
jobs: jobs:
quality:
runs-on: docker
steps:
- name: Install tools
run: apt-get update && apt-get install -y nodejs
- name: Checkout code
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build shared packages
run: pnpm --filter @lila/shared build && pnpm --filter @lila/db build
- name: Format check
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Type-check
run: pnpm typecheck
- name: Test
run: pnpm test:run
build-and-deploy: build-and-deploy:
runs-on: docker runs-on: docker
needs: quality
steps: steps:
- name: Install tools - name: Install tools
run: apt-get update && apt-get install -y docker.io openssh-client run: apt-get update && apt-get install -y docker.io openssh-client

1
.husky/pre-commit Executable file
View file

@ -0,0 +1 @@
pnpm lint-staged

1
.husky/pre-push Executable file
View file

@ -0,0 +1 @@
pnpm test:run

View file

@ -18,3 +18,5 @@ coverage/
pnpm-lock.yaml pnpm-lock.yaml
routeTree.gen.ts routeTree.gen.ts
.pnpm-store/

View file

@ -7,7 +7,8 @@
"dev": "pnpm --filter shared build && pnpm --filter db build && tsx watch src/server.ts", "dev": "pnpm --filter shared build && pnpm --filter db build && tsx watch src/server.ts",
"build": "tsc", "build": "tsc",
"start": "node dist/src/server.js", "start": "node dist/src/server.js",
"test": "vitest" "test": "vitest",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@lila/db": "workspace:*", "@lila/db": "workspace:*",
@ -17,6 +18,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"express-rate-limit": "^8.4.0", "express-rate-limit": "^8.4.0",
"helmet": "^8.1.0", "helmet": "^8.1.0",
"resend": "^6.12.2",
"ws": "^8.20.0" "ws": "^8.20.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,8 +1,11 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { Resend } from "resend";
import { db } from "@lila/db"; import { db } from "@lila/db";
import * as schema from "@lila/db/schema"; import * as schema from "@lila/db/schema";
const emailFrom = process.env["EMAIL_FROM"] ?? "noreply@lilastudy.com";
export const auth = betterAuth({ export const auth = betterAuth({
baseURL: process.env["BETTER_AUTH_URL"] || "http://localhost:3000", baseURL: process.env["BETTER_AUTH_URL"] || "http://localhost:3000",
advanced: { advanced: {
@ -16,6 +19,44 @@ export const auth = betterAuth({
}, },
}, },
database: drizzleAdapter(db, { provider: "pg", schema }), database: drizzleAdapter(db, { provider: "pg", schema }),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({
user,
url,
}: {
user: { email: string };
url: string;
}) => {
const resend = new Resend(process.env["RESEND_API_KEY"]);
await resend.emails.send({
from: emailFrom,
to: user.email,
subject: "Reset your lila password",
html: `<p>Click <a href="${url}">here</a> to reset your password. This link expires in 1 hour.</p>`,
});
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({
user,
url,
}: {
user: { email: string };
url: string;
}) => {
const resend = new Resend(process.env["RESEND_API_KEY"]);
await resend.emails.send({
from: emailFrom,
to: user.email,
subject: "Verify your lila account",
html: `<p>Click <a href="${url}">here</a> to verify your email address.</p>`,
});
},
},
trustedOrigins: [process.env["CORS_ORIGIN"] || "http://localhost:5173"], trustedOrigins: [process.env["CORS_ORIGIN"] || "http://localhost:5173"],
socialProviders: { socialProviders: {
google: { google: {

View file

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"preview": "vite preview" "preview": "vite preview",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@lila/shared": "workspace:*", "@lila/shared": "workspace:*",
@ -16,6 +17,7 @@
"better-auth": "^1.6.2", "better-auth": "^1.6.2",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"sonner": "^2.0.7",
"tailwindcss": "^4.2.2" "tailwindcss": "^4.2.2"
}, },
"devDependencies": { "devDependencies": {

View file

@ -0,0 +1,249 @@
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { authClient } from "../../lib/auth-client";
type Tab = "login" | "register";
type AuthModalProps = { onClose: () => void; onSuccess: () => void };
type LoginFormProps = { onSuccess: () => void };
const LoginForm = ({ onSuccess }: LoginFormProps) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
await authClient.signIn.email(
{ email, password },
{
onSuccess: () => {
toast.success("Welcome back!");
onSuccess();
},
onError: (ctx) => {
toast.error(ctx.error.message ?? "Something went wrong.");
setIsPending(false);
},
},
);
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
void handleSubmit();
}}
className="flex flex-col gap-3"
>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
/>
<div className="text-right">
<a
href="/forgot-password"
className="text-xs text-(--color-text-muted) hover:text-(--color-primary) transition-colors"
>
Forgot password?
</a>
</div>
<button
type="submit"
disabled={isPending}
className="w-full rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) transition-colors disabled:opacity-50"
>
{isPending ? "Logging in..." : "Login"}
</button>
</form>
);
};
type RegisterFormProps = { onSuccess: () => void };
const RegisterForm = ({ onSuccess }: RegisterFormProps) => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
await authClient.signUp.email(
{ name, email, password },
{
onSuccess: () => {
toast.success("Check your email to verify your account.");
onSuccess();
},
onError: (ctx) => {
toast.error(ctx.error.message ?? "Something went wrong.");
setIsPending(false);
},
},
);
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
void handleSubmit();
}}
className="flex flex-col gap-3"
>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
/>
<button
type="submit"
disabled={isPending}
className="w-full rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) transition-colors disabled:opacity-50"
>
{isPending ? "Creating account..." : "Register"}
</button>
</form>
);
};
type SocialButtonsProps = { onSuccess: () => void };
const SocialButtons = ({ onSuccess }: SocialButtonsProps) => {
const handleSocial = (provider: "google" | "github") => {
void authClient.signIn.social(
{ provider, callbackURL: window.location.origin },
{
onSuccess,
onError: (ctx) => {
toast.error(ctx.error.message ?? "Something went wrong.");
},
},
);
};
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<div className="flex-1 h-px bg-(--color-primary-light)" />
<span className="text-xs text-(--color-text-muted) font-medium">
or continue with
</span>
<div className="flex-1 h-px bg-(--color-primary-light)" />
</div>
<button
onClick={() => handleSocial("github")}
className="w-full rounded-2xl bg-(--color-text) px-4 py-3 text-white font-bold hover:opacity-90 shadow-sm hover:shadow-md transition-all"
>
Continue with GitHub
</button>
<button
onClick={() => handleSocial("google")}
className="w-full rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) shadow-sm hover:shadow-md transition-all"
>
Continue with Google
</button>
</div>
);
};
export const AuthModal = ({ onClose, onSuccess }: AuthModalProps) => {
const [tab, setTab] = useState<Tab>("login");
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
onClick={onClose}
>
<div
className="relative w-full max-w-sm rounded-3xl border border-(--color-primary-light) bg-white shadow-lg p-8 flex flex-col gap-6"
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={onClose}
className="absolute top-4 right-4 text-(--color-text-muted) hover:text-(--color-primary) transition-colors"
aria-label="Close"
>
</button>
{/* Header */}
<div className="text-center">
<h2 className="text-2xl font-black tracking-tight text-(--color-text)">
lila
</h2>
</div>
{/* Tabs */}
<div className="flex rounded-2xl border border-(--color-primary-light) overflow-hidden">
{(["login", "register"] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`flex-1 py-2 text-sm font-bold transition-colors capitalize ${
tab === t
? "bg-(--color-primary) text-white"
: "text-(--color-text-muted) hover:text-(--color-primary)"
}`}
>
{t}
</button>
))}
</div>
{tab === "login" ? (
<LoginForm onSuccess={onSuccess} />
) : (
<RegisterForm onSuccess={onClose} />
)}
{/* Social */}
<SocialButtons onSuccess={onSuccess} />
</div>
</div>
);
};

View file

@ -66,13 +66,15 @@ const Hero = () => {
) : ( ) : (
<> <>
<Link <Link
to="/login" to="/"
search={{ modal: "auth" }}
className="px-7 py-3 rounded-full text-white font-bold text-sm bg-(--color-primary) hover:bg-(--color-primary-dark)" className="px-7 py-3 rounded-full text-white font-bold text-sm bg-(--color-primary) hover:bg-(--color-primary-dark)"
> >
Get started Get started
</Link> </Link>
<Link <Link
to="/login" to="/"
search={{ modal: "auth" }}
className="px-7 py-3 rounded-full font-bold text-sm text-(--color-primary) border-2 border-(--color-primary) hover:bg-(--color-surface)" className="px-7 py-3 rounded-full font-bold text-sm text-(--color-primary) border-2 border-(--color-primary) hover:bg-(--color-surface)"
> >
Log in Log in

View file

@ -24,13 +24,14 @@ const NavAuth = () => {
</button> </button>
) : ( ) : (
<Link <Link
to="/login" to="/"
search={{ modal: "auth" }}
className="text-sm font-medium px-4 py-1.5 rounded-full className="text-sm font-medium px-4 py-1.5 rounded-full
text-white bg-(--color-primary) text-white bg-(--color-primary)
hover:bg-(--color-primary-dark) hover:bg-(--color-primary-dark)
transition-colors duration-200" transition-colors duration-200"
> >
Sign in Login
</Link> </Link>
)} )}
</div> </div>

View file

@ -1,17 +0,0 @@
import { Link } from "@tanstack/react-router";
const NavLogin = () => {
return (
<Link
to="/login"
className="text-sm font-medium px-4 py-1.5 rounded-full
text-white bg-(--color-primary)
hover:bg-(--color-primary-dark)
transition-colors duration-200"
>
Login
</Link>
);
};
export default NavLogin;

View file

@ -9,15 +9,21 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as PlayRouteImport } from './routes/play' import { Route as PlayRouteImport } from './routes/play'
import { Route as MultiplayerRouteImport } from './routes/multiplayer' import { Route as MultiplayerRouteImport } from './routes/multiplayer'
import { Route as LoginRouteImport } from './routes/login' import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
import { Route as AboutRouteImport } from './routes/about' import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as MultiplayerIndexRouteImport } from './routes/multiplayer/index' import { Route as MultiplayerIndexRouteImport } from './routes/multiplayer/index'
import { Route as MultiplayerLobbyCodeRouteImport } from './routes/multiplayer/lobby.$code' import { Route as MultiplayerLobbyCodeRouteImport } from './routes/multiplayer/lobby.$code'
import { Route as MultiplayerGameCodeRouteImport } from './routes/multiplayer/game.$code' import { Route as MultiplayerGameCodeRouteImport } from './routes/multiplayer/game.$code'
const ResetPasswordRoute = ResetPasswordRouteImport.update({
id: '/reset-password',
path: '/reset-password',
getParentRoute: () => rootRouteImport,
} as any)
const PlayRoute = PlayRouteImport.update({ const PlayRoute = PlayRouteImport.update({
id: '/play', id: '/play',
path: '/play', path: '/play',
@ -28,9 +34,9 @@ const MultiplayerRoute = MultiplayerRouteImport.update({
path: '/multiplayer', path: '/multiplayer',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const LoginRoute = LoginRouteImport.update({ const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
id: '/login', id: '/forgot-password',
path: '/login', path: '/forgot-password',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AboutRoute = AboutRouteImport.update({ const AboutRoute = AboutRouteImport.update({
@ -62,9 +68,10 @@ const MultiplayerGameCodeRoute = MultiplayerGameCodeRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/login': typeof LoginRoute '/forgot-password': typeof ForgotPasswordRoute
'/multiplayer': typeof MultiplayerRouteWithChildren '/multiplayer': typeof MultiplayerRouteWithChildren
'/play': typeof PlayRoute '/play': typeof PlayRoute
'/reset-password': typeof ResetPasswordRoute
'/multiplayer/': typeof MultiplayerIndexRoute '/multiplayer/': typeof MultiplayerIndexRoute
'/multiplayer/game/$code': typeof MultiplayerGameCodeRoute '/multiplayer/game/$code': typeof MultiplayerGameCodeRoute
'/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute '/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute
@ -72,8 +79,9 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/login': typeof LoginRoute '/forgot-password': typeof ForgotPasswordRoute
'/play': typeof PlayRoute '/play': typeof PlayRoute
'/reset-password': typeof ResetPasswordRoute
'/multiplayer': typeof MultiplayerIndexRoute '/multiplayer': typeof MultiplayerIndexRoute
'/multiplayer/game/$code': typeof MultiplayerGameCodeRoute '/multiplayer/game/$code': typeof MultiplayerGameCodeRoute
'/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute '/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute
@ -82,9 +90,10 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/login': typeof LoginRoute '/forgot-password': typeof ForgotPasswordRoute
'/multiplayer': typeof MultiplayerRouteWithChildren '/multiplayer': typeof MultiplayerRouteWithChildren
'/play': typeof PlayRoute '/play': typeof PlayRoute
'/reset-password': typeof ResetPasswordRoute
'/multiplayer/': typeof MultiplayerIndexRoute '/multiplayer/': typeof MultiplayerIndexRoute
'/multiplayer/game/$code': typeof MultiplayerGameCodeRoute '/multiplayer/game/$code': typeof MultiplayerGameCodeRoute
'/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute '/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute
@ -94,9 +103,10 @@ export interface FileRouteTypes {
fullPaths: fullPaths:
| '/' | '/'
| '/about' | '/about'
| '/login' | '/forgot-password'
| '/multiplayer' | '/multiplayer'
| '/play' | '/play'
| '/reset-password'
| '/multiplayer/' | '/multiplayer/'
| '/multiplayer/game/$code' | '/multiplayer/game/$code'
| '/multiplayer/lobby/$code' | '/multiplayer/lobby/$code'
@ -104,8 +114,9 @@ export interface FileRouteTypes {
to: to:
| '/' | '/'
| '/about' | '/about'
| '/login' | '/forgot-password'
| '/play' | '/play'
| '/reset-password'
| '/multiplayer' | '/multiplayer'
| '/multiplayer/game/$code' | '/multiplayer/game/$code'
| '/multiplayer/lobby/$code' | '/multiplayer/lobby/$code'
@ -113,9 +124,10 @@ export interface FileRouteTypes {
| '__root__' | '__root__'
| '/' | '/'
| '/about' | '/about'
| '/login' | '/forgot-password'
| '/multiplayer' | '/multiplayer'
| '/play' | '/play'
| '/reset-password'
| '/multiplayer/' | '/multiplayer/'
| '/multiplayer/game/$code' | '/multiplayer/game/$code'
| '/multiplayer/lobby/$code' | '/multiplayer/lobby/$code'
@ -124,13 +136,21 @@ export interface FileRouteTypes {
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute AboutRoute: typeof AboutRoute
LoginRoute: typeof LoginRoute ForgotPasswordRoute: typeof ForgotPasswordRoute
MultiplayerRoute: typeof MultiplayerRouteWithChildren MultiplayerRoute: typeof MultiplayerRouteWithChildren
PlayRoute: typeof PlayRoute PlayRoute: typeof PlayRoute
ResetPasswordRoute: typeof ResetPasswordRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/reset-password': {
id: '/reset-password'
path: '/reset-password'
fullPath: '/reset-password'
preLoaderRoute: typeof ResetPasswordRouteImport
parentRoute: typeof rootRouteImport
}
'/play': { '/play': {
id: '/play' id: '/play'
path: '/play' path: '/play'
@ -145,11 +165,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MultiplayerRouteImport preLoaderRoute: typeof MultiplayerRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/login': { '/forgot-password': {
id: '/login' id: '/forgot-password'
path: '/login' path: '/forgot-password'
fullPath: '/login' fullPath: '/forgot-password'
preLoaderRoute: typeof LoginRouteImport preLoaderRoute: typeof ForgotPasswordRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/about': { '/about': {
@ -209,9 +229,10 @@ const MultiplayerRouteWithChildren = MultiplayerRoute._addFileChildren(
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AboutRoute: AboutRoute, AboutRoute: AboutRoute,
LoginRoute: LoginRoute, ForgotPasswordRoute: ForgotPasswordRoute,
MultiplayerRoute: MultiplayerRouteWithChildren, MultiplayerRoute: MultiplayerRouteWithChildren,
PlayRoute: PlayRoute, PlayRoute: PlayRoute,
ResetPasswordRoute: ResetPasswordRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View file

@ -1,16 +1,39 @@
import { createRootRoute, Outlet } from "@tanstack/react-router"; import {
createRootRoute,
Outlet,
useNavigate,
useSearch,
} from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Toaster } from "sonner";
import Navbar from "../components/navbar/NavBar"; import Navbar from "../components/navbar/NavBar";
import NotFound from "../components/NotFound"; import NotFound from "../components/NotFound";
import RootError from "../components/RootError"; import RootError from "../components/RootError";
import { AuthModal } from "../components/auth/AuthModal";
import { AuthModalSearchSchema } from "@lila/shared";
const RootLayout = () => { const RootLayout = () => {
const navigate = useNavigate();
const { modal, redirect } = useSearch({ from: "__root__" });
const handleClose = () => {
void navigate({ to: "/", search: {} });
};
const handleSuccess = () => {
void navigate({ to: (redirect as string) ?? "/", search: {} });
};
return ( return (
<> <>
<Navbar /> <Navbar />
<main className="max-w-5xl mx-auto px-6 py-8"> <main className="max-w-5xl mx-auto px-6 py-8">
<Outlet /> <Outlet />
</main> </main>
{modal === "auth" && (
<AuthModal onClose={handleClose} onSuccess={handleSuccess} />
)}
<Toaster richColors position="top-center" />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</> </>
); );
@ -20,4 +43,5 @@ export const Route = createRootRoute({
component: RootLayout, component: RootLayout,
notFoundComponent: NotFound, notFoundComponent: NotFound,
errorComponent: RootError, errorComponent: RootError,
validateSearch: AuthModalSearchSchema,
}); });

View file

@ -0,0 +1,74 @@
import { useState } from "react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { authClient } from "../lib/auth-client";
import { toast } from "sonner";
function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
await authClient.requestPasswordReset(
{ email, redirectTo: `${window.location.origin}/reset-password` },
{
onSuccess: () => {
toast.success("Check your email for a reset link.");
setIsPending(false);
},
onError: (ctx) => {
toast.error(ctx.error.message ?? "Something went wrong.");
setIsPending(false);
},
},
);
};
return (
<div className="flex flex-col items-center justify-center gap-6 p-8 max-w-sm mx-auto">
<div className="w-full text-center">
<h1 className="text-2xl font-black tracking-tight text-(--color-text)">
Forgot password
</h1>
<p className="mt-1 text-sm text-(--color-text-muted)">
Enter your email and we'll send you a reset link.
</p>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
void handleSubmit();
}}
className="w-full flex flex-col gap-3"
>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
/>
<button
type="submit"
disabled={isPending}
className="w-full rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) transition-colors disabled:opacity-50"
>
{isPending ? "Sending..." : "Send reset link"}
</button>
</form>
<Link
to="/"
className="text-sm text-(--color-text-muted) hover:text-(--color-primary) transition-colors"
>
Back to home
</Link>
</div>
);
}
export const Route = createFileRoute("/forgot-password")({
component: ForgotPasswordPage,
});

View file

@ -1,46 +0,0 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { signIn, useSession } from "../lib/auth-client";
const LoginPage = () => {
const { data: session, isPending } = useSession();
const navigate = useNavigate();
if (isPending) return <div className="p-4">Loading...</div>;
if (session) {
void navigate({ to: "/" });
return null;
}
return (
<div className="flex flex-col items-center justify-center gap-4 p-8">
<h1 className="text-2xl font-bold">sign in to lila</h1>
<button
className="w-64 rounded-2xl bg-(--color-text) px-4 py-3 text-white font-bold hover:opacity-90 shadow-sm hover:shadow-md transition-all"
onClick={() => {
void signIn
.social({ provider: "github", callbackURL: window.location.origin })
.catch((err) => {
console.error("GitHub sign in error:", err);
});
}}
>
Continue with GitHub
</button>
<button
className="w-64 rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) shadow-sm hover:shadow-md transition-all"
onClick={() => {
void signIn
.social({ provider: "google", callbackURL: window.location.origin })
.catch((err) => {
console.error("Google sign in error:", err);
});
}}
>
Continue with Google
</button>
</div>
);
};
export const Route = createFileRoute("/login")({ component: LoginPage });

View file

@ -14,7 +14,10 @@ export const Route = createFileRoute("/multiplayer")({
beforeLoad: async () => { beforeLoad: async () => {
const { data: session } = await authClient.getSession(); const { data: session } = await authClient.getSession();
if (!session) { if (!session) {
throw redirect({ to: "/login" }); throw redirect({
to: "/",
search: { modal: "auth", redirect: "/multiplayer" },
});
} }
return { session }; return { session };
}, },

View file

@ -132,7 +132,7 @@ export const Route = createFileRoute("/play")({
beforeLoad: async () => { beforeLoad: async () => {
const { data: session } = await authClient.getSession(); const { data: session } = await authClient.getSession();
if (!session) { if (!session) {
throw redirect({ to: "/login" }); throw redirect({ to: "/", search: { modal: "auth", redirect: "/play" } });
} }
}, },
}); });

View file

@ -0,0 +1,91 @@
import { useState } from "react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { authClient } from "../lib/auth-client";
import { toast } from "sonner";
import { ResetPasswordSearchSchema } from "@lila/shared";
function ResetPasswordPage() {
const { token } = Route.useSearch();
const navigate = useNavigate();
const [password, setPassword] = useState("");
const [isPending, setIsPending] = useState(false);
if (!token) {
return (
<div className="flex flex-col items-center justify-center gap-4 p-8 max-w-sm mx-auto text-center">
<h1 className="text-2xl font-black tracking-tight text-(--color-text)">
Invalid link
</h1>
<p className="text-sm text-(--color-text-muted)">
This reset link is invalid or has expired.
</p>
<Link
to="/forgot-password"
className="text-sm text-(--color-primary) hover:underline"
>
Request a new one
</Link>
</div>
);
}
const handleSubmit = async () => {
setIsPending(true);
await authClient.resetPassword(
{ newPassword: password, token },
{
onSuccess: () => {
toast.success("Password updated. You can now sign in.");
void navigate({ to: "/" });
},
onError: (ctx) => {
toast.error(ctx.error.message ?? "Something went wrong.");
setIsPending(false);
},
},
);
};
return (
<div className="flex flex-col items-center justify-center gap-6 p-8 max-w-sm mx-auto">
<div className="w-full text-center">
<h1 className="text-2xl font-black tracking-tight text-(--color-text)">
Reset password
</h1>
<p className="mt-1 text-sm text-(--color-text-muted)">
Enter your new password below.
</p>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
void handleSubmit();
}}
className="w-full flex flex-col gap-3"
>
<input
type="password"
placeholder="New password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
/>
<button
type="submit"
disabled={isPending}
className="w-full rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) transition-colors disabled:opacity-50"
>
{isPending ? "Updating..." : "Update password"}
</button>
</form>
</div>
);
}
export const Route = createFileRoute("/reset-password")({
component: ResetPasswordPage,
validateSearch: ResetPasswordSearchSchema,
});

View file

@ -90,9 +90,6 @@ Directionally right, timing is unclear. Revisit when the next/now work is done.
- **Resolve eslint peer dependency warning** `[debt]` - **Resolve eslint peer dependency warning** `[debt]`
`eslint-plugin-react-hooks 7.0.1` expects `eslint ^3.0.0^9.0.0` but found `10.0.3`. Low impact but worth cleaning up when nearby. `eslint-plugin-react-hooks 7.0.1` expects `eslint ^3.0.0^9.0.0` but found `10.0.3`. Low impact but worth cleaning up when nearby.
- **husky + lint-staged** `[debt]`
Set up husky and lint-staged to run linting and formatting checks before every commit. Prevents CI failures from formatting or lint issues that slipped through locally.
- **OpenAPI documentation for REST endpoints** `[feature]` - **OpenAPI documentation for REST endpoints** `[feature]`
Document the API surface using OpenAPI/Swagger. Covers all REST endpoints with request/response shapes. Useful groundwork for the admin dashboard and any future contributors. Document the API surface using OpenAPI/Swagger. Covers all REST endpoints with request/response shapes. Useful groundwork for the admin dashboard and any future contributors.
@ -105,6 +102,7 @@ Directionally right, timing is unclear. Revisit when the next/now work is done.
Shipped milestones, newest first. Shipped milestones, newest first.
- **04 - 2026 - husky + lint-staged + CI quality gate** - Pre-commit formatting, pre-push tests, and CI lint/typecheck/test gate before every deploy.
- **04 - 2026 - t00001 - Docker credential helper** - **04 - 2026 - t00001 - Docker credential helper**
- **04 - 2026 - Pin dependencies in package.json** - Unpinned deps in a CI/CD pipeline are a real risk. - **04 - 2026 - Pin dependencies in package.json** - Unpinned deps in a CI/CD pipeline are a real risk.
- **04 - 2026 - React error boundaries** - Catch and display runtime errors gracefully instead of crashing the entire app. - **04 - 2026 - React error boundaries** - Catch and display runtime errors gracefully instead of crashing the entire app.

View file

@ -12,7 +12,8 @@ task description.
4. if we go through a file, we'll do it slowly section by section, no matter how many sections 4. if we go through a file, we'll do it slowly section by section, no matter how many sections
5. how to name the current feature branch? also tell me when its time to git commit and provide a commit message 5. how to name the current feature branch? also tell me when its time to git commit and provide a commit message
6. if we have multiple options to do something, also always provide options that reflect current industry standards and best practices 6. if we have multiple options to do something, also always provide options that reflect current industry standards and best practices
7. For every completed task, produce a ticket file in documentation/tickets/. Use ADR format (adr-) for decisions between options with long-term consequences. Use feat-/fix-/chore- for routine tasks. Always include a setup guide or summary of what was done. Suggest the filename. 7. never assume anything! always ask for clarification!
8. For every completed task, produce a ticket file in documentation/tickets/. Use ADR format (adr-) for decisions between options with long-term consequences. Use feat-/fix-/chore- for routine tasks. Always include a setup guide or summary of what was done. Suggest the filename.
## tasks ## tasks

View file

@ -1,6 +1,6 @@
# 🔥 GameService Roast: `apps/api/src/services/gameService.ts` # 🔥 GameService Roast: `apps/api/src/services/gameService.ts`
> *"It works on my machine" is not a scalability strategy.* > _"It works on my machine" is not a scalability strategy._
**Project:** lila — Vocabulary Trainer **Project:** lila — Vocabulary Trainer
**File Roasted:** `gameService.ts` **File Roasted:** `gameService.ts`
@ -46,43 +46,34 @@ Fix Options:
// Option A: Add atomic operation to store interface // Option A: Add atomic operation to store interface
interface GameSessionStore { interface GameSessionStore {
deleteAnswer(sessionId: string, questionId: string): Promise<boolean>; deleteAnswer(sessionId: string, questionId: string): Promise<boolean>;
} }
// Option B: Use Valkey Lua script for atomic read-modify-write // Option B: Use Valkey Lua script for atomic read-modify-write
// Option C: Optimistic locking with version numbers // Option C: Optimistic locking with version numbers
Priority: 🔴 CRITICAL — Data integrity issue Priority: 🔴 CRITICAL — Data integrity issue 2. N+1 Query: Database Performance Bomb
2. N+1 Query: Database Performance Bomb
Location: gameService.ts:24-26 + termModel.ts:getDistractors() Location: gameService.ts:24-26 + termModel.ts:getDistractors()
// For each of N terms, we call getDistractors(): // For each of N terms, we call getDistractors():
const questions: GameQuestion[] = await Promise.all( const questions: GameQuestion[] = await Promise.all(
terms.map(async (term) => { terms.map(async (term) => {
const distractorTexts = await getDistractors(term.termId, ...); // 🚩 N queries! const distractorTexts = await getDistractors(term.termId, ...); // 🚩 N queries!
}) })
); );
Impact Analysis: Impact Analysis:
Rounds Rounds
DB Queries DB Queries
At 50 concurrent users At 50 concurrent users
3 3
1 + 3 = 4 1 + 3 = 4
200 queries/min 200 queries/min
10 10
1 + 10 = 11 1 + 10 = 11
550 queries/min 550 queries/min
20 20
1 + 20 = 21 1 + 20 = 21
1,050 queries/min 1,050 queries/min
Each getDistractors() runs: Each getDistractors() runs:
@ -95,15 +86,15 @@ Fix: Batch Fetch Distractors
// Fetch all distractors in ONE query // Fetch all distractors in ONE query
const allDistractors = await db const allDistractors = await db
.select({ termId: terms.id, text: translations.text }) .select({ termId: terms.id, text: translations.text })
.from(terms) .from(terms)
.innerJoin(translations, /* ... */) .innerJoin(translations, /_ ... _/)
.where(and( .where(and(
eq(terms.pos, pos), eq(terms.pos, pos),
eq(translations.difficulty, difficulty), eq(translations.difficulty, difficulty),
inArray(terms.id, termIds), // Batch! inArray(terms.id, termIds), // Batch!
)) ))
.limit(DISTRACTOR_FETCH_COUNT * termIds.length); .limit(DISTRACTOR_FETCH_COUNT \* termIds.length);
// Group by termId in JS, then slice to 3 unique distractors per term // Group by termId in JS, then slice to 3 unique distractors per term
const distractorsByTerm = groupByTermId(allDistractors); const distractorsByTerm = groupByTermId(allDistractors);
@ -111,10 +102,10 @@ const distractorsByTerm = groupByTermId(allDistractors);
Priority: 🔴 CRITICAL — Performance/scalability issue Priority: 🔴 CRITICAL — Performance/scalability issue
3. Error Handling Inconsistency 3. Error Handling Inconsistency
Location: gameService.ts:33-36 Location: gameService.ts:33-36
if (uniqueDistractors.length < 3) { if (uniqueDistractors.length < 3) {
throw new Error(`Not enough unique distractors for term: ${term.targetText}`); // 🚩 throw new Error(`Not enough unique distractors for term: ${term.targetText}`); // 🚩
} }
Problem: Raw Error bypasses your errorHandler middleware: Problem: Raw Error bypasses your errorHandler middleware:
@ -127,15 +118,14 @@ Fix:
import { UnprocessableEntityError } from "../errors/AppError.js"; import { UnprocessableEntityError } from "../errors/AppError.js";
if (uniqueDistractors.length < 3) { if (uniqueDistractors.length < 3) {
logger.warn({ termId: term.termId, uniqueCount: uniqueDistractors.length }, logger.warn({ termId: term.termId, uniqueCount: uniqueDistractors.length },
"insufficient_distractors"); "insufficient_distractors");
throw new UnprocessableEntityError( throw new UnprocessableEntityError(
`Not enough unique distractors for term: ${term.targetText}` `Not enough unique distractors for term: ${term.targetText}`
); );
} }
Priority: 🟡 HIGH — Observability & UX issue Priority: 🟡 HIGH — Observability & UX issue
⚠️ High-Severity Smells ⚠️ High-Severity Smells 4. Code Duplication: Singleplayer vs Multiplayer
4. Code Duplication: Singleplayer vs Multiplayer
Compare: gameService.ts vs multiplayerGameService.ts Compare: gameService.ts vs multiplayerGameService.ts
// gameService.ts // gameService.ts
const optionTexts = [term.targetText, ...uniqueDistractors.slice(0, 3)]; const optionTexts = [term.targetText, ...uniqueDistractors.slice(0, 3)];
@ -157,30 +147,29 @@ Fix: Extract pure function to @lila/shared or new @lila/game-logic:
// packages/shared/src/game-logic.ts // packages/shared/src/game-logic.ts
export const buildQuestionOptions = ( export const buildQuestionOptions = (
correctAnswer: string, correctAnswer: string,
distractors: string[], distractors: string[],
optionCount: number = 4 optionCount: number = 4
): { options: AnswerOption[]; correctOptionId: number } => { ): { options: AnswerOption[]; correctOptionId: number } => {
const uniqueDistractors = [...new Set(distractors.filter(d => d !== correctAnswer))]; const uniqueDistractors = [...new Set(distractors.filter(d => d !== correctAnswer))];
const optionTexts = [correctAnswer, ...uniqueDistractors.slice(0, optionCount - 1)]; const optionTexts = [correctAnswer, ...uniqueDistractors.slice(0, optionCount - 1)];
const shuffled = shuffleSecure(optionTexts); const shuffled = shuffleSecure(optionTexts);
const correctOptionId = shuffled.indexOf(correctAnswer); const correctOptionId = shuffled.indexOf(correctAnswer);
return { return {
options: shuffled.map((text, idx) => ({ optionId: idx, text })), options: shuffled.map((text, idx) => ({ optionId: idx, text })),
correctOptionId correctOptionId
}; };
}; };
Priority: 🟡 HIGH — Maintainability issue Priority: 🟡 HIGH — Maintainability issue 5. Shuffle Bias: Math.random() Trap
5. Shuffle Bias: Math.random() Trap
Location: utils.ts:shuffleArray() + multiplayerGameService.ts:shuffle() Location: utils.ts:shuffleArray() + multiplayerGameService.ts:shuffle()
export const shuffleArray = <T>(array: T[]): T[] => { export const shuffleArray = <T>(array: T[]): T[] => {
for (let i = result.length - 1; i > 0; i--) { for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // 🚩 Modulo bias + non-crypto RNG const j = Math.floor(Math.random() \* (i + 1)); // 🚩 Modulo bias + non-crypto RNG
// ... // ...
} }
}; };
The Math: The Math:
@ -199,21 +188,21 @@ Fix (if needed):
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
const shuffleSecure = <T>(array: T[]): T[] => { const shuffleSecure = <T>(array: T[]): T[] => {
const result = [...array]; const result = [...array];
for (let i = result.length - 1; i > 0; i--) { for (let i = result.length - 1; i > 0; i--) {
// Use crypto.getRandomValues for better randomness // Use crypto.getRandomValues for better randomness
const rand = randomBytes(4).readUInt32LE(0); const rand = randomBytes(4).readUInt32LE(0);
const j = rand % (i + 1); const j = rand % (i + 1);
[result[i], result[j]] = [result[j], result[i]]; [result[i], result[j]] = [result[j], result[i]];
} }
return result; return result;
}; };
Priority: 🟢 LOW — Document tradeoff and move on for now Priority: 🟢 LOW — Document tradeoff and move on for now
6. Test Coverage Gaps 6. Test Coverage Gaps
File: gameService.test.ts File: gameService.test.ts
✅ Well Tested: ✅ Well Tested:
Happy path: session creation, answer evaluation Happy path: session creation, answer evaluation
Edge cases: duplicate distractors, empty results, invalid inputs Edge cases: duplicate distractors, empty results, invalid inputs
@ -223,41 +212,40 @@ File: gameService.test.ts
// 1. Concurrency test (race condition) // 1. Concurrency test (race condition)
it("rejects duplicate answers for same question under concurrent load", async () => { it("rejects duplicate answers for same question under concurrent load", async () => {
const session = await createGameSession(validRequest, store, "user-1"); const session = await createGameSession(validRequest, store, "user-1");
const question = session.questions[0]!; const question = session.questions[0]!;
// Submit two answers simultaneously // Submit two answers simultaneously
const [result1, result2] = await Promise.allSettled([ const [result1, result2] = await Promise.allSettled([
evaluateAnswer({ sessionId, questionId, selectedOptionId: 0 }, store, "user-1"), evaluateAnswer({ sessionId, questionId, selectedOptionId: 0 }, store, "user-1"),
evaluateAnswer({ sessionId, questionId, selectedOptionId: 1 }, store, "user-1"), evaluateAnswer({ sessionId, questionId, selectedOptionId: 1 }, store, "user-1"),
]); ]);
// Exactly one should succeed, one should throw ConflictError // Exactly one should succeed, one should throw ConflictError
expect([result1, result2].filter(r => r.status === "fulfilled")).toHaveLength(1); expect([result1, result2].filter(r => r.status === "fulfilled")).toHaveLength(1);
}); });
// 2. TTL expiration test // 2. TTL expiration test
it("deletes session after TTL expires", async () => { it("deletes session after TTL expires", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
const session = await createGameSession(validRequest, store, "user-1"); const session = await createGameSession(validRequest, store, "user-1");
vi.advanceTimersByTime(31 * 60 * 1000); // 31 minutes vi.advanceTimersByTime(31 _ 60 _ 1000); // 31 minutes
await expect(store.get(session.sessionId)).resolves.toBeNull(); await expect(store.get(session.sessionId)).resolves.toBeNull();
}); });
// 3. Distractor fallback strategy test // 3. Distractor fallback strategy test
it("uses fallback when <3 unique distractors available", async () => { it("uses fallback when <3 unique distractors available", async () => {
mockGetDistractors.mockResolvedValue(["same", "same", "same", "same"]); mockGetDistractors.mockResolvedValue(["same", "same", "same", "same"]);
// Should either: (a) fetch from broader pool, or (b) reduce rounds gracefully // Should either: (a) fetch from broader pool, or (b) reduce rounds gracefully
}); });
Priority: 🟡 HIGH — Prevents regression on critical fixes Priority: 🟡 HIGH — Prevents regression on critical fixes
🧼 Code Quality Nitpicks 🧼 Code Quality Nitpicks 7. Magic Numbers
7. Magic Numbers
// gameService.ts:52 // gameService.ts:52
await store.create(sessionId, {...}, 30 * 60 * 1000); // What is this? await store.create(sessionId, {...}, 30 _ 60 _ 1000); // What is this?
// termModel.ts:65 // termModel.ts:65
.limit(count); // count=6, but why? .limit(count); // count=6, but why?
@ -267,16 +255,16 @@ optionId: z.number().int().min(0).max(3), // Why 4 options?
Fix: Centralize in @lila/shared/constants.ts: Fix: Centralize in @lila/shared/constants.ts:
export const GAME_SESSION_TTL_MS = 30 * 60 * 1000; export const GAME*SESSION_TTL_MS = 30 * 60 \_ 1000;
export const DISTRACTOR_FETCH_COUNT = 6; export const DISTRACTOR_FETCH_COUNT = 6;
export const GAME_OPTION_COUNT = 4; export const GAME_OPTION_COUNT = 4;
export const MIN_UNIQUE_DISTRACTORS = 3; export const MIN_UNIQUE_DISTRACTORS = 3;
8. Mutable Reference Leakage 8. Mutable Reference Leakage
Location: InMemoryGameSessionStore.ts:get() Location: InMemoryGameSessionStore.ts:get()
get(sessionId: string): Promise<GameSessionData | null> { get(sessionId: string): Promise<GameSessionData | null> {
return Promise.resolve(entry.data); // 🚩 Returns mutable reference to internal state return Promise.resolve(entry.data); // 🚩 Returns mutable reference to internal state
} }
Risk: Any code that does session.answers.delete(...) mutates the store's internal Map directly. Risk: Any code that does session.answers.delete(...) mutates the store's internal Map directly.
@ -291,37 +279,35 @@ return Promise.resolve(entry.data as Readonly<GameSessionData>);
// Option C: Use immutable data structures (overkill for now) // Option C: Use immutable data structures (overkill for now)
9. Zero Observability 9. Zero Observability
Problem: No logging, no metrics. You're flying blind in production. Problem: No logging, no metrics. You're flying blind in production.
Minimal Fix (5 minutes): Minimal Fix (5 minutes):
// apps/api/src/lib/logger.ts // apps/api/src/lib/logger.ts
import pino from "pino"; import pino from "pino";
export const logger = pino({ export const logger = pino({
level: process.env.LOG_LEVEL || "info", level: process.env.LOG_LEVEL || "info",
transport: process.env.NODE_ENV === "production" transport: process.env.NODE_ENV === "production"
? { target: "pino-pretty" } ? { target: "pino-pretty" }
: undefined : undefined
}); });
// In gameService.ts: // In gameService.ts:
import { logger } from "../lib/logger.js"; import { logger } from "../lib/logger.js";
logger.info( logger.info(
{ userId, sourceLang, targetLang, termCount: terms.length }, { userId, sourceLang, targetLang, termCount: terms.length },
"game_session_created" "game_session_created"
); );
logger.debug( logger.debug(
{ sessionId, questionId, isCorrect, responseTimeMs }, { sessionId, questionId, isCorrect, responseTimeMs },
"answer_evaluated" "answer_evaluated"
); );
Bonus: Export a Prometheus histogram for game_service_duration_seconds. Bonus: Export a Prometheus histogram for game_service_duration_seconds.
10. ORDER BY RANDOM() Time Bomb 10. ORDER BY RANDOM() Time Bomb
Location: termModel.ts:getGameTerms() + getDistractors() Location: termModel.ts:getGameTerms() + getDistractors()
.orderBy(sql`RANDOM()`) // 🚩 Fine for 10k rows, slow for 1M .orderBy(sql`RANDOM()`) // 🚩 Fine for 10k rows, slow for 1M
@ -343,6 +329,6 @@ WHERE ...
LIMIT $1 LIMIT $1
-- Option C: Random offset (simple, but still scans) -- Option C: Random offset (simple, but still scans)
OFFSET floor(random() * (SELECT count(*) FROM terms WHERE ...)) OFFSET floor(random() _ (SELECT count(_) FROM terms WHERE ...))
Action: Add a ticket to documentation/tickets/t00009.md now. Action: Add a ticket to documentation/tickets/t00009.md now.

View file

@ -84,7 +84,10 @@ Rejected because: for user-owned resources identified by opaque IDs, confirming
2. `GameSessionStore.ts` — add `userId` to `GameSessionData`: 2. `GameSessionStore.ts` — add `userId` to `GameSessionData`:
```ts ```ts
export type GameSessionData = { answers: Map<string, number>; userId: string }; export type GameSessionData = {
answers: Map<string, number>;
userId: string;
};
``` ```
3. `gameService.ts` — add `userId` to both function signatures: 3. `gameService.ts` — add `userId` to both function signatures:
@ -100,7 +103,11 @@ Rejected because: for user-owned resources identified by opaque IDs, confirming
Store it on create: Store it on create:
```ts ```ts
await store.create(sessionId, { answers: answerKey, userId }, 30 * 60 * 1000); await store.create(
sessionId,
{ answers: answerKey, userId },
30 * 60 * 1000,
);
``` ```
Assert on evaluate: Assert on evaluate:
@ -114,7 +121,7 @@ Rejected because: for user-owned resources identified by opaque IDs, confirming
4. `gameController.ts` — extract from authenticated request: 4. `gameController.ts` — extract from authenticated request:
```ts ```ts
req.session.user.id req.session.user.id;
``` ```
5. `gameRouter.ts` — cast at registration: 5. `gameRouter.ts` — cast at registration:

View file

@ -27,7 +27,9 @@ Not chosen for this ticket — the database query is in `@lila/db` and is a sepa
- Filter distractors against the correct answer before building options: - Filter distractors against the correct answer before building options:
```ts ```ts
const uniqueDistractors = distractorTexts.filter((t) => t !== term.targetText); const uniqueDistractors = distractorTexts.filter(
(t) => t !== term.targetText,
);
const optionTexts = [term.targetText, ...uniqueDistractors.slice(0, 3)]; const optionTexts = [term.targetText, ...uniqueDistractors.slice(0, 3)];
``` ```

View file

@ -10,8 +10,6 @@ export default defineConfig([
globalIgnores([ globalIgnores([
"**/dist/**", "**/dist/**",
"node_modules/", "node_modules/",
"eslint.config.mjs",
"**/*.config.ts",
"routeTree.gen.ts", "routeTree.gen.ts",
"scripts/**", "scripts/**",
"data-pipeline/**/*", "data-pipeline/**/*",
@ -24,12 +22,19 @@ export default defineConfig([
{ {
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
projectService: true, projectService: { allowDefaultProject: ["*.mjs", "*.ts"] },
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
}, },
}, },
{
files: ["eslint.config.mjs"],
rules: {
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
},
},
{ {
files: ["apps/web/**/*.{ts,tsx}"], files: ["apps/web/**/*.{ts,tsx}"],
extends: [ extends: [
@ -43,6 +48,9 @@ export default defineConfig([
rules: { rules: {
"react-refresh/only-export-components": "off", "react-refresh/only-export-components": "off",
"@typescript-eslint/only-throw-error": "off", "@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
}, },
}, },
{ {

View file

@ -6,11 +6,22 @@
"scripts": { "scripts": {
"build": "pnpm --filter @lila/shared build && pnpm --filter @lila/db build && pnpm --filter @lila/api build", "build": "pnpm --filter @lila/shared build && pnpm --filter @lila/db build && pnpm --filter @lila/api build",
"dev": "concurrently --names \"api,web\" -c \"magenta.bold,green.bold\" \"pnpm --filter @lila/api dev\" \"pnpm --filter @lila/web dev\"", "dev": "concurrently --names \"api,web\" -c \"magenta.bold,green.bold\" \"pnpm --filter @lila/api dev\" \"pnpm --filter @lila/web dev\"",
"prepare": "husky || true",
"test": "vitest", "test": "vitest",
"test:run": "vitest run", "test:run": "vitest run",
"lint": "eslint .", "lint": "eslint .",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check ." "format:check": "prettier --check .",
"typecheck": "pnpm -r typecheck"
},
"lint-staged": {
"**/*.{ts,tsx}": [
"prettier --write",
"eslint --fix"
],
"**/*.{js,mjs,json,md,css,html}": [
"prettier --write"
]
}, },
"packageManager": "pnpm@10.33.1", "packageManager": "pnpm@10.33.1",
"devDependencies": { "devDependencies": {
@ -22,6 +33,8 @@
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.57.1", "typescript-eslint": "^8.57.1",

View file

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"build": "rm -rf dist && tsc", "build": "rm -rf dist && tsc",
"generate": "drizzle-kit generate", "generate": "drizzle-kit generate",
"migrate": "drizzle-kit migrate" "migrate": "drizzle-kit migrate",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@lila/shared": "workspace:*", "@lila/shared": "workspace:*",

View file

@ -6,10 +6,13 @@ import { dirname } from "path";
import * as schema from "./db/schema.js"; import * as schema from "./db/schema.js";
config({ config({
path: resolve(dirname(fileURLToPath(import.meta.url)), "../../../.env"), path: resolve(dirname(fileURLToPath(import.meta.url)), "../../../../.env"),
}); });
export const db = drizzle(process.env["DATABASE_URL"]!, { schema }); export const db = drizzle(
process.env["DATABASE_URL_LOCAL"] ?? process.env["DATABASE_URL"]!,
{ schema },
);
export * from "./models/termModel.js"; export * from "./models/termModel.js";
export * from "./models/lobbyModel.js"; export * from "./models/lobbyModel.js";

View file

@ -10,6 +10,7 @@
"include": [ "include": [
"src", "src",
"vitest.config.ts", "vitest.config.ts",
"drizzle.config.ts",
"../../data-pipeline/archive/packages-db-src-old-seeding-scripts/data" "../../data-pipeline/archive/packages-db-src-old-seeding-scripts/data"
] ]
} }

View file

@ -4,7 +4,8 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc" "build": "tsc",
"typecheck": "tsc --noEmit"
}, },
"exports": { "exports": {
".": "./dist/src/index.js" ".": "./dist/src/index.js"

View file

@ -1,3 +1,4 @@
export * from "./constants.js"; export * from "./constants.js";
export * from "./schemas/game.js"; export * from "./schemas/game.js";
export * from "./schemas/lobby.js"; export * from "./schemas/lobby.js";
export * from "./schemas/auth.js";

View file

@ -0,0 +1,14 @@
import * as z from "zod";
export const ResetPasswordSearchSchema = z.object({
token: z.string().catch(""),
});
export type ResetPasswordSearch = z.infer<typeof ResetPasswordSearchSchema>;
export const AuthModalSearchSchema = z.object({
modal: z.enum(["auth"]).optional().catch(undefined),
redirect: z.string().optional().catch(undefined),
});
export type AuthModalSearch = z.infer<typeof AuthModalSearchSchema>;

367
pnpm-lock.yaml generated
View file

@ -16,7 +16,7 @@ importers:
version: 1.161.6(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) version: 1.161.6(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) version: 4.1.0(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))
concurrently: concurrently:
specifier: ^9.2.1 specifier: ^9.2.1
version: 9.2.1 version: 9.2.1
@ -32,6 +32,12 @@ importers:
eslint-plugin-react-refresh: eslint-plugin-react-refresh:
specifier: ^0.5.2 specifier: ^0.5.2
version: 0.5.2(eslint@10.0.3(jiti@2.6.1)) version: 0.5.2(eslint@10.0.3(jiti@2.6.1))
husky:
specifier: ^9.1.7
version: 9.1.7
lint-staged:
specifier: ^16.4.0
version: 16.4.0
prettier: prettier:
specifier: ^3.8.1 specifier: ^3.8.1
version: 3.8.1 version: 3.8.1
@ -43,7 +49,7 @@ importers:
version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
vitest: vitest:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
apps/api: apps/api:
dependencies: dependencies:
@ -55,7 +61,7 @@ importers:
version: link:../../packages/shared version: link:../../packages/shared
better-auth: better-auth:
specifier: ^1.6.2 specifier: ^1.6.2
version: 1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) version: 1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))
cors: cors:
specifier: ^2.8.6 specifier: ^2.8.6
version: 2.8.6 version: 2.8.6
@ -68,6 +74,9 @@ importers:
helmet: helmet:
specifier: ^8.1.0 specifier: ^8.1.0
version: 8.1.0 version: 8.1.0
resend:
specifier: ^6.12.2
version: 6.12.2
ws: ws:
specifier: ^8.20.0 specifier: ^8.20.0
version: 8.20.0 version: 8.20.0
@ -98,7 +107,7 @@ importers:
version: link:../../packages/shared version: link:../../packages/shared
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) version: 4.2.2(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
'@tanstack/react-router': '@tanstack/react-router':
specifier: ^1.168.1 specifier: ^1.168.1
version: 1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -107,20 +116,23 @@ importers:
version: 1.166.10(@tanstack/react-router@1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.1)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.166.10(@tanstack/react-router@1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.1)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
better-auth: better-auth:
specifier: ^1.6.2 specifier: ^1.6.2
version: 1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) version: 1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))
react: react:
specifier: ^19.2.4 specifier: ^19.2.4
version: 19.2.4 version: 19.2.4
react-dom: react-dom:
specifier: ^19.2.4 specifier: ^19.2.4
version: 19.2.4(react@19.2.4) version: 19.2.4(react@19.2.4)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwindcss: tailwindcss:
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2 version: 4.2.2
devDependencies: devDependencies:
'@tanstack/router-plugin': '@tanstack/router-plugin':
specifier: ^1.167.2 specifier: ^1.167.2
version: 1.167.2(@tanstack/react-router@1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) version: 1.167.2(@tanstack/react-router@1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
'@types/node': '@types/node':
specifier: ^24.12.0 specifier: ^24.12.0
version: 24.12.0 version: 24.12.0
@ -132,13 +144,13 @@ importers:
version: 19.2.3(@types/react@19.2.14) version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) version: 6.0.1(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
jsdom: jsdom:
specifier: ^29.0.1 specifier: ^29.0.1
version: 29.0.1(@noble/hashes@2.2.0) version: 29.0.1(@noble/hashes@2.2.0)
vite: vite:
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) version: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
data-pipeline: data-pipeline:
dependencies: dependencies:
@ -1084,6 +1096,9 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.7': '@rolldown/pluginutils@1.0.0-rc.7':
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
'@stablelib/base64@1.0.1':
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
'@standard-schema/spec@1.1.0': '@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@ -1482,14 +1497,26 @@ packages:
ajv@6.14.0: ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ansi-escapes@7.3.0:
resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==}
engines: {node: '>=18'}
ansi-regex@5.0.1: ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0: ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'} engines: {node: '>=8'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
ansis@4.2.0: ansis@4.2.0:
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -1674,6 +1701,14 @@ packages:
chownr@1.1.4: chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
cli-truncate@5.2.0:
resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
engines: {node: '>=20'}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1693,10 +1728,17 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
combined-stream@1.0.8: combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
component-emitter@1.3.1: component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
@ -1909,6 +1951,9 @@ packages:
electron-to-chromium@1.5.321: electron-to-chromium@1.5.321:
resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@ -1927,6 +1972,10 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
es-define-property@1.0.1: es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2043,6 +2092,9 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
expand-template@2.0.3: expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2073,6 +2125,9 @@ packages:
fast-safe-stringify@2.1.1: fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fast-sha256@1.3.0:
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -2147,6 +2202,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*} engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.5.0:
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
engines: {node: '>=18'}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2218,6 +2277,11 @@ packages:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
iconv-lite@0.7.2: iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2263,6 +2327,10 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'} engines: {node: '>=8'}
is-fullwidth-code-point@5.1.0:
resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==}
engines: {node: '>=18'}
is-glob@4.0.3: is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2422,10 +2490,23 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
lint-staged@16.4.0:
resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==}
engines: {node: '>=20.17'}
hasBin: true
listr2@9.0.5:
resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
engines: {node: '>=20.0.0'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
log-update@6.1.0:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
lru-cache@11.2.7: lru-cache@11.2.7:
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@ -2483,6 +2564,10 @@ packages:
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
hasBin: true hasBin: true
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
mimic-response@3.1.0: mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2548,6 +2633,10 @@ packages:
once@1.4.0: once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -2626,6 +2715,9 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
postal-mime@2.7.4:
resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==}
postcss@8.5.8: postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -2717,9 +2809,25 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
resend@6.12.2:
resolution: {integrity: sha512-xwgmU4b0OqoabJsIoK/x0Whk0Fcs3bpbK4i/DEWPiE5hYJHyHl0TbB6QbI3gIr+bLdLUJ1GYm/fe41aVFuHXgw==}
engines: {node: '>=20'}
peerDependencies:
'@react-email/render': '*'
peerDependenciesMeta:
'@react-email/render':
optional: true
resolve-pkg-maps@1.0.0: resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rolldown@1.0.0-rc.10: rolldown@1.0.0-rc.10:
resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@ -2812,12 +2920,30 @@ packages:
siginfo@2.0.0: siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-concat@1.0.1: simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
simple-get@4.0.1: simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
slice-ansi@7.1.2:
resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==}
engines: {node: '>=18'}
slice-ansi@8.0.0:
resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==}
engines: {node: '>=20'}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2844,6 +2970,9 @@ packages:
stackback@0.0.2: stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
standardwebhooks@1.0.0:
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
statuses@2.0.2: statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -2851,10 +2980,22 @@ packages:
std-env@4.0.0: std-env@4.0.0:
resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
string-width@4.2.3: string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string-width@8.2.1:
resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==}
engines: {node: '>=20'}
string_decoder@1.3.0: string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@ -2862,6 +3003,10 @@ packages:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'} engines: {node: '>=8'}
strip-ansi@7.2.0:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
strip-json-comments@2.0.1: strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2882,6 +3027,9 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
svix@1.90.0:
resolution: {integrity: sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==}
symbol-tree@3.2.4: symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@ -3019,6 +3167,11 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
vary@1.1.2: vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -3146,6 +3299,10 @@ packages:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@ -3184,6 +3341,11 @@ packages:
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yaml@2.8.3:
resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1: yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -3816,6 +3978,8 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.7': {} '@rolldown/pluginutils@1.0.0-rc.7': {}
'@stablelib/base64@1.0.1': {}
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
'@tailwindcss/node@4.2.2': '@tailwindcss/node@4.2.2':
@ -3879,12 +4043,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2
'@tailwindcss/vite@4.2.2(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': '@tailwindcss/vite@4.2.2(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
'@tailwindcss/node': 4.2.2 '@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2 '@tailwindcss/oxide': 4.2.2
tailwindcss: 4.2.2 tailwindcss: 4.2.2
vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
'@tanstack/eslint-plugin-router@1.161.6(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': '@tanstack/eslint-plugin-router@1.161.6(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
@ -3956,7 +4120,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@tanstack/router-plugin@1.167.2(@tanstack/react-router@1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': '@tanstack/router-plugin@1.167.2(@tanstack/react-router@1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
@ -3973,7 +4137,7 @@ snapshots:
zod: 3.25.76 zod: 3.25.76
optionalDependencies: optionalDependencies:
'@tanstack/react-router': 1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-router': 1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -4192,12 +4356,12 @@ snapshots:
'@typescript-eslint/types': 8.57.1 '@typescript-eslint/types': 8.57.1
eslint-visitor-keys: 5.0.1 eslint-visitor-keys: 5.0.1
'@vitejs/plugin-react@6.0.1(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': '@vitejs/plugin-react@6.0.1(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7 '@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
'@vitest/coverage-v8@4.1.0(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))': '@vitest/coverage-v8@4.1.0(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))':
dependencies: dependencies:
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.0 '@vitest/utils': 4.1.0
@ -4209,7 +4373,7 @@ snapshots:
obug: 2.1.1 obug: 2.1.1
std-env: 4.0.0 std-env: 4.0.0
tinyrainbow: 3.1.0 tinyrainbow: 3.1.0
vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/expect@4.1.0': '@vitest/expect@4.1.0':
dependencies: dependencies:
@ -4220,22 +4384,22 @@ snapshots:
chai: 6.2.2 chai: 6.2.2
tinyrainbow: 3.1.0 tinyrainbow: 3.1.0
'@vitest/mocker@4.1.0(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': '@vitest/mocker@4.1.0(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
'@vitest/spy': 4.1.0 '@vitest/spy': 4.1.0
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: optionalDependencies:
vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
optional: true optional: true
'@vitest/mocker@4.1.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': '@vitest/mocker@4.1.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
'@vitest/spy': 4.1.0 '@vitest/spy': 4.1.0
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: optionalDependencies:
vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
'@vitest/pretty-format@4.1.0': '@vitest/pretty-format@4.1.0':
dependencies: dependencies:
@ -4281,12 +4445,20 @@ snapshots:
json-schema-traverse: 0.4.1 json-schema-traverse: 0.4.1
uri-js: 4.4.1 uri-js: 4.4.1
ansi-escapes@7.3.0:
dependencies:
environment: 1.1.0
ansi-regex@5.0.1: {} ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0: ansi-styles@4.3.0:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
ansi-styles@6.2.3: {}
ansis@4.2.0: {} ansis@4.2.0: {}
anymatch@3.1.3: anymatch@3.1.3:
@ -4325,7 +4497,7 @@ snapshots:
baseline-browser-mapping@2.10.9: {} baseline-browser-mapping@2.10.9: {}
better-auth@1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))): better-auth@1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))):
dependencies: dependencies:
'@better-auth/core': 1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0) '@better-auth/core': 1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0)
'@better-auth/drizzle-adapter': 1.6.2(@better-auth/core@1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0)) '@better-auth/drizzle-adapter': 1.6.2(@better-auth/core@1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))
@ -4351,12 +4523,12 @@ snapshots:
pg: 8.20.0 pg: 8.20.0
react: 19.2.4 react: 19.2.4
react-dom: 19.2.4(react@19.2.4) react-dom: 19.2.4(react@19.2.4)
vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies: transitivePeerDependencies:
- '@cloudflare/workers-types' - '@cloudflare/workers-types'
- '@opentelemetry/api' - '@opentelemetry/api'
better-auth@1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))): better-auth@1.6.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))):
dependencies: dependencies:
'@better-auth/core': 1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0) '@better-auth/core': 1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0)
'@better-auth/drizzle-adapter': 1.6.2(@better-auth/core@1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0)) '@better-auth/drizzle-adapter': 1.6.2(@better-auth/core@1.6.2(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.2.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.9.0)(kysely@0.28.16)(pg@8.20.0))
@ -4382,7 +4554,7 @@ snapshots:
pg: 8.20.0 pg: 8.20.0
react: 19.2.4 react: 19.2.4
react-dom: 19.2.4(react@19.2.4) react-dom: 19.2.4(react@19.2.4)
vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies: transitivePeerDependencies:
- '@cloudflare/workers-types' - '@cloudflare/workers-types'
- '@opentelemetry/api' - '@opentelemetry/api'
@ -4494,6 +4666,15 @@ snapshots:
chownr@1.1.4: {} chownr@1.1.4: {}
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
cli-truncate@5.2.0:
dependencies:
slice-ansi: 8.0.0
string-width: 8.2.1
cliui@8.0.1: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@ -4510,10 +4691,14 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
colorette@2.0.20: {}
combined-stream@1.0.8: combined-stream@1.0.8:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
commander@14.0.3: {}
component-emitter@1.3.1: {} component-emitter@1.3.1: {}
concurrently@9.2.1: concurrently@9.2.1:
@ -4623,6 +4808,8 @@ snapshots:
electron-to-chromium@1.5.321: {} electron-to-chromium@1.5.321: {}
emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
encodeurl@2.0.0: {} encodeurl@2.0.0: {}
@ -4638,6 +4825,8 @@ snapshots:
entities@6.0.1: {} entities@6.0.1: {}
environment@1.1.0: {}
es-define-property@1.0.1: {} es-define-property@1.0.1: {}
es-errors@1.3.0: {} es-errors@1.3.0: {}
@ -4837,6 +5026,8 @@ snapshots:
etag@1.8.1: {} etag@1.8.1: {}
eventemitter3@5.0.4: {}
expand-template@2.0.3: {} expand-template@2.0.3: {}
expect-type@1.3.0: {} expect-type@1.3.0: {}
@ -4887,6 +5078,8 @@ snapshots:
fast-safe-stringify@2.1.1: {} fast-safe-stringify@2.1.1: {}
fast-sha256@1.3.0: {}
fdir@6.5.0(picomatch@4.0.3): fdir@6.5.0(picomatch@4.0.3):
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
@ -4955,6 +5148,8 @@ snapshots:
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
get-east-asian-width@1.5.0: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -5031,6 +5226,8 @@ snapshots:
statuses: 2.0.2 statuses: 2.0.2
toidentifier: 1.0.1 toidentifier: 1.0.1
husky@9.1.7: {}
iconv-lite@0.7.2: iconv-lite@0.7.2:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
@ -5059,6 +5256,10 @@ snapshots:
is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@3.0.0: {}
is-fullwidth-code-point@5.1.0:
dependencies:
get-east-asian-width: 1.5.0
is-glob@4.0.3: is-glob@4.0.3:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
@ -5190,10 +5391,36 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0
lint-staged@16.4.0:
dependencies:
commander: 14.0.3
listr2: 9.0.5
picomatch: 4.0.3
string-argv: 0.3.2
tinyexec: 1.0.4
yaml: 2.8.3
listr2@9.0.5:
dependencies:
cli-truncate: 5.2.0
colorette: 2.0.20
eventemitter3: 5.0.4
log-update: 6.1.0
rfdc: 1.4.1
wrap-ansi: 9.0.2
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
log-update@6.1.0:
dependencies:
ansi-escapes: 7.3.0
cli-cursor: 5.0.0
slice-ansi: 7.1.2
strip-ansi: 7.2.0
wrap-ansi: 9.0.2
lru-cache@11.2.7: {} lru-cache@11.2.7: {}
lru-cache@5.1.1: lru-cache@5.1.1:
@ -5238,6 +5465,8 @@ snapshots:
mime@2.6.0: {} mime@2.6.0: {}
mimic-function@5.0.1: {}
mimic-response@3.1.0: {} mimic-response@3.1.0: {}
minimatch@10.2.4: minimatch@10.2.4:
@ -5282,6 +5511,10 @@ snapshots:
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@ -5354,6 +5587,8 @@ snapshots:
picomatch@4.0.3: {} picomatch@4.0.3: {}
postal-mime@2.7.4: {}
postcss@8.5.8: postcss@8.5.8:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
@ -5450,8 +5685,20 @@ snapshots:
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
resend@6.12.2:
dependencies:
postal-mime: 2.7.4
svix: 1.90.0
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0: {}
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
signal-exit: 4.1.0
rfdc@1.4.1: {}
rolldown@1.0.0-rc.10: rolldown@1.0.0-rc.10:
dependencies: dependencies:
'@oxc-project/types': 0.120.0 '@oxc-project/types': 0.120.0
@ -5576,6 +5823,8 @@ snapshots:
siginfo@2.0.0: {} siginfo@2.0.0: {}
signal-exit@4.1.0: {}
simple-concat@1.0.1: {} simple-concat@1.0.1: {}
simple-get@4.0.1: simple-get@4.0.1:
@ -5584,6 +5833,21 @@ snapshots:
once: 1.4.0 once: 1.4.0
simple-concat: 1.0.1 simple-concat: 1.0.1
slice-ansi@7.1.2:
dependencies:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.1.0
slice-ansi@8.0.0:
dependencies:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.1.0
sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map-support@0.5.21: source-map-support@0.5.21:
@ -5603,16 +5867,34 @@ snapshots:
stackback@0.0.2: {} stackback@0.0.2: {}
standardwebhooks@1.0.0:
dependencies:
'@stablelib/base64': 1.0.1
fast-sha256: 1.3.0
statuses@2.0.2: {} statuses@2.0.2: {}
std-env@4.0.0: {} std-env@4.0.0: {}
string-argv@0.3.2: {}
string-width@4.2.3: string-width@4.2.3:
dependencies: dependencies:
emoji-regex: 8.0.0 emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0 is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1 strip-ansi: 6.0.1
string-width@7.2.0:
dependencies:
emoji-regex: 10.6.0
get-east-asian-width: 1.5.0
strip-ansi: 7.2.0
string-width@8.2.1:
dependencies:
get-east-asian-width: 1.5.0
strip-ansi: 7.2.0
string_decoder@1.3.0: string_decoder@1.3.0:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
@ -5621,6 +5903,10 @@ snapshots:
dependencies: dependencies:
ansi-regex: 5.0.1 ansi-regex: 5.0.1
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.2.2
strip-json-comments@2.0.1: {} strip-json-comments@2.0.1: {}
superagent@10.3.0: superagent@10.3.0:
@ -5653,6 +5939,11 @@ snapshots:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
svix@1.90.0:
dependencies:
standardwebhooks: 1.0.0
uuid: 10.0.0
symbol-tree@3.2.4: {} symbol-tree@3.2.4: {}
tailwindcss@4.2.2: {} tailwindcss@4.2.2: {}
@ -5783,9 +6074,11 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@10.0.0: {}
vary@1.1.2: {} vary@1.1.2: {}
vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.3 picomatch: 4.0.3
@ -5798,8 +6091,9 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.6.1
tsx: 4.21.0 tsx: 4.21.0
yaml: 2.8.3
vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.3 picomatch: 4.0.3
@ -5812,11 +6106,12 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.6.1
tsx: 4.21.0 tsx: 4.21.0
yaml: 2.8.3
vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies: dependencies:
'@vitest/expect': 4.1.0 '@vitest/expect': 4.1.0
'@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/pretty-format': 4.1.0 '@vitest/pretty-format': 4.1.0
'@vitest/runner': 4.1.0 '@vitest/runner': 4.1.0
'@vitest/snapshot': 4.1.0 '@vitest/snapshot': 4.1.0
@ -5833,7 +6128,7 @@ snapshots:
tinyexec: 1.0.4 tinyexec: 1.0.4
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinyrainbow: 3.1.0 tinyrainbow: 3.1.0
vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@opentelemetry/api': 1.9.1 '@opentelemetry/api': 1.9.1
@ -5843,10 +6138,10 @@ snapshots:
- msw - msw
optional: true optional: true
vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies: dependencies:
'@vitest/expect': 4.1.0 '@vitest/expect': 4.1.0
'@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/pretty-format': 4.1.0 '@vitest/pretty-format': 4.1.0
'@vitest/runner': 4.1.0 '@vitest/runner': 4.1.0
'@vitest/snapshot': 4.1.0 '@vitest/snapshot': 4.1.0
@ -5863,7 +6158,7 @@ snapshots:
tinyexec: 1.0.4 tinyexec: 1.0.4
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinyrainbow: 3.1.0 tinyrainbow: 3.1.0
vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@opentelemetry/api': 1.9.1 '@opentelemetry/api': 1.9.1
@ -5911,6 +6206,12 @@ snapshots:
string-width: 4.2.3 string-width: 4.2.3
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi@9.0.2:
dependencies:
ansi-styles: 6.2.3
string-width: 7.2.0
strip-ansi: 7.2.0
wrappy@1.0.2: {} wrappy@1.0.2: {}
ws@8.20.0: {} ws@8.20.0: {}
@ -5935,6 +6236,8 @@ snapshots:
yallist@3.1.1: {} yallist@3.1.1: {}
yaml@2.8.3: {}
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}
yargs@17.7.2: yargs@17.7.2: