diff --git a/.env.example b/.env.example index eba6428..0c7804a 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,4 @@ DATABASE_URL=postgres://postgres:mypassword@db-host:5432/databasename -DATABASE_URL_LOCAL=postgres://postgres:mypassword@localhost:5432/databasename POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres @@ -11,11 +10,3 @@ GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= - -VITE_WS_URL= - -UID=1000 -GID=1000 - -RESEND_API_KEY= -EMAIL_FROM=mail@example.com diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index dc34ae3..a48cee1 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -5,31 +5,8 @@ on: branches: [main] 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: runs-on: docker - needs: quality steps: - name: Install tools run: apt-get update && apt-get install -y docker.io openssh-client diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index cb2c84d..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm lint-staged diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 6c45485..0000000 --- a/.husky/pre-push +++ /dev/null @@ -1 +0,0 @@ -pnpm test:run diff --git a/.prettierignore b/.prettierignore index 773c5d1..9699559 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,5 +18,3 @@ coverage/ pnpm-lock.yaml routeTree.gen.ts - -.pnpm-store/ diff --git a/apps/api/package.json b/apps/api/package.json index 1a7cdc3..870a77d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -7,8 +7,7 @@ "dev": "pnpm --filter shared build && pnpm --filter db build && tsx watch src/server.ts", "build": "tsc", "start": "node dist/src/server.js", - "test": "vitest", - "typecheck": "tsc --noEmit" + "test": "vitest" }, "dependencies": { "@lila/db": "workspace:*", @@ -18,7 +17,6 @@ "express": "^5.2.1", "express-rate-limit": "^8.4.0", "helmet": "^8.1.0", - "resend": "^6.12.2", "ws": "^8.20.0" }, "devDependencies": { diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index ad9f509..47c51e6 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -27,7 +27,7 @@ export function createApp() { const store = new InMemoryGameSessionStore(); app.use("/api/v1", createApiRouter(store)); - + app.use(errorHandler); return app; diff --git a/apps/api/src/controllers/gameController.test.ts b/apps/api/src/controllers/gameController.test.ts index 0351c2a..168d2d1 100644 --- a/apps/api/src/controllers/gameController.test.ts +++ b/apps/api/src/controllers/gameController.test.ts @@ -197,7 +197,7 @@ describe("POST /api/v1/game/answer", () => { expect(body.success).toBe(false); expect(body.error).toContain("Question already answered"); }); - + it("returns 400 when a field has an invalid value", async () => { const res = await request(app) .post("/api/v1/game/start") diff --git a/apps/api/src/errors/AppError.ts b/apps/api/src/errors/AppError.ts index bc5a92a..7805f3e 100644 --- a/apps/api/src/errors/AppError.ts +++ b/apps/api/src/errors/AppError.ts @@ -30,4 +30,4 @@ export class UnprocessableEntityError extends AppError { constructor(message: string) { super(message, 422); } -} +} \ No newline at end of file diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts index fe41e35..8e2b818 100644 --- a/apps/api/src/lib/auth.ts +++ b/apps/api/src/lib/auth.ts @@ -1,11 +1,8 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { Resend } from "resend"; import { db } from "@lila/db"; import * as schema from "@lila/db/schema"; -const emailFrom = process.env["EMAIL_FROM"] ?? "noreply@lilastudy.com"; - export const auth = betterAuth({ baseURL: process.env["BETTER_AUTH_URL"] || "http://localhost:3000", advanced: { @@ -19,44 +16,6 @@ export const auth = betterAuth({ }, }, 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: `

Click here to reset your password. This link expires in 1 hour.

`, - }); - }, - }, - 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: `

Click here to verify your email address.

`, - }); - }, - }, trustedOrigins: [process.env["CORS_ORIGIN"] || "http://localhost:5173"], socialProviders: { google: { diff --git a/apps/api/src/routes/gameRouter.ts b/apps/api/src/routes/gameRouter.ts index a40622d..9e29a5d 100644 --- a/apps/api/src/routes/gameRouter.ts +++ b/apps/api/src/routes/gameRouter.ts @@ -14,6 +14,6 @@ export const createGameRouter = (store: GameSessionStore): Router => { router.post("/start", controller.createGame as express.RequestHandler); router.post("/answer", controller.submitAnswer as express.RequestHandler); - + return router; }; diff --git a/apps/web/package.json b/apps/web/package.json index 8068c8f..922f6f9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,8 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview", - "typecheck": "tsc --noEmit" + "preview": "vite preview" }, "dependencies": { "@lila/shared": "workspace:*", @@ -17,7 +16,6 @@ "better-auth": "^1.6.2", "react": "^19.2.4", "react-dom": "^19.2.4", - "sonner": "^2.0.7", "tailwindcss": "^4.2.2" }, "devDependencies": { diff --git a/apps/web/src/components/auth/AuthModal.tsx b/apps/web/src/components/auth/AuthModal.tsx deleted file mode 100644 index 01b2c20..0000000 --- a/apps/web/src/components/auth/AuthModal.tsx +++ /dev/null @@ -1,249 +0,0 @@ -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 ( -
{ - e.preventDefault(); - void handleSubmit(); - }} - className="flex flex-col gap-3" - > - 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)" - /> - 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)" - /> -
- - Forgot password? - -
- -
- ); -}; - -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 ( -
{ - e.preventDefault(); - void handleSubmit(); - }} - className="flex flex-col gap-3" - > - 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)" - /> - 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)" - /> - 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)" - /> - -
- ); -}; - -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 ( -
-
-
- - or continue with - -
-
- - -
- ); -}; - -export const AuthModal = ({ onClose, onSuccess }: AuthModalProps) => { - const [tab, setTab] = useState("login"); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [onClose]); - - return ( -
-
e.stopPropagation()} - > - {/* Close button */} - - - {/* Header */} -
-

- lila -

-
- - {/* Tabs */} -
- {(["login", "register"] as Tab[]).map((t) => ( - - ))} -
- - {tab === "login" ? ( - - ) : ( - - )} - - {/* Social */} - -
-
- ); -}; diff --git a/apps/web/src/components/landing/Hero.tsx b/apps/web/src/components/landing/Hero.tsx index 238d313..81f7bba 100644 --- a/apps/web/src/components/landing/Hero.tsx +++ b/apps/web/src/components/landing/Hero.tsx @@ -66,15 +66,13 @@ const Hero = () => { ) : ( <> Get started Log in diff --git a/apps/web/src/components/navbar/NavAuth.tsx b/apps/web/src/components/navbar/NavAuth.tsx index f65f569..22b8479 100644 --- a/apps/web/src/components/navbar/NavAuth.tsx +++ b/apps/web/src/components/navbar/NavAuth.tsx @@ -24,14 +24,13 @@ const NavAuth = () => { ) : ( - Login + Sign in )}
diff --git a/apps/web/src/components/navbar/NavLogin.tsx b/apps/web/src/components/navbar/NavLogin.tsx new file mode 100644 index 0000000..f28bfdd --- /dev/null +++ b/apps/web/src/components/navbar/NavLogin.tsx @@ -0,0 +1,17 @@ +import { Link } from "@tanstack/react-router"; + +const NavLogin = () => { + return ( + + Login + + ); +}; + +export default NavLogin; diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index a85f2f2..96c3044 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -9,21 +9,15 @@ // 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 ResetPasswordRouteImport } from './routes/reset-password' import { Route as PlayRouteImport } from './routes/play' import { Route as MultiplayerRouteImport } from './routes/multiplayer' -import { Route as ForgotPasswordRouteImport } from './routes/forgot-password' +import { Route as LoginRouteImport } from './routes/login' import { Route as AboutRouteImport } from './routes/about' import { Route as IndexRouteImport } from './routes/index' import { Route as MultiplayerIndexRouteImport } from './routes/multiplayer/index' import { Route as MultiplayerLobbyCodeRouteImport } from './routes/multiplayer/lobby.$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({ id: '/play', path: '/play', @@ -34,9 +28,9 @@ const MultiplayerRoute = MultiplayerRouteImport.update({ path: '/multiplayer', getParentRoute: () => rootRouteImport, } as any) -const ForgotPasswordRoute = ForgotPasswordRouteImport.update({ - id: '/forgot-password', - path: '/forgot-password', +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', getParentRoute: () => rootRouteImport, } as any) const AboutRoute = AboutRouteImport.update({ @@ -68,10 +62,9 @@ const MultiplayerGameCodeRoute = MultiplayerGameCodeRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/about': typeof AboutRoute - '/forgot-password': typeof ForgotPasswordRoute + '/login': typeof LoginRoute '/multiplayer': typeof MultiplayerRouteWithChildren '/play': typeof PlayRoute - '/reset-password': typeof ResetPasswordRoute '/multiplayer/': typeof MultiplayerIndexRoute '/multiplayer/game/$code': typeof MultiplayerGameCodeRoute '/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute @@ -79,9 +72,8 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/about': typeof AboutRoute - '/forgot-password': typeof ForgotPasswordRoute + '/login': typeof LoginRoute '/play': typeof PlayRoute - '/reset-password': typeof ResetPasswordRoute '/multiplayer': typeof MultiplayerIndexRoute '/multiplayer/game/$code': typeof MultiplayerGameCodeRoute '/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute @@ -90,10 +82,9 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/about': typeof AboutRoute - '/forgot-password': typeof ForgotPasswordRoute + '/login': typeof LoginRoute '/multiplayer': typeof MultiplayerRouteWithChildren '/play': typeof PlayRoute - '/reset-password': typeof ResetPasswordRoute '/multiplayer/': typeof MultiplayerIndexRoute '/multiplayer/game/$code': typeof MultiplayerGameCodeRoute '/multiplayer/lobby/$code': typeof MultiplayerLobbyCodeRoute @@ -103,10 +94,9 @@ export interface FileRouteTypes { fullPaths: | '/' | '/about' - | '/forgot-password' + | '/login' | '/multiplayer' | '/play' - | '/reset-password' | '/multiplayer/' | '/multiplayer/game/$code' | '/multiplayer/lobby/$code' @@ -114,9 +104,8 @@ export interface FileRouteTypes { to: | '/' | '/about' - | '/forgot-password' + | '/login' | '/play' - | '/reset-password' | '/multiplayer' | '/multiplayer/game/$code' | '/multiplayer/lobby/$code' @@ -124,10 +113,9 @@ export interface FileRouteTypes { | '__root__' | '/' | '/about' - | '/forgot-password' + | '/login' | '/multiplayer' | '/play' - | '/reset-password' | '/multiplayer/' | '/multiplayer/game/$code' | '/multiplayer/lobby/$code' @@ -136,21 +124,13 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute AboutRoute: typeof AboutRoute - ForgotPasswordRoute: typeof ForgotPasswordRoute + LoginRoute: typeof LoginRoute MultiplayerRoute: typeof MultiplayerRouteWithChildren PlayRoute: typeof PlayRoute - ResetPasswordRoute: typeof ResetPasswordRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/reset-password': { - id: '/reset-password' - path: '/reset-password' - fullPath: '/reset-password' - preLoaderRoute: typeof ResetPasswordRouteImport - parentRoute: typeof rootRouteImport - } '/play': { id: '/play' path: '/play' @@ -165,11 +145,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MultiplayerRouteImport parentRoute: typeof rootRouteImport } - '/forgot-password': { - id: '/forgot-password' - path: '/forgot-password' - fullPath: '/forgot-password' - preLoaderRoute: typeof ForgotPasswordRouteImport + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport parentRoute: typeof rootRouteImport } '/about': { @@ -229,10 +209,9 @@ const MultiplayerRouteWithChildren = MultiplayerRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AboutRoute: AboutRoute, - ForgotPasswordRoute: ForgotPasswordRoute, + LoginRoute: LoginRoute, MultiplayerRoute: MultiplayerRouteWithChildren, PlayRoute: PlayRoute, - ResetPasswordRoute: ResetPasswordRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 7df9998..c672ced 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,39 +1,16 @@ -import { - createRootRoute, - Outlet, - useNavigate, - useSearch, -} from "@tanstack/react-router"; +import { createRootRoute, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; -import { Toaster } from "sonner"; import Navbar from "../components/navbar/NavBar"; import NotFound from "../components/NotFound"; import RootError from "../components/RootError"; -import { AuthModal } from "../components/auth/AuthModal"; -import { AuthModalSearchSchema } from "@lila/shared"; 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 ( <>
- {modal === "auth" && ( - - )} - ); @@ -43,5 +20,4 @@ export const Route = createRootRoute({ component: RootLayout, notFoundComponent: NotFound, errorComponent: RootError, - validateSearch: AuthModalSearchSchema, }); diff --git a/apps/web/src/routes/forgot-password.tsx b/apps/web/src/routes/forgot-password.tsx deleted file mode 100644 index 9d929a8..0000000 --- a/apps/web/src/routes/forgot-password.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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 ( -
-
-

- Forgot password -

-

- Enter your email and we'll send you a reset link. -

-
- -
{ - e.preventDefault(); - void handleSubmit(); - }} - className="w-full flex flex-col gap-3" - > - 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)" - /> - -
- - - Back to home - -
- ); -} - -export const Route = createFileRoute("/forgot-password")({ - component: ForgotPasswordPage, -}); diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx new file mode 100644 index 0000000..8451d41 --- /dev/null +++ b/apps/web/src/routes/login.tsx @@ -0,0 +1,46 @@ +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
Loading...
; + + if (session) { + void navigate({ to: "/" }); + return null; + } + + return ( +
+

sign in to lila

+ + +
+ ); +}; + +export const Route = createFileRoute("/login")({ component: LoginPage }); diff --git a/apps/web/src/routes/multiplayer.tsx b/apps/web/src/routes/multiplayer.tsx index 0008b37..7adffd6 100644 --- a/apps/web/src/routes/multiplayer.tsx +++ b/apps/web/src/routes/multiplayer.tsx @@ -14,10 +14,7 @@ export const Route = createFileRoute("/multiplayer")({ beforeLoad: async () => { const { data: session } = await authClient.getSession(); if (!session) { - throw redirect({ - to: "/", - search: { modal: "auth", redirect: "/multiplayer" }, - }); + throw redirect({ to: "/login" }); } return { session }; }, diff --git a/apps/web/src/routes/play.tsx b/apps/web/src/routes/play.tsx index bc4cde3..df4959d 100644 --- a/apps/web/src/routes/play.tsx +++ b/apps/web/src/routes/play.tsx @@ -132,7 +132,7 @@ export const Route = createFileRoute("/play")({ beforeLoad: async () => { const { data: session } = await authClient.getSession(); if (!session) { - throw redirect({ to: "/", search: { modal: "auth", redirect: "/play" } }); + throw redirect({ to: "/login" }); } }, }); diff --git a/apps/web/src/routes/reset-password.tsx b/apps/web/src/routes/reset-password.tsx deleted file mode 100644 index 837949b..0000000 --- a/apps/web/src/routes/reset-password.tsx +++ /dev/null @@ -1,91 +0,0 @@ -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 ( -
-

- Invalid link -

-

- This reset link is invalid or has expired. -

- - Request a new one - -
- ); - } - - 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 ( -
-
-

- Reset password -

-

- Enter your new password below. -

-
- -
{ - e.preventDefault(); - void handleSubmit(); - }} - className="w-full flex flex-col gap-3" - > - 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)" - /> - -
-
- ); -} - -export const Route = createFileRoute("/reset-password")({ - component: ResetPasswordPage, - validateSearch: ResetPasswordSearchSchema, -}); diff --git a/documentation/backlog.md b/documentation/backlog.md index 35a5f05..6a91e2d 100644 --- a/documentation/backlog.md +++ b/documentation/backlog.md @@ -90,6 +90,9 @@ Directionally right, timing is unclear. Revisit when the next/now work is done. - **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. +- **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]` 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. @@ -102,7 +105,6 @@ Directionally right, timing is unclear. Revisit when the next/now work is done. 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 - 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. diff --git a/documentation/notes.md b/documentation/notes.md index 45f8955..4391d87 100644 --- a/documentation/notes.md +++ b/documentation/notes.md @@ -12,8 +12,7 @@ task description. 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 6. if we have multiple options to do something, also always provide options that reflect current industry standards and best practices -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. +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. ## tasks diff --git a/documentation/roasts/gameService.md b/documentation/roasts/gameService.md index ac3d2ed..73283de 100644 --- a/documentation/roasts/gameService.md +++ b/documentation/roasts/gameService.md @@ -1,11 +1,11 @@ # ๐Ÿ”ฅ 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 **File Roasted:** `gameService.ts` **Date:** $(date) -**Roaster:** Qwen3.6 +**Roaster:** Qwen3.6 --- @@ -28,8 +28,8 @@ **Location:** `gameService.ts:45-58` + `InMemoryGameSessionStore.ts:update()` // Current flow (VULNERABLE): -const session = await store.get(submission.sessionId); // READ -const updatedAnswers = new Map(session.answers); // MODIFY (local copy) +const session = await store.get(submission.sessionId); // READ +const updatedAnswers = new Map(session.answers); // MODIFY (local copy) updatedAnswers.delete(submission.questionId); await store.update(submission.sessionId, { answers: updatedAnswers }); // WRITE @@ -46,55 +46,64 @@ Fix Options: // Option A: Add atomic operation to store interface interface GameSessionStore { -deleteAnswer(sessionId: string, questionId: string): Promise; + deleteAnswer(sessionId: string, questionId: string): Promise; } // Option B: Use Valkey Lua script for atomic read-modify-write // Option C: Optimistic locking with version numbers -Priority: ๐Ÿ”ด CRITICAL โ€” Data integrity issue 2. N+1 Query: Database Performance Bomb +Priority: ๐Ÿ”ด CRITICAL โ€” Data integrity issue +2. N+1 Query: Database Performance Bomb Location: gameService.ts:24-26 + termModel.ts:getDistractors() // For each of N terms, we call getDistractors(): const questions: GameQuestion[] = await Promise.all( -terms.map(async (term) => { -const distractorTexts = await getDistractors(term.termId, ...); // ๐Ÿšฉ N queries! -}) + terms.map(async (term) => { + const distractorTexts = await getDistractors(term.termId, ...); // ๐Ÿšฉ N queries! + }) ); Impact Analysis: Rounds + DB Queries + At 50 concurrent users 3 + 1 + 3 = 4 + 200 queries/min 10 + 1 + 10 = 11 + 550 queries/min 20 + 1 + 20 = 21 + 1,050 queries/min Each getDistractors() runs: -SELECT text FROM terms -JOIN translations ON ... -WHERE pos = $1 AND difficulty = $2 AND term_id != $3 AND text != $4 +SELECT text FROM terms +JOIN translations ON ... +WHERE pos = $1 AND difficulty = $2 AND term_id != $3 AND text != $4 ORDER BY RANDOM() LIMIT 6 Fix: Batch Fetch Distractors // Fetch all distractors in ONE query const allDistractors = await db -.select({ termId: terms.id, text: translations.text }) -.from(terms) -.innerJoin(translations, /_ ... _/) -.where(and( -eq(terms.pos, pos), -eq(translations.difficulty, difficulty), -inArray(terms.id, termIds), // Batch! -)) -.limit(DISTRACTOR_FETCH_COUNT \* termIds.length); + .select({ termId: terms.id, text: translations.text }) + .from(terms) + .innerJoin(translations, /* ... */) + .where(and( + eq(terms.pos, pos), + eq(translations.difficulty, difficulty), + inArray(terms.id, termIds), // Batch! + )) + .limit(DISTRACTOR_FETCH_COUNT * termIds.length); // Group by termId in JS, then slice to 3 unique distractors per term const distractorsByTerm = groupByTermId(allDistractors); @@ -102,10 +111,10 @@ const distractorsByTerm = groupByTermId(allDistractors); Priority: ๐Ÿ”ด CRITICAL โ€” Performance/scalability issue 3. Error Handling Inconsistency - Location: gameService.ts:33-36 +Location: gameService.ts:33-36 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: @@ -118,14 +127,15 @@ Fix: import { UnprocessableEntityError } from "../errors/AppError.js"; if (uniqueDistractors.length < 3) { -logger.warn({ termId: term.termId, uniqueCount: uniqueDistractors.length }, -"insufficient_distractors"); -throw new UnprocessableEntityError( -`Not enough unique distractors for term: ${term.targetText}` -); + logger.warn({ termId: term.termId, uniqueCount: uniqueDistractors.length }, + "insufficient_distractors"); + throw new UnprocessableEntityError( + `Not enough unique distractors for term: ${term.targetText}` + ); } Priority: ๐ŸŸก HIGH โ€” Observability & UX issue -โš ๏ธ High-Severity Smells 4. Code Duplication: Singleplayer vs Multiplayer +โš ๏ธ High-Severity Smells +4. Code Duplication: Singleplayer vs Multiplayer Compare: gameService.ts vs multiplayerGameService.ts // gameService.ts const optionTexts = [term.targetText, ...uniqueDistractors.slice(0, 3)]; @@ -147,29 +157,30 @@ Fix: Extract pure function to @lila/shared or new @lila/game-logic: // packages/shared/src/game-logic.ts export const buildQuestionOptions = ( -correctAnswer: string, -distractors: string[], -optionCount: number = 4 + correctAnswer: string, + distractors: string[], + optionCount: number = 4 ): { options: AnswerOption[]; correctOptionId: number } => { -const uniqueDistractors = [...new Set(distractors.filter(d => d !== correctAnswer))]; -const optionTexts = [correctAnswer, ...uniqueDistractors.slice(0, optionCount - 1)]; -const shuffled = shuffleSecure(optionTexts); -const correctOptionId = shuffled.indexOf(correctAnswer); - -return { -options: shuffled.map((text, idx) => ({ optionId: idx, text })), -correctOptionId -}; + const uniqueDistractors = [...new Set(distractors.filter(d => d !== correctAnswer))]; + const optionTexts = [correctAnswer, ...uniqueDistractors.slice(0, optionCount - 1)]; + const shuffled = shuffleSecure(optionTexts); + const correctOptionId = shuffled.indexOf(correctAnswer); + + return { + options: shuffled.map((text, idx) => ({ optionId: idx, text })), + correctOptionId + }; }; -Priority: ๐ŸŸก HIGH โ€” Maintainability issue 5. Shuffle Bias: Math.random() Trap +Priority: ๐ŸŸก HIGH โ€” Maintainability issue +5. Shuffle Bias: Math.random() Trap Location: utils.ts:shuffleArray() + multiplayerGameService.ts:shuffle() export const shuffleArray = (array: T[]): T[] => { -for (let i = result.length - 1; i > 0; i--) { -const j = Math.floor(Math.random() \* (i + 1)); // ๐Ÿšฉ Modulo bias + non-crypto RNG -// ... -} + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); // ๐Ÿšฉ Modulo bias + non-crypto RNG + // ... + } }; The Math: @@ -188,64 +199,65 @@ Fix (if needed): import { randomBytes } from "crypto"; const shuffleSecure = (array: T[]): T[] => { -const result = [...array]; -for (let i = result.length - 1; i > 0; i--) { -// Use crypto.getRandomValues for better randomness -const rand = randomBytes(4).readUInt32LE(0); -const j = rand % (i + 1); -[result[i], result[j]] = [result[j], result[i]]; -} -return result; + const result = [...array]; + for (let i = result.length - 1; i > 0; i--) { + // Use crypto.getRandomValues for better randomness + const rand = randomBytes(4).readUInt32LE(0); + const j = rand % (i + 1); + [result[i], result[j]] = [result[j], result[i]]; + } + return result; }; Priority: ๐ŸŸข LOW โ€” Document tradeoff and move on for now -6. Test Coverage Gaps - File: gameService.test.ts - โœ… Well Tested: +6. Test Coverage Gaps +File: gameService.test.ts +โœ… Well Tested: - Happy path: session creation, answer evaluation - Edge cases: duplicate distractors, empty results, invalid inputs - Error propagation from DB layer + Happy path: session creation, answer evaluation + Edge cases: duplicate distractors, empty results, invalid inputs + Error propagation from DB layer โŒ Missing Tests: // 1. Concurrency test (race condition) it("rejects duplicate answers for same question under concurrent load", async () => { -const session = await createGameSession(validRequest, store, "user-1"); -const question = session.questions[0]!; - -// Submit two answers simultaneously -const [result1, result2] = await Promise.allSettled([ -evaluateAnswer({ sessionId, questionId, selectedOptionId: 0 }, store, "user-1"), -evaluateAnswer({ sessionId, questionId, selectedOptionId: 1 }, store, "user-1"), -]); - -// Exactly one should succeed, one should throw ConflictError -expect([result1, result2].filter(r => r.status === "fulfilled")).toHaveLength(1); + const session = await createGameSession(validRequest, store, "user-1"); + const question = session.questions[0]!; + + // Submit two answers simultaneously + const [result1, result2] = await Promise.allSettled([ + evaluateAnswer({ sessionId, questionId, selectedOptionId: 0 }, store, "user-1"), + evaluateAnswer({ sessionId, questionId, selectedOptionId: 1 }, store, "user-1"), + ]); + + // Exactly one should succeed, one should throw ConflictError + expect([result1, result2].filter(r => r.status === "fulfilled")).toHaveLength(1); }); // 2. TTL expiration test it("deletes session after TTL expires", async () => { -vi.useFakeTimers(); -const session = await createGameSession(validRequest, store, "user-1"); - -vi.advanceTimersByTime(31 _ 60 _ 1000); // 31 minutes - -await expect(store.get(session.sessionId)).resolves.toBeNull(); + vi.useFakeTimers(); + const session = await createGameSession(validRequest, store, "user-1"); + + vi.advanceTimersByTime(31 * 60 * 1000); // 31 minutes + + await expect(store.get(session.sessionId)).resolves.toBeNull(); }); // 3. Distractor fallback strategy test it("uses fallback when <3 unique distractors available", async () => { -mockGetDistractors.mockResolvedValue(["same", "same", "same", "same"]); -// Should either: (a) fetch from broader pool, or (b) reduce rounds gracefully + mockGetDistractors.mockResolvedValue(["same", "same", "same", "same"]); + // Should either: (a) fetch from broader pool, or (b) reduce rounds gracefully }); Priority: ๐ŸŸก HIGH โ€” Prevents regression on critical fixes -๐Ÿงผ Code Quality Nitpicks 7. Magic Numbers +๐Ÿงผ Code Quality Nitpicks +7. Magic Numbers // 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 .limit(count); // count=6, but why? @@ -255,16 +267,16 @@ optionId: z.number().int().min(0).max(3), // Why 4 options? 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 GAME_OPTION_COUNT = 4; export const MIN_UNIQUE_DISTRACTORS = 3; 8. Mutable Reference Leakage - Location: InMemoryGameSessionStore.ts:get() +Location: InMemoryGameSessionStore.ts:get() get(sessionId: string): Promise { -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. @@ -279,35 +291,37 @@ return Promise.resolve(entry.data as Readonly); // Option C: Use immutable data structures (overkill for now) 9. Zero Observability - Problem: No logging, no metrics. You're flying blind in production. - Minimal Fix (5 minutes): +Problem: No logging, no metrics. You're flying blind in production. +Minimal Fix (5 minutes): + + // apps/api/src/lib/logger.ts import pino from "pino"; -export const logger = pino({ -level: process.env.LOG_LEVEL || "info", -transport: process.env.NODE_ENV === "production" -? { target: "pino-pretty" } -: undefined +export const logger = pino({ + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV === "production" + ? { target: "pino-pretty" } + : undefined }); // In gameService.ts: import { logger } from "../lib/logger.js"; logger.info( -{ userId, sourceLang, targetLang, termCount: terms.length }, -"game_session_created" + { userId, sourceLang, targetLang, termCount: terms.length }, + "game_session_created" ); logger.debug( -{ sessionId, questionId, isCorrect, responseTimeMs }, -"answer_evaluated" + { sessionId, questionId, isCorrect, responseTimeMs }, + "answer_evaluated" ); Bonus: Export a Prometheus histogram for game_service_duration_seconds. 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 @@ -319,16 +333,16 @@ Reality Check: "Post-MVP" never comes without a ticket. Fix Options: -- Option A: Pre-computed random_seed column (updated nightly) -WHERE ... AND random_seed >= random() -ORDER BY random_seed +WHERE ... AND random_seed >= random() +ORDER BY random_seed LIMIT $1 -- Option B: TABLESAMPLE for approximate sampling (Postgres 9.5+) -FROM terms TABLESAMPLE SYSTEM(10) -WHERE ... +FROM terms TABLESAMPLE SYSTEM(10) +WHERE ... LIMIT $1 -- 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. \ No newline at end of file diff --git a/documentation/tickets/t00006.md b/documentation/tickets/t00006.md index edc5e25..8e777f0 100644 --- a/documentation/tickets/t00006.md +++ b/documentation/tickets/t00006.md @@ -84,10 +84,7 @@ Rejected because: for user-owned resources identified by opaque IDs, confirming 2. `GameSessionStore.ts` โ€” add `userId` to `GameSessionData`: ```ts - export type GameSessionData = { - answers: Map; - userId: string; - }; + export type GameSessionData = { answers: Map; userId: string }; ``` 3. `gameService.ts` โ€” add `userId` to both function signatures: @@ -103,11 +100,7 @@ Rejected because: for user-owned resources identified by opaque IDs, confirming Store it on create: ```ts - await store.create( - sessionId, - { answers: answerKey, userId }, - 30 * 60 * 1000, - ); + await store.create(sessionId, { answers: answerKey, userId }, 30 * 60 * 1000); ``` Assert on evaluate: @@ -121,7 +114,7 @@ Rejected because: for user-owned resources identified by opaque IDs, confirming 4. `gameController.ts` โ€” extract from authenticated request: ```ts - req.session.user.id; + req.session.user.id ``` 5. `gameRouter.ts` โ€” cast at registration: diff --git a/documentation/tickets/t00008.md b/documentation/tickets/t00008.md index 670f610..b171abc 100644 --- a/documentation/tickets/t00008.md +++ b/documentation/tickets/t00008.md @@ -27,9 +27,7 @@ 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: ```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)]; ``` diff --git a/eslint.config.mjs b/eslint.config.mjs index a88b6f1..290fa14 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,6 +10,8 @@ export default defineConfig([ globalIgnores([ "**/dist/**", "node_modules/", + "eslint.config.mjs", + "**/*.config.ts", "routeTree.gen.ts", "scripts/**", "data-pipeline/**/*", @@ -22,19 +24,12 @@ export default defineConfig([ { languageOptions: { parserOptions: { - projectService: { allowDefaultProject: ["*.mjs", "*.ts"] }, + projectService: true, 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}"], extends: [ @@ -48,9 +43,6 @@ export default defineConfig([ rules: { "react-refresh/only-export-components": "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", }, }, { diff --git a/package.json b/package.json index 8e9a1f6..d900474 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,11 @@ "scripts": { "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\"", - "prepare": "husky || true", "test": "vitest", "test:run": "vitest run", "lint": "eslint .", "format": "prettier --write .", - "format:check": "prettier --check .", - "typecheck": "pnpm -r typecheck" - }, - "lint-staged": { - "**/*.{ts,tsx}": [ - "prettier --write", - "eslint --fix" - ], - "**/*.{js,mjs,json,md,css,html}": [ - "prettier --write" - ] + "format:check": "prettier --check ." }, "packageManager": "pnpm@10.33.1", "devDependencies": { @@ -33,8 +22,6 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", - "husky": "^9.1.7", - "lint-staged": "^16.4.0", "prettier": "^3.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.1", diff --git a/packages/db/package.json b/packages/db/package.json index ee17556..914e989 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -6,8 +6,7 @@ "scripts": { "build": "rm -rf dist && tsc", "generate": "drizzle-kit generate", - "migrate": "drizzle-kit migrate", - "typecheck": "tsc --noEmit" + "migrate": "drizzle-kit migrate" }, "dependencies": { "@lila/shared": "workspace:*", diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 567a460..baa05e0 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -6,13 +6,10 @@ import { dirname } from "path"; import * as schema from "./db/schema.js"; 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_LOCAL"] ?? process.env["DATABASE_URL"]!, - { schema }, -); +export const db = drizzle(process.env["DATABASE_URL"]!, { schema }); export * from "./models/termModel.js"; export * from "./models/lobbyModel.js"; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index c8c1b3a..af1fba6 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -10,7 +10,6 @@ "include": [ "src", "vitest.config.ts", - "drizzle.config.ts", "../../data-pipeline/archive/packages-db-src-old-seeding-scripts/data" ] } diff --git a/packages/shared/package.json b/packages/shared/package.json index c46ab0a..bf6aa8a 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -4,8 +4,7 @@ "private": true, "type": "module", "scripts": { - "build": "tsc", - "typecheck": "tsc --noEmit" + "build": "tsc" }, "exports": { ".": "./dist/src/index.js" diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 582394e..7dc79f5 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,3 @@ export * from "./constants.js"; export * from "./schemas/game.js"; export * from "./schemas/lobby.js"; -export * from "./schemas/auth.js"; diff --git a/packages/shared/src/schemas/auth.ts b/packages/shared/src/schemas/auth.ts deleted file mode 100644 index 6aaf35d..0000000 --- a/packages/shared/src/schemas/auth.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as z from "zod"; - -export const ResetPasswordSearchSchema = z.object({ - token: z.string().catch(""), -}); - -export type ResetPasswordSearch = z.infer; - -export const AuthModalSearchSchema = z.object({ - modal: z.enum(["auth"]).optional().catch(undefined), - redirect: z.string().optional().catch(undefined), -}); - -export type AuthModalSearch = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4453586..15acc4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 1.161.6(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': 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)(yaml@2.8.3))) + 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))) concurrently: specifier: ^9.2.1 version: 9.2.1 @@ -32,12 +32,6 @@ importers: eslint-plugin-react-refresh: specifier: ^0.5.2 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: specifier: ^3.8.1 version: 3.8.1 @@ -49,7 +43,7 @@ importers: version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) vitest: 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)(yaml@2.8.3)) + 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)) apps/api: dependencies: @@ -61,7 +55,7 @@ importers: version: link:../../packages/shared better-auth: 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)(yaml@2.8.3))) + 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))) cors: specifier: ^2.8.6 version: 2.8.6 @@ -74,9 +68,6 @@ importers: helmet: specifier: ^8.1.0 version: 8.1.0 - resend: - specifier: ^6.12.2 - version: 6.12.2 ws: specifier: ^8.20.0 version: 8.20.0 @@ -107,7 +98,7 @@ importers: version: link:../../packages/shared '@tailwindcss/vite': 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)(yaml@2.8.3)) + 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)) '@tanstack/react-router': specifier: ^1.168.1 version: 1.168.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -116,23 +107,20 @@ 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) better-auth: 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)(yaml@2.8.3))) + 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))) react: specifier: ^19.2.4 version: 19.2.4 react-dom: specifier: ^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: specifier: ^4.2.2 version: 4.2.2 devDependencies: '@tanstack/router-plugin': 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)(yaml@2.8.3)) + 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)) '@types/node': specifier: ^24.12.0 version: 24.12.0 @@ -144,13 +132,13 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': 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)(yaml@2.8.3)) + 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)) jsdom: specifier: ^29.0.1 version: 29.0.1(@noble/hashes@2.2.0) vite: 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)(yaml@2.8.3) + version: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) data-pipeline: dependencies: @@ -1096,9 +1084,6 @@ packages: '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - '@stablelib/base64@1.0.1': - resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1497,26 +1482,14 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - ansi-escapes@7.3.0: - resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} - engines: {node: '>=18'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -1701,14 +1674,6 @@ packages: chownr@1.1.4: 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: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1728,17 +1693,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} - component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -1951,9 +1909,6 @@ packages: electron-to-chromium@1.5.321: resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1972,10 +1927,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2092,9 +2043,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -2125,9 +2073,6 @@ packages: fast-safe-stringify@2.1.1: 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: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2202,10 +2147,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 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: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2277,11 +2218,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} 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: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2327,10 +2263,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-fullwidth-code-point@5.1.0: - resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} - engines: {node: '>=18'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2490,23 +2422,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} 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: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - log-update@6.1.0: - resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} - engines: {node: '>=18'} - lru-cache@11.2.7: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} @@ -2564,10 +2483,6 @@ packages: engines: {node: '>=4.0.0'} hasBin: true - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -2633,10 +2548,6 @@ packages: once@1.4.0: 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: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2715,9 +2626,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - postal-mime@2.7.4: - resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} - postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -2809,25 +2717,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 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: 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: resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2920,30 +2812,12 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} simple-get@4.0.1: 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: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2970,9 +2844,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - standardwebhooks@1.0.0: - resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2980,22 +2851,10 @@ packages: std-env@4.0.0: 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: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 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: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -3003,10 +2862,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} - strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -3027,9 +2882,6 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - svix@1.90.0: - resolution: {integrity: sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==} - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -3167,11 +3019,6 @@ packages: util-deprecate@1.0.2: 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: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3299,10 +3146,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -3341,11 +3184,6 @@ packages: yallist@3.1.1: 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: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -3978,8 +3816,6 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.7': {} - '@stablelib/base64@1.0.1': {} - '@standard-schema/spec@1.1.0': {} '@tailwindcss/node@4.2.2': @@ -4043,12 +3879,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-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)(yaml@2.8.3))': + '@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))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 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)(yaml@2.8.3) + vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) '@tanstack/eslint-plugin-router@1.161.6(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: @@ -4120,7 +3956,7 @@ snapshots: transitivePeerDependencies: - 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)(yaml@2.8.3))': + '@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))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -4137,7 +3973,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@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) + vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -4356,12 +4192,12 @@ snapshots: '@typescript-eslint/types': 8.57.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)(yaml@2.8.3))': + '@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))': dependencies: '@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)(yaml@2.8.3) + vite: 8.0.1(@types/node@24.12.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)))': + '@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)))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.0 @@ -4373,7 +4209,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.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)(yaml@2.8.3)) + 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/expect@4.1.0': dependencies: @@ -4384,22 +4220,22 @@ snapshots: chai: 6.2.2 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)(yaml@2.8.3))': + '@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))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - 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) + vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) 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)(yaml@2.8.3))': + '@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))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - 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) + vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) '@vitest/pretty-format@4.1.0': dependencies: @@ -4445,20 +4281,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-escapes@7.3.0: - dependencies: - environment: 1.1.0 - ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} - ansis@4.2.0: {} anymatch@3.1.3: @@ -4497,7 +4325,7 @@ snapshots: 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)(yaml@2.8.3))): + 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))): 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/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)) @@ -4523,12 +4351,12 @@ snapshots: pg: 8.20.0 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)(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)) transitivePeerDependencies: - '@cloudflare/workers-types' - '@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)(yaml@2.8.3))): + 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))): 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/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)) @@ -4554,7 +4382,7 @@ snapshots: pg: 8.20.0 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)(yaml@2.8.3)) + 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)) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -4666,15 +4494,6 @@ snapshots: 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: dependencies: string-width: 4.2.3 @@ -4691,14 +4510,10 @@ snapshots: color-name@1.1.4: {} - colorette@2.0.20: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - commander@14.0.3: {} - component-emitter@1.3.1: {} concurrently@9.2.1: @@ -4808,8 +4623,6 @@ snapshots: electron-to-chromium@1.5.321: {} - emoji-regex@10.6.0: {} - emoji-regex@8.0.0: {} encodeurl@2.0.0: {} @@ -4825,8 +4638,6 @@ snapshots: entities@6.0.1: {} - environment@1.1.0: {} - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -5026,8 +4837,6 @@ snapshots: etag@1.8.1: {} - eventemitter3@5.0.4: {} - expand-template@2.0.3: {} expect-type@1.3.0: {} @@ -5078,8 +4887,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-sha256@1.3.0: {} - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -5148,8 +4955,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5226,8 +5031,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - husky@9.1.7: {} - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5256,10 +5059,6 @@ snapshots: 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: dependencies: is-extglob: 2.1.1 @@ -5391,36 +5190,10 @@ snapshots: lightningcss-win32-arm64-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: dependencies: 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@5.1.1: @@ -5465,8 +5238,6 @@ snapshots: mime@2.6.0: {} - mimic-function@5.0.1: {} - mimic-response@3.1.0: {} minimatch@10.2.4: @@ -5511,10 +5282,6 @@ snapshots: dependencies: wrappy: 1.0.2 - onetime@7.0.0: - dependencies: - mimic-function: 5.0.1 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -5587,8 +5354,6 @@ snapshots: picomatch@4.0.3: {} - postal-mime@2.7.4: {} - postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -5685,20 +5450,8 @@ snapshots: 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: {} - 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: dependencies: '@oxc-project/types': 0.120.0 @@ -5823,8 +5576,6 @@ snapshots: siginfo@2.0.0: {} - signal-exit@4.1.0: {} - simple-concat@1.0.1: {} simple-get@4.0.1: @@ -5833,21 +5584,6 @@ snapshots: once: 1.4.0 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-support@0.5.21: @@ -5867,34 +5603,16 @@ snapshots: stackback@0.0.2: {} - standardwebhooks@1.0.0: - dependencies: - '@stablelib/base64': 1.0.1 - fast-sha256: 1.3.0 - statuses@2.0.2: {} std-env@4.0.0: {} - string-argv@0.3.2: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 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: dependencies: safe-buffer: 5.2.1 @@ -5903,10 +5621,6 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.2.0: - dependencies: - ansi-regex: 6.2.2 - strip-json-comments@2.0.1: {} superagent@10.3.0: @@ -5939,11 +5653,6 @@ snapshots: dependencies: has-flag: 4.0.0 - svix@1.90.0: - dependencies: - standardwebhooks: 1.0.0 - uuid: 10.0.0 - symbol-tree@3.2.4: {} tailwindcss@4.2.2: {} @@ -6074,11 +5783,9 @@ snapshots: util-deprecate@1.0.2: {} - uuid@10.0.0: {} - 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)(yaml@2.8.3): + vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.3 @@ -6091,9 +5798,8 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 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)(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): dependencies: lightningcss: 1.32.0 picomatch: 4.0.3 @@ -6106,12 +5812,11 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 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)(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)): dependencies: '@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)(yaml@2.8.3)) + '@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/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -6128,7 +5833,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 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)(yaml@2.8.3) + vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -6138,10 +5843,10 @@ snapshots: - msw 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)(yaml@2.8.3)): + 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)): dependencies: '@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)(yaml@2.8.3)) + '@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/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -6158,7 +5863,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 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)(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) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -6206,12 +5911,6 @@ snapshots: string-width: 4.2.3 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: {} ws@8.20.0: {} @@ -6236,8 +5935,6 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.3: {} - yargs-parser@21.1.1: {} yargs@17.7.2: