From ad1f32eb8f5123f9febf30dd70c461966fcf51fd Mon Sep 17 00:00:00 2001 From: "groombook-engineer[bot]" <269742240+groombook-engineer[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 03:50:45 +0000 Subject: [PATCH] feat(auth): replace OIDC/jose with Better-Auth (#136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(db): add Better-Auth schema tables (GRO-118) Add user, session, account, and verification tables required by Better-Auth's Drizzle adapter. Add nullable userId FK on staff to link business identity to auth identity. Fix test fixtures and factory to include the new column. Co-Authored-By: Paperclip * feat(api): mount Better-Auth handler at /api/auth/** (GRO-118) - Import toNodeHandler from better-auth/node and auth from ./lib/auth.js - Mount Better-Auth HTTP handler before auth middleware block - Handles OAuth callbacks, sign-in/sign-out, session management - Supports GET/POST/PUT/PATCH/DELETE/OPTIONS methods Co-Authored-By: Paperclip * feat(api): replace JWT auth with Better-Auth session validation (GRO-118) - Replace jose/jwtVerify with auth.api.getSession() - Session token validated via cookie/header, DB-backed - jwtPayload.sub now = Better-Auth user ID (not OIDC sub) - Dev mode bypass preserved; production guard against AUTH_DISABLED preserved - rbac.ts and tests updated in subsequent tasks Co-Authored-By: Paperclip * feat(api): update resolveStaffMiddleware for Better-Auth userId (GRO-118) - Remove JwtPayload import; use inline type in AppEnv - Production and dev mode lookups now use staff.userId (not oidcSub) - Backward compat: jwtPayload.sub now = Better-Auth user ID Co-Authored-By: Paperclip * chore(api): remove jose and openid-client deps (GRO-118) - Remove unused jose and openid-client packages - Regenerate pnpm lockfile - Pre-existing Zod type errors resolved (1 remaining: JwtPayload in test) Co-Authored-By: Paperclip * fix(api): remove stale JwtPayload import from impersonation test (GRO-118) auth.ts no longer exports JwtPayload — replace with inline type. Co-Authored-By: Paperclip * test(api): update RBAC tests for Better-Auth userId (GRO-128) - Add userId field to mock staff records (MANAGER, RECEPTIONIST, GROOMER) - Update jwtPayload.sub to use userId instead of oidcSub in test helpers - Update dev mode X-Dev-User-Id header to use userId Co-Authored-By: Paperclip * chore(api): upgrade zod to v4 with v3 compat layer (GRO-131) - Bump zod from ^3.24.1 to ^4.3.6 - Bump @hono/zod-validator from ^0.4.3 to ^0.7.6 - Update all 12 route files to import from "zod/v3" compat layer Co-Authored-By: Paperclip * feat(api): add Better-Auth configuration (GRO-118) Exports the better-auth() instance configured with: - Drizzle PG adapter - genericOAuth plugin for Authentik OIDC - 7-day session with 5-min cookie cache Co-Authored-By: Paperclip * feat(web): install Better-Auth client and create config (GRO-118) - Add better-auth to apps/web/package.json dependencies - Create apps/web/src/lib/auth-client.ts with createAuthClient config - Export signIn, signOut, useSession from the client - Add vite-env.d.ts for Vite client types Co-Authored-By: Paperclip * feat(web): use Better-Auth session state in App.tsx (GRO-126) Add useSession hook to check Better-Auth session for production auth. Redirect to Authentik sign-in when no session in production mode. Dev mode flow (DevLoginSelector) preserved. Co-Authored-By: Paperclip * fix(web): scope devFetch interceptor to dev mode only (GRO-127) * fix(api): validate BETTER_AUTH_SECRET and fix lockfile specifier (GRO-118) - Add startup validation for BETTER_AUTH_SECRET when auth is enabled - Fix pnpm-lock.yaml typescript specifier mismatch (^5.9.3 → ^5.7.3) Co-Authored-By: Paperclip * fix(web): mock authDisabled=true in App.test.tsx to fix CI failures App.test.tsx "App navigation" tests were failing because the beforeEach set authDisabled=false (production mode), which triggers the Better Auth useSession() path. Since useSession() was not mocked in tests, the component rendered null instead of the admin nav. Now uses authDisabled=true + dev user in localStorage for those tests, bypassing the Better Auth dependency while still testing the nav render. Also removes duplicate App.test.js (compiled artifact). Co-Authored-By: Paperclip * fix(e2e): set authDisabled=true in fixtures to bypass Better Auth The App.tsx production auth path calls signIn.social() when authDisabled=false, causing E2E tests to render blank. The fixtures must mock authDisabled=true so the dev login selector is used instead. Co-Authored-By: Paperclip * fix(e2e): add dev/config, dev/users, and branding mocks to navigation.spec.ts Playwright matches routes in last-registered-first-served order, so the catch-all /api/** handler was overwriting the authDisabled: true fixture. Added specific handlers before the catch-all to ensure auth config, user list, and branding responses are properly shaped. Co-Authored-By: Paperclip * fix(web): gate DevLoginSelector on API authDisabled, not import.meta.env.DEV Move the DevLoginSelector rendering check from import.meta.env.DEV to the API-driven authDisabled state, after the loading guard. Simplify the redirect condition to remove the now-redundant pathname exception. Fixes E2E login tests that were failing because DevLoginSelector was never rendered in Docker production builds where import.meta.env.DEV is false. Co-Authored-By: Paperclip * fix(db): add missing migration journal entries 0012-0017 Co-Authored-By: Paperclip * fix(web): import App.tsx (not App.js) in App.test.tsx (#137) * fix(web): mock /api/auth/get-session in Dev login selector test The "redirects to /login when auth is disabled and no user selected" test fails because useSession() from better-auth/react calls /api/auth/get-session which wasn't mocked, causing sessionLoading to stay true indefinitely. Co-Authored-By: Paperclip * fix(web): import App.tsx (not App.js) in test to get authDisabled bypass The Dev login selector test was importing the compiled App.js instead of the source App.tsx. App.js has different logic (uses import.meta.env.DEV instead of API-based authDisabled) and doesn't implement the sessionLoading bypass needed for tests to pass. Also applied the rawSession/rawSessionLoading refactor in App.tsx that bypasses useSession result when authDisabled=true. Co-Authored-By: Paperclip * fix(web): use extensionless import for App in test The `.tsx` extension in the import path is not allowed without `allowImportingTsExtensions` (TS5097). Use extensionless `../App` which resolves correctly via moduleResolution: "bundler". Co-Authored-By: Paperclip --------- Co-authored-by: Paperclip * fix(auth): dev login resolve staff by id, not userId Co-Authored-By: Paperclip * fix(rbac): fallback lookup for staff records predating Better-Auth userId (#140) GRO-153: /api/staff returned 403 for all staff because resolveStaffMiddleware looked up by staff.userId (Better-Auth ID) but dev login sent staff.id (PK), and existing staff records had userId=NULL. Changes: - resolveStaffMiddleware: try userId first, fall back to staff.id (dev mode) - resolveStaffMiddleware: try userId first, fall back to oidcSub (production) - GET /api/dev/users: include userId field for DevLoginSelector - DevLoginSelector: send userId (not staff.id) as X-Dev-User-Id - Migration 0018: backfill userId for known demo staff Co-authored-by: groombook-engineer[bot] Co-authored-by: Paperclip Co-authored-by: Barkley Trimsworth * fix(rbac): allow all staff roles to READ /api/staff GRO-156 follow-up: RBAC middleware was blocking groomer/receptionist from GET /api/staff. The QA review found 403 with "role groomer is not permitted" after PR #140 deployment. Fix: split the /staff/* guard — GET requests allow all roles (groomer, receptionist, manager); write operations remain manager-only. Co-Authored-By: Paperclip --------- Co-authored-by: Paperclip Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com> Co-authored-by: Flea Flicker Co-authored-by: groombook-engineer[bot] Co-authored-by: Barkley Trimsworth --- apps/api/package.json | 11 +- .../src/__tests__/groomerIsolation.test.ts | 1 + apps/api/src/__tests__/impersonation.test.ts | 3 +- apps/api/src/__tests__/petPhotos.test.ts | 1 + apps/api/src/__tests__/rbac.test.ts | 7 +- apps/api/src/index.ts | 15 +- apps/api/src/lib/auth.ts | 48 +++ apps/api/src/middleware/auth.ts | 59 +-- apps/api/src/middleware/rbac.ts | 38 +- apps/api/src/routes/appointmentGroups.ts | 2 +- apps/api/src/routes/appointments.ts | 2 +- apps/api/src/routes/book.ts | 2 +- apps/api/src/routes/clients.ts | 2 +- apps/api/src/routes/dev.ts | 1 + apps/api/src/routes/groomingLogs.ts | 2 +- apps/api/src/routes/impersonation.ts | 2 +- apps/api/src/routes/invoices.ts | 2 +- apps/api/src/routes/pets.ts | 2 +- apps/api/src/routes/portal.ts | 2 +- apps/api/src/routes/services.ts | 2 +- apps/api/src/routes/settings.ts | 2 +- apps/api/src/routes/staff.ts | 2 +- apps/e2e/tests/fixtures.ts | 16 +- apps/e2e/tests/navigation.spec.ts | 9 + apps/web/package.json | 1 + apps/web/src/App.tsx | 37 +- apps/web/src/__tests__/App.test.tsx | 35 +- apps/web/src/lib/auth-client.ts | 7 + apps/web/src/lib/devFetch.ts | 3 + apps/web/src/vite-env.d.ts | 1 + .../db/migrations/0017_better_auth_tables.sql | 49 +++ .../0018_backfill_staff_user_id.sql | 14 + packages/db/migrations/meta/_journal.json | 49 +++ packages/db/src/factories.ts | 1 + packages/db/src/schema.ts | 54 +++ pnpm-lock.yaml | 342 ++++++++++++++++-- 36 files changed, 695 insertions(+), 131 deletions(-) create mode 100644 apps/api/src/lib/auth.ts create mode 100644 apps/web/src/lib/auth-client.ts create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 packages/db/migrations/0017_better_auth_tables.sql create mode 100644 packages/db/migrations/0018_backfill_staff_user_id.sql diff --git a/apps/api/package.json b/apps/api/package.json index ec2a70a..55c1c9d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -12,18 +12,17 @@ "test": "vitest run" }, "dependencies": { + "@aws-sdk/client-s3": "^3.800.0", + "@aws-sdk/s3-request-presigner": "^3.800.0", "@groombook/db": "workspace:*", "@groombook/types": "workspace:*", "@hono/node-server": "^1.13.7", - "@hono/zod-validator": "^0.4.3", + "@hono/zod-validator": "^0.7.6", + "better-auth": "^1.5.6", "hono": "^4.6.17", - "jose": "^5.9.6", "node-cron": "^3.0.3", "nodemailer": "^6.9.16", - "openid-client": "^6.1.7", - "zod": "^3.24.1", - "@aws-sdk/client-s3": "^3.800.0", - "@aws-sdk/s3-request-presigner": "^3.800.0" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^22.10.7", diff --git a/apps/api/src/__tests__/groomerIsolation.test.ts b/apps/api/src/__tests__/groomerIsolation.test.ts index f1bd97d..04f087f 100644 --- a/apps/api/src/__tests__/groomerIsolation.test.ts +++ b/apps/api/src/__tests__/groomerIsolation.test.ts @@ -15,6 +15,7 @@ import type { StaffRow } from "../middleware/rbac.js"; const MANAGER: StaffRow = { id: "staff-manager-id", oidcSub: "oidc-manager-sub", + userId: null, role: "manager", name: "Manager McManager", email: "manager@example.com", diff --git a/apps/api/src/__tests__/impersonation.test.ts b/apps/api/src/__tests__/impersonation.test.ts index 2ba232f..de7688d 100644 --- a/apps/api/src/__tests__/impersonation.test.ts +++ b/apps/api/src/__tests__/impersonation.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { Hono } from "hono"; -import type { JwtPayload } from "../middleware/auth.js"; import type { AppEnv, StaffRow } from "../middleware/rbac.js"; import { buildStaff } from "@groombook/db/factories"; @@ -167,7 +166,7 @@ function createApp( if (!staffRow) { return c.json({ error: "Forbidden: no staff record found for authenticated user" }, 403); } - c.set("jwtPayload", { sub: staffRow.oidcSub } as JwtPayload); + c.set("jwtPayload", { sub: staffRow.oidcSub } as { sub: string; email?: string; name?: string }); c.set("staff", staffRow as unknown as StaffRow); await next(); }); diff --git a/apps/api/src/__tests__/petPhotos.test.ts b/apps/api/src/__tests__/petPhotos.test.ts index b4d2d6b..19b8564 100644 --- a/apps/api/src/__tests__/petPhotos.test.ts +++ b/apps/api/src/__tests__/petPhotos.test.ts @@ -7,6 +7,7 @@ import type { AppEnv, StaffRow } from "../middleware/rbac.js"; const MANAGER: StaffRow = { id: "staff-manager-id", oidcSub: "oidc-manager-sub", + userId: null, role: "manager", name: "Manager McManager", email: "manager@example.com", diff --git a/apps/api/src/__tests__/rbac.test.ts b/apps/api/src/__tests__/rbac.test.ts index d8c26bf..e213ed7 100644 --- a/apps/api/src/__tests__/rbac.test.ts +++ b/apps/api/src/__tests__/rbac.test.ts @@ -8,6 +8,7 @@ import type { AppEnv, StaffRow } from "../middleware/rbac.js"; const MANAGER: StaffRow = { id: "staff-manager-id", oidcSub: "oidc-manager-sub", + userId: "ba-user-manager", role: "manager", name: "Manager McManager", email: "manager@example.com", @@ -21,6 +22,7 @@ const RECEPTIONIST: StaffRow = { ...MANAGER, id: "staff-receptionist-id", oidcSub: "oidc-receptionist-sub", + userId: "ba-user-receptionist", role: "receptionist", name: "Receptionist Rita", email: "receptionist@example.com", @@ -30,6 +32,7 @@ const GROOMER: StaffRow = { ...MANAGER, id: "staff-groomer-id", oidcSub: "oidc-groomer-sub", + userId: "ba-user-groomer", role: "groomer", name: "Groomer Gary", email: "groomer@example.com", @@ -89,7 +92,7 @@ function buildApp( ) { const app = new Hono(); app.use("*", async (c, next) => { - c.set("jwtPayload", { sub: staffLookupResult?.oidcSub ?? "unknown-sub" }); + c.set("jwtPayload", { sub: staffLookupResult?.userId ?? "unknown-sub" }); await next(); }); app.use("*", middleware); @@ -106,7 +109,7 @@ function buildWithStaff( ) { const app = new Hono(); app.use("*", async (c, next) => { - c.set("jwtPayload", { sub: staffRow.oidcSub ?? "" }); + c.set("jwtPayload", { sub: staffRow.userId ?? "" }); c.set("staff", staffRow); await next(); }); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index f20f277..d1820f5 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -2,6 +2,8 @@ import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { logger } from "hono/logger"; import { cors } from "hono/cors"; +import { toNodeHandler } from "better-auth/node"; +import { auth } from "./lib/auth.js"; import { clientsRouter } from "./routes/clients.js"; import { petsRouter } from "./routes/pets.js"; import { servicesRouter } from "./routes/services.js"; @@ -65,13 +67,24 @@ app.get("/api/branding", async (c) => { // Public iCal calendar feed — token auth in URL, no auth middleware required app.route("/api/calendar", calendarRouter); + +// Better-Auth handler — public, handles OAuth callbacks, session management +// Mounted BEFORE auth middleware so it's accessible without authentication +app.on(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], "/api/auth/**", (c) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { incoming, outgoing } = c.env as any; + return toNodeHandler(auth)(incoming, outgoing); +}); + // Protected API routes const api = app.basePath("/api"); api.use("*", authMiddleware); api.use("*", resolveStaffMiddleware); // ── Role guards ──────────────────────────────────────────────────────────────── -// Manager-only: staff, admin settings, reports, invoices, impersonation +// Manager-only: admin settings, reports, invoices, impersonation +// Staff CRUD: all roles may READ; manager-only for CREATE/UPDATE/DELETE +api.on(["GET"], "/staff/*", requireRole("manager", "receptionist", "groomer")); api.use("/staff/*", requireRole("manager")); api.use("/admin/*", requireRole("manager")); api.use("/reports/*", requireRole("manager")); diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts new file mode 100644 index 0000000..3dda63b --- /dev/null +++ b/apps/api/src/lib/auth.ts @@ -0,0 +1,48 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { genericOAuth } from "better-auth/plugins"; +import { getDb } from "@groombook/db"; + +const OIDC_ISSUER = process.env.OIDC_ISSUER; +const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID; +const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET; +const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET; +const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? "http://localhost:3000"; + +if (!BETTER_AUTH_SECRET && process.env.AUTH_DISABLED !== "true") { + throw new Error( + "[FATAL] BETTER_AUTH_SECRET environment variable is required when auth is enabled" + ); +} + +export const auth = betterAuth({ + database: drizzleAdapter(getDb(), { + provider: "pg", + }), + secret: BETTER_AUTH_SECRET, + baseURL: BETTER_AUTH_URL, + plugins: [ + genericOAuth({ + config: [ + { + providerId: "authentik", + clientId: OIDC_CLIENT_ID ?? "", + clientSecret: OIDC_CLIENT_SECRET ?? "", + discoveryUrl: OIDC_ISSUER + ? `${OIDC_ISSUER}/.well-known/openid-configuration` + : undefined, + scopes: ["openid", "profile", "email"], + }, + ], + }), + ], + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // 1 day + cookieCache: { + enabled: true, + maxAge: 5 * 60, // 5 minutes + }, + }, + trustedOrigins: [process.env.CORS_ORIGIN ?? "http://localhost:5173"], +}); diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts index 44f4100..66ec3d4 100644 --- a/apps/api/src/middleware/auth.ts +++ b/apps/api/src/middleware/auth.ts @@ -1,34 +1,18 @@ import type { MiddlewareHandler } from "hono"; -import { createRemoteJWKSet, jwtVerify } from "jose"; +import { auth } from "../lib/auth.js"; -// Authentik OIDC configuration — loaded from env at startup -const OIDC_ISSUER = process.env.OIDC_ISSUER; -const OIDC_AUDIENCE = process.env.OIDC_AUDIENCE; - -let jwks: ReturnType | null = null; - -function getJwks() { - if (!OIDC_ISSUER) throw new Error("OIDC_ISSUER is not set"); - if (!jwks) { - jwks = createRemoteJWKSet( - new URL(`${OIDC_ISSUER}/application/o/groombook/jwks/`) - ); - } - return jwks; +export interface AuthUser { + id: string; + email: string; + name: string; } -export interface JwtPayload { - sub: string; - email?: string; - name?: string; -} - -// Guard: refuse to start with AUTH_DISABLED in production (fixes #22). +// Guard: refuse to start with AUTH_DISABLED in production. if (process.env.AUTH_DISABLED === "true") { if (process.env.NODE_ENV === "production") { console.error( "[FATAL] AUTH_DISABLED=true is not allowed in production. " + - "Remove AUTH_DISABLED from your environment and configure OIDC_ISSUER." + "Remove AUTH_DISABLED from your environment and configure Better-Auth." ); process.exit(1); } @@ -42,27 +26,24 @@ export const authMiddleware: MiddlewareHandler = async (c, next) => { if (process.env.AUTH_DISABLED === "true") { const devUserId = c.req.header("X-Dev-User-Id"); const sub = devUserId ?? "dev-user"; - c.set("jwtPayload", { sub } as JwtPayload); + c.set("jwtPayload", { sub } as { sub: string }); await next(); return; } - const authorization = c.req.header("Authorization"); - if (!authorization?.startsWith("Bearer ")) { + const session = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + + if (!session) { return c.json({ error: "Unauthorized" }, 401); } - const token = authorization.slice(7); - - try { - const { payload } = await jwtVerify(token, getJwks(), { - issuer: OIDC_ISSUER, - audience: OIDC_AUDIENCE, - }); - - c.set("jwtPayload", payload as JwtPayload); - await next(); - } catch { - return c.json({ error: "Invalid or expired token" }, 401); - } + // Set jwtPayload with sub = Better-Auth user ID for backward compat with resolveStaffMiddleware + c.set("jwtPayload", { + sub: session.user.id, + email: session.user.email, + name: session.user.name, + }); + await next(); }; diff --git a/apps/api/src/middleware/rbac.ts b/apps/api/src/middleware/rbac.ts index 98d9405..1bc2228 100644 --- a/apps/api/src/middleware/rbac.ts +++ b/apps/api/src/middleware/rbac.ts @@ -1,13 +1,12 @@ import type { MiddlewareHandler } from "hono"; import { eq, getDb, staff } from "@groombook/db"; -import type { JwtPayload } from "./auth.js"; export type StaffRole = "groomer" | "receptionist" | "manager"; export type StaffRow = typeof staff.$inferSelect; export interface AppEnv { Variables: { - jwtPayload: JwtPayload; + jwtPayload: { sub: string; email?: string; name?: string }; staff: StaffRow; }; } @@ -16,8 +15,8 @@ export interface AppEnv { * Resolves the authenticated staff record from the DB and stores it in context. * Must be applied after authMiddleware on all protected routes. * - * Dev mode (AUTH_DISABLED=true): resolves staff by X-Dev-User-Id header (treated - * as oidcSub), or falls back to the first manager in the DB. + * Dev mode (AUTH_DISABLED=true): resolves staff by X-Dev-User-Id header (Better-Auth + * user ID), or falls back to the first manager in the DB. */ export const resolveStaffMiddleware: MiddlewareHandler = async ( c, @@ -41,34 +40,55 @@ export const resolveStaffMiddleware: MiddlewareHandler = async ( await next(); return; } - // Treat X-Dev-User-Id as the staff database id (the frontend stores staff.id) + // Treat X-Dev-User-Id as the Better-Auth user ID first const [row] = await db + .select() + .from(staff) + .where(eq(staff.userId, devUserId)); + if (row) { + c.set("staff", row); + await next(); + return; + } + // Fallback: if userId is null, treat X-Dev-User-Id as staff.id (dev login + // may send the primary key for staff records that predate the userId field) + const [fallbackRow] = await db .select() .from(staff) .where(eq(staff.id, devUserId)); - if (!row) { + if (!fallbackRow) { return c.json( { error: "Forbidden: no staff record found for X-Dev-User-Id" }, 403 ); } - c.set("staff", row); + c.set("staff", fallbackRow); await next(); return; } const jwt = c.get("jwtPayload"); const [row] = await db + .select() + .from(staff) + .where(eq(staff.userId, jwt.sub)); + if (row) { + c.set("staff", row); + await next(); + return; + } + // Fallback: staff records that predate the userId field may still have oidcSub + const [fallbackRow] = await db .select() .from(staff) .where(eq(staff.oidcSub, jwt.sub)); - if (!row) { + if (!fallbackRow) { return c.json( { error: "Forbidden: no staff record found for authenticated user" }, 403 ); } - c.set("staff", row); + c.set("staff", fallbackRow); await next(); }; diff --git a/apps/api/src/routes/appointmentGroups.ts b/apps/api/src/routes/appointmentGroups.ts index e2790a4..8ecbb45 100644 --- a/apps/api/src/routes/appointmentGroups.ts +++ b/apps/api/src/routes/appointmentGroups.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { and, eq, diff --git a/apps/api/src/routes/appointments.ts b/apps/api/src/routes/appointments.ts index c693325..6ed72e2 100644 --- a/apps/api/src/routes/appointments.ts +++ b/apps/api/src/routes/appointments.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { randomBytes } from "node:crypto"; import { and, diff --git a/apps/api/src/routes/book.ts b/apps/api/src/routes/book.ts index 3b12089..d82823f 100644 --- a/apps/api/src/routes/book.ts +++ b/apps/api/src/routes/book.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { and, eq, diff --git a/apps/api/src/routes/clients.ts b/apps/api/src/routes/clients.ts index d569247..fe639c5 100644 --- a/apps/api/src/routes/clients.ts +++ b/apps/api/src/routes/clients.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { and, eq, exists, getDb, or, clients, appointments } from "@groombook/db"; import type { AppEnv } from "../middleware/rbac.js"; diff --git a/apps/api/src/routes/dev.ts b/apps/api/src/routes/dev.ts index dfc5708..363da85 100644 --- a/apps/api/src/routes/dev.ts +++ b/apps/api/src/routes/dev.ts @@ -20,6 +20,7 @@ devRouter.get("/users", async (c) => { const staffList = await db .select({ id: staff.id, + userId: staff.userId, name: staff.name, email: staff.email, role: staff.role, diff --git a/apps/api/src/routes/groomingLogs.ts b/apps/api/src/routes/groomingLogs.ts index a89c2ed..81eeaf4 100644 --- a/apps/api/src/routes/groomingLogs.ts +++ b/apps/api/src/routes/groomingLogs.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { desc, eq, getDb, groomingVisitLogs } from "@groombook/db"; export const groomingLogsRouter = new Hono(); diff --git a/apps/api/src/routes/impersonation.ts b/apps/api/src/routes/impersonation.ts index 00feb9d..350f086 100644 --- a/apps/api/src/routes/impersonation.ts +++ b/apps/api/src/routes/impersonation.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { and, eq, diff --git a/apps/api/src/routes/invoices.ts b/apps/api/src/routes/invoices.ts index 9994d7f..ee2f473 100644 --- a/apps/api/src/routes/invoices.ts +++ b/apps/api/src/routes/invoices.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { and, eq, diff --git a/apps/api/src/routes/pets.ts b/apps/api/src/routes/pets.ts index 5bcb20e..a6b9982 100644 --- a/apps/api/src/routes/pets.ts +++ b/apps/api/src/routes/pets.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { and, eq, exists, getDb, or, pets, appointments } from "@groombook/db"; import type { AppEnv } from "../middleware/rbac.js"; import { diff --git a/apps/api/src/routes/portal.ts b/apps/api/src/routes/portal.ts index a40fd42..7003a43 100644 --- a/apps/api/src/routes/portal.ts +++ b/apps/api/src/routes/portal.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { and, eq, getDb, appointments, impersonationSessions, waitlistEntries } from "@groombook/db"; import type { AppEnv } from "../middleware/rbac.js"; diff --git a/apps/api/src/routes/services.ts b/apps/api/src/routes/services.ts index 621a797..e9ccc44 100644 --- a/apps/api/src/routes/services.ts +++ b/apps/api/src/routes/services.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { eq, getDb, services } from "@groombook/db"; export const servicesRouter = new Hono(); diff --git a/apps/api/src/routes/settings.ts b/apps/api/src/routes/settings.ts index 2641c8c..55332e4 100644 --- a/apps/api/src/routes/settings.ts +++ b/apps/api/src/routes/settings.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { eq, getDb, businessSettings } from "@groombook/db"; export const settingsRouter = new Hono(); diff --git a/apps/api/src/routes/staff.ts b/apps/api/src/routes/staff.ts index 0aa6b70..3316c45 100644 --- a/apps/api/src/routes/staff.ts +++ b/apps/api/src/routes/staff.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; +import { z } from "zod/v3"; import { randomBytes } from "node:crypto"; import { and, eq, getDb, ne, staff, appointments } from "@groombook/db"; import type { AppEnv } from "../middleware/rbac.js"; diff --git a/apps/e2e/tests/fixtures.ts b/apps/e2e/tests/fixtures.ts index d043cc1..8e02aa4 100644 --- a/apps/e2e/tests/fixtures.ts +++ b/apps/e2e/tests/fixtures.ts @@ -1,14 +1,14 @@ import { test as base } from "@playwright/test"; /** - * Custom test fixture that bypasses the dev login redirect for E2E tests. + * Custom test fixture that bypasses auth for E2E tests. * - * When AUTH_DISABLED=true, the app fetches /api/dev/config and redirects to - * /login if no dev-user is in localStorage. This fixture: - * 1. Mocks /api/dev/config to return authDisabled: false - * 2. Seeds localStorage with a dev user as a fallback + * When authDisabled=true, the app uses the dev login selector instead of + * Better Auth signIn.social(). This fixture: + * 1. Mocks /api/dev/config to return authDisabled: true + * 2. Seeds localStorage with a dev user so the selector auto-selects a session * - * This ensures E2E tests render pages directly without the login redirect. + * This ensures E2E tests render pages directly without the auth redirect. */ const MOCK_DEV_USERS = { staff: [ @@ -23,9 +23,9 @@ const MOCK_DEV_USERS = { export const test = base.extend({ page: async ({ page }, use) => { - // Mock the dev config endpoint so the app skips the auth-disabled redirect + // Mock the dev config endpoint so the app uses dev login selector (bypasses Better Auth) await page.route("**/api/dev/config", (route) => - route.fulfill({ json: { authDisabled: false } }) + route.fulfill({ json: { authDisabled: true } }) ); // Mock the dev users endpoint for login selector tests await page.route("**/api/dev/users", (route) => diff --git a/apps/e2e/tests/navigation.spec.ts b/apps/e2e/tests/navigation.spec.ts index 544518a..8221ede 100644 --- a/apps/e2e/tests/navigation.spec.ts +++ b/apps/e2e/tests/navigation.spec.ts @@ -10,6 +10,15 @@ test.beforeEach(async ({ page }) => { // Reports endpoints need shaped responses (not bare []) to avoid render crashes. await page.route("/api/**", (route) => { const url = route.request().url(); + if (url.includes("/api/dev/config")) { + return route.fulfill({ json: { authDisabled: true } }); + } + if (url.includes("/api/dev/users")) { + return route.fulfill({ json: { staff: [], clients: [] } }); + } + if (url.includes("/api/branding")) { + return route.fulfill({ json: { businessName: "GroomBook", logoUrl: null, theme: "default" } }); + } if (url.includes("/api/reports/summary")) { return route.fulfill({ json: { diff --git a/apps/web/package.json b/apps/web/package.json index 82c6707..bab5329 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "dependencies": { "@groombook/types": "workspace:*", "@tailwindcss/vite": "^4.2.2", + "better-auth": "^1.0.0", "lucide-react": "^0.577.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index cdf9d1f..8840370 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -17,6 +17,7 @@ import { DevLoginSelector, getDevUser } from "./pages/DevLoginSelector.js"; import { DevSessionIndicator } from "./components/DevSessionIndicator.js"; import { BrandingProvider, useBranding } from "./BrandingContext.js"; import { GlobalSearch } from "./components/GlobalSearch.js"; +import { useSession, signIn } from "./lib/auth-client.js"; const NAV_LINKS = [ { to: "/admin", label: "Appointments" }, @@ -133,6 +134,10 @@ function AdminLayout() { export function App() { const location = useLocation(); const [authDisabled, setAuthDisabled] = useState(null); + const { data: rawSession, isPending: rawSessionLoading } = useSession(); + // In dev mode (authDisabled=true), session state is irrelevant - skip useSession result + const session = authDisabled ? null : rawSession; + const sessionLoading = authDisabled ? false : rawSessionLoading; useEffect(() => { fetch("/api/dev/config") @@ -141,19 +146,6 @@ export function App() { .catch(() => setAuthDisabled(false)); }, []); - // Show login selector page - if (location.pathname === "/login") { - return ; - } - - // While checking auth config, render nothing briefly - if (authDisabled === null) return null; - - // If auth is disabled and no dev user is selected, redirect to login selector - if (authDisabled && !getDevUser() && location.pathname !== "/login") { - return ; - } - // Public booking redirect pages — no auth or portal chrome needed if (location.pathname === "/booking/confirmed") { return ; @@ -165,6 +157,25 @@ export function App() { return ; } + // Still loading auth state + if (authDisabled === null || sessionLoading) return null; + + // Dev mode: show login selector + if (authDisabled && location.pathname === "/login") { + return ; + } + + // Dev mode: use dev login selector + if (authDisabled && !getDevUser()) { + return ; + } + + // Production mode: if no session, redirect to Authentik sign-in + if (!authDisabled && !session) { + signIn.social({ provider: "authentik" }); + return null; + } + return ( {location.pathname.startsWith("/admin") ? ( diff --git a/apps/web/src/__tests__/App.test.tsx b/apps/web/src/__tests__/App.test.tsx index 97434eb..ea5aea8 100644 --- a/apps/web/src/__tests__/App.test.tsx +++ b/apps/web/src/__tests__/App.test.tsx @@ -1,7 +1,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, within, waitFor } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; -import { App } from "../App.js"; +import { App } from "../App"; + // Mock fetch to return appropriate responses based on URL beforeEach(() => { @@ -44,6 +45,32 @@ async function renderApp(route = "/admin") { } describe("App navigation", () => { + // Use authDisabled=true (dev mode) so nav renders without needing Better Auth useSession() mock + beforeEach(() => { + localStorage.setItem("dev-user", JSON.stringify({ type: "staff", id: "s1", name: "Sarah" })); + global.fetch = vi.fn((url: string) => { + if (url === "/api/dev/config") { + return Promise.resolve({ + ok: true, + json: async () => ({ authDisabled: true }), + } as Response); + } + if (url === "/api/branding") { + return Promise.resolve({ + ok: true, + json: async () => ({ + businessName: "GroomBook", + primaryColor: "#4f8a6f", + accentColor: "#8b7355", + logoBase64: null, + logoMimeType: null, + }), + } as Response); + } + return Promise.resolve({ ok: true, json: async () => [] } as Response); + }) as unknown as typeof fetch; + }); + it("renders the Groom Book brand", async () => { const nav = await renderApp(); expect( @@ -124,6 +151,12 @@ describe("Dev login selector", () => { }), } as Response); } + if (url === "/api/auth/get-session") { + return Promise.resolve({ + ok: true, + json: async () => ({ user: null }), + } as Response); + } return Promise.resolve({ ok: true, json: async () => [] } as Response); }) as unknown as typeof fetch; diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts new file mode 100644 index 0000000..1a4587b --- /dev/null +++ b/apps/web/src/lib/auth-client.ts @@ -0,0 +1,7 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: import.meta.env.VITE_API_URL ?? "http://localhost:3000", +}); + +export const { signIn, signOut, useSession } = authClient; \ No newline at end of file diff --git a/apps/web/src/lib/devFetch.ts b/apps/web/src/lib/devFetch.ts index 42078ce..02b974b 100644 --- a/apps/web/src/lib/devFetch.ts +++ b/apps/web/src/lib/devFetch.ts @@ -9,6 +9,9 @@ const originalFetch = window.fetch; * Intentionally mutates window.fetch — this is dev-only (AUTH_DISABLED=true). */ export function installDevFetchInterceptor() { + // In production, Better-Auth handles auth via cookies — no interception needed + if (!import.meta.env.DEV) return; + window.fetch = function (input: RequestInfo | URL, init?: RequestInit) { const user = getDevUser(); if (!user) return originalFetch(input, init); diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/db/migrations/0017_better_auth_tables.sql b/packages/db/migrations/0017_better_auth_tables.sql new file mode 100644 index 0000000..b5e1f74 --- /dev/null +++ b/packages/db/migrations/0017_better_auth_tables.sql @@ -0,0 +1,49 @@ +-- Better-Auth required tables for session-based authentication +CREATE TABLE "user" ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + email_verified BOOLEAN NOT NULL DEFAULT false, + image TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE "session" ( + id TEXT PRIMARY KEY, + expires_at TIMESTAMPTZ NOT NULL, + token TEXT NOT NULL UNIQUE, + ip_address TEXT, + user_agent TEXT, + user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE "account" ( + id TEXT PRIMARY KEY, + account_id TEXT NOT NULL, + provider_id TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, + access_token TEXT, + refresh_token TEXT, + id_token TEXT, + access_token_expires_at TIMESTAMPTZ, + refresh_token_expires_at TIMESTAMPTZ, + scope TEXT, + password TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE "verification" ( + id TEXT PRIMARY KEY, + identifier TEXT NOT NULL, + value TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Link staff records to auth identity +ALTER TABLE staff ADD COLUMN user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL; diff --git a/packages/db/migrations/0018_backfill_staff_user_id.sql b/packages/db/migrations/0018_backfill_staff_user_id.sql new file mode 100644 index 0000000..9da9f54 --- /dev/null +++ b/packages/db/migrations/0018_backfill_staff_user_id.sql @@ -0,0 +1,14 @@ +-- Backfill staff.user_id for staff records created before Better-Auth integration. +-- Staff records that predate this migration have user_id = NULL; the resolveStaffMiddleware +-- now falls back to staff.id (dev mode) and oidcSub (production) so these records still work. +-- This migration populates user_id for the known demo/dev staff seeded by seed.ts. + +-- Create demo Better-Auth users for seeded staff (these match the ba-user-* IDs used in tests) +INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) +VALUES ('ba-user-manager', 'Demo Manager', 'demo-manager@groombook.dev', true, NOW(), NOW()) +ON CONFLICT (id) DO NOTHING; + +-- Link the demo manager staff record to the Better-Auth user +UPDATE staff +SET user_id = 'ba-user-manager', updated_at = NOW() +WHERE oidc_sub = 'demo-manager-001' AND user_id IS NULL; diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 7a47235..9bc272a 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -85,6 +85,55 @@ "when": 1742587200000, "tag": "0011_impersonation_indexes", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1774080000000, + "tag": "0012_pet_photo", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1774166400000, + "tag": "0013_appointment_confirmation", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1774252800000, + "tag": "0014_customer_notes", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1774339200000, + "tag": "0015_waitlist", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1774425600000, + "tag": "0016_ical_token", + "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1774512000000, + "tag": "0017_better_auth_tables", + "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1774598400000, + "tag": "0018_backfill_staff_user_id", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/factories.ts b/packages/db/src/factories.ts index b2327e3..df67583 100644 --- a/packages/db/src/factories.ts +++ b/packages/db/src/factories.ts @@ -50,6 +50,7 @@ export function buildStaff(overrides: Partial = {}): StaffRow { name: `Staff Member ${id}`, email: `${id}@groombook.test`, oidcSub: `oidc-${id}`, + userId: null, role: "groomer", active: true, icalToken: null, diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index ddcddc7..e1bb62f 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -48,6 +48,58 @@ export const clientStatusEnum = pgEnum("client_status", [ "disabled", ]); +// ─── Better-Auth Tables ────────────────────────────────────────────────────── + +export const user = pgTable("user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").notNull().default(false), + image: text("image"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const session = pgTable("session", { + id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const account = pgTable("account", { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const verification = pgTable("verification", { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + // ─── Tables ─────────────────────────────────────────────────────────────────── export const clients = pgTable("clients", { @@ -104,6 +156,8 @@ export const staff = pgTable("staff", { email: text("email").notNull().unique(), // oidcSub links to the Authentik OIDC subject claim oidcSub: text("oidc_sub").unique(), + // Better-Auth user ID — links staff business record to auth identity + userId: text("user_id").references(() => user.id, { onDelete: "set null" }), role: staffRoleEnum("role").notNull().default("groomer"), active: boolean("active").notNull().default(true), // Token for iCal calendar feed subscription (no auth required) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13caf7c..029de5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,26 +26,23 @@ importers: specifier: ^1.13.7 version: 1.19.11(hono@4.12.8) '@hono/zod-validator': - specifier: ^0.4.3 - version: 0.4.3(hono@4.12.8)(zod@3.25.76) + specifier: ^0.7.6 + version: 0.7.6(hono@4.12.8)(zod@4.3.6) + better-auth: + specifier: ^1.5.6 + version: 1.5.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)) hono: specifier: ^4.6.17 version: 4.12.8 - jose: - specifier: ^5.9.6 - version: 5.10.0 node-cron: specifier: ^3.0.3 version: 3.0.3 nodemailer: specifier: ^6.9.16 version: 6.10.1 - openid-client: - specifier: ^6.1.7 - version: 6.8.2 zod: - specifier: ^3.24.1 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@types/node': specifier: ^22.10.7 @@ -89,6 +86,9 @@ importers: '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)) + better-auth: + specifier: ^1.0.0 + version: 1.5.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)) lucide-react: specifier: ^0.577.0 version: 0.577.0(react@19.2.4) @@ -155,7 +155,7 @@ importers: dependencies: drizzle-orm: specifier: ^0.38.4 - version: 0.38.4(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4) + version: 0.38.4(@opentelemetry/api@1.9.1)(@types/react@19.2.14)(kysely@0.28.14)(postgres@3.4.8)(react@19.2.4) postgres: specifier: ^3.4.5 version: 3.4.8 @@ -875,6 +875,81 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@better-auth/core@1.5.6': + resolution: {integrity: sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==} + peerDependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' + '@opentelemetry/api': ^1.9.0 + better-call: 1.3.2 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + '@better-auth/drizzle-adapter@1.5.6': + resolution: {integrity: sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + drizzle-orm: '>=0.41.0' + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.5.6': + resolution: {integrity: sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + kysely: ^0.27.0 || ^0.28.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.5.6': + resolution: {integrity: sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + + '@better-auth/mongo-adapter@1.5.6': + resolution: {integrity: sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.5.6': + resolution: {integrity: sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.5.6': + resolution: {integrity: sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==} + peerDependencies: + '@better-auth/core': 1.5.6 + + '@better-auth/utils@0.3.1': + resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -1540,11 +1615,11 @@ packages: peerDependencies: hono: ^4 - '@hono/zod-validator@0.4.3': - resolution: {integrity: sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==} + '@hono/zod-validator@0.7.6': + resolution: {integrity: sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw==} peerDependencies: hono: '>=3.9.0' - zod: ^3.19.1 + zod: ^3.25.0 || ^4.0.0 '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -1593,6 +1668,22 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} @@ -2430,6 +2521,76 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + better-auth@1.5.6: + resolution: {integrity: sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.3.2: + resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -2629,6 +2790,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -3283,9 +3447,6 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.2.1: resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} @@ -3346,6 +3507,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kysely@0.28.14: + resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} + engines: {node: '>=20.0.0'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -3510,6 +3675,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@1.2.0: + resolution: {integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==} + engines: {node: ^20.0.0 || >=22.0.0} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3527,9 +3696,6 @@ packages: nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} - oauth4webapi@3.8.5: - resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -3542,9 +3708,6 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} - openid-client@6.8.2: - resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3777,6 +3940,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -3820,6 +3986,9 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4369,8 +4538,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} snapshots: @@ -5524,6 +5693,56 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0)': + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.2(zod@4.3.6) + jose: 6.2.1 + kysely: 0.28.14 + nanostores: 1.2.0 + zod: 4.3.6 + + '@better-auth/drizzle-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/kysely-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + optionalDependencies: + kysely: 0.28.14 + + '@better-auth/memory-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/prisma-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/telemetry@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.1': {} + + '@better-fetch/fetch@1.1.21': {} + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -5897,10 +6116,10 @@ snapshots: dependencies: hono: 4.12.8 - '@hono/zod-validator@0.4.3(hono@4.12.8)(zod@3.25.76)': + '@hono/zod-validator@0.7.6(hono@4.12.8)(zod@4.3.6)': dependencies: hono: 4.12.8 - zod: 3.25.76 + zod: 4.3.6 '@humanfs/core@0.19.1': {} @@ -5950,6 +6169,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@noble/ciphers@2.1.1': {} + + '@noble/hashes@2.0.1': {} + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/semantic-conventions@1.40.0': {} + '@petamoriken/float16@3.9.3': {} '@pkgjs/parseargs@0.11.0': @@ -6903,6 +7130,42 @@ snapshots: baseline-browser-mapping@2.10.8: {} + better-auth@1.5.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)): + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14) + '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.14)(nanostores@1.2.0)) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.3.2(zod@4.3.6) + defu: 6.1.4 + jose: 6.2.1 + kysely: 0.28.14 + nanostores: 1.2.0 + zod: 4.3.6 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + vitest: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0) + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@opentelemetry/api' + + better-call@1.3.2(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.1.0 + optionalDependencies: + zod: 4.3.6 + bowser@2.14.1: {} brace-expansion@1.1.12: @@ -7092,6 +7355,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.4: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -7110,9 +7375,11 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.38.4(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4): + drizzle-orm@0.38.4(@opentelemetry/api@1.9.1)(@types/react@19.2.14)(kysely@0.28.14)(postgres@3.4.8)(react@19.2.4): optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/react': 19.2.14 + kysely: 0.28.14 postgres: 3.4.8 react: 19.2.4 @@ -7820,8 +8087,6 @@ snapshots: jiti@2.6.1: {} - jose@5.10.0: {} - jose@6.2.1: {} js-tokens@10.0.0: {} @@ -7887,6 +8152,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kysely@0.28.14: {} + leven@3.1.0: {} levn@0.4.1: @@ -8015,6 +8282,8 @@ snapshots: nanoid@3.3.11: {} + nanostores@1.2.0: {} + natural-compare@1.4.0: {} node-cron@3.0.3: @@ -8027,8 +8296,6 @@ snapshots: nwsapi@2.2.23: {} - oauth4webapi@3.8.5: {} - object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -8042,11 +8309,6 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - openid-client@6.8.2: - dependencies: - jose: 6.2.1 - oauth4webapi: 3.8.5 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8299,6 +8561,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rou3@0.7.12: {} + rrweb-cssom@0.8.0: {} safe-array-concat@1.1.3: @@ -8340,6 +8604,8 @@ snapshots: set-cookie-parser@2.7.2: {} + set-cookie-parser@3.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -9008,4 +9274,4 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.25.76: {} + zod@4.3.6: {}