diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e8536f1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +.next +.data +backups +.git +.env* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +Dockerfile +docker-compose.yml diff --git a/.env.example b/.env.example index b6153f4..d2ad3ab 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,28 @@ # Public URL where users open Neta. NEXT_PUBLIC_SITE_URL=http://localhost:3000 -# Supabase project API URL, for example: -# https://your-project-ref.supabase.co +# Canonical server-side app URL used by auth callbacks and trusted origin checks. +# Defaults to NEXT_PUBLIC_SITE_URL when empty. +APP_URL= + +# Optional Better Auth base URL override. Defaults to APP_URL/NEXT_PUBLIC_SITE_URL. +BETTER_AUTH_URL= + +# Required at production runtime. Generate with: openssl rand -base64 32 +BETTER_AUTH_SECRET= + +# Optional comma-separated extra trusted origins. Wildcards are rejected. +TRUSTED_ORIGINS= + +# Persistent application data directory. In Docker this should be /app/data. +DATA_DIR=.data + +# Optional explicit SQLite database path. Defaults to DATA_DIR/neta.db. +DATABASE_PATH= + +# Legacy Supabase values are kept only while old feature data screens are being migrated. NEXT_PUBLIC_SUPABASE_URL= -# Supabase anon/public key. NEXT_PUBLIC_SUPABASE_ANON_KEY= -# Supabase service role key. Required for creating the first admin, -# creating client portal users, and server-side storage uploads. -# Keep this secret. Never expose it with a NEXT_PUBLIC_ prefix. SUPABASE_SERVICE_ROLE_KEY= diff --git a/.gitignore b/.gitignore index b6f7fde..ae28c77 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ out dist build backups/ +.data/ .env* !.env.example !.env.full.example diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..48176cd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM node:22-bookworm-slim AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:22-bookworm-slim AS builder +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +FROM node:22-bookworm-slim AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 +ENV DATA_DIR=/app/data + +RUN groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs \ + && mkdir -p /app/data \ + && chown -R nextjs:nodejs /app/data + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/server/db/migrations ./server/db/migrations +COPY --from=builder /app/node_modules/drizzle-orm ./node_modules/drizzle-orm +COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3 +COPY --from=builder /app/node_modules/bindings ./node_modules/bindings +COPY --from=builder /app/node_modules/file-uri-to-path ./node_modules/file-uri-to-path + +USER nextjs +EXPOSE 3000 +VOLUME ["/app/data"] +CMD ["sh", "-c", "node scripts/migrate.mjs && node server.js"] diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 67e50a4..6bb277e 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,53 +1,30 @@ import { DashboardShell } from "@/components/layout/dashboard-shell"; -import { createClient } from "@/lib/supabase/server"; +import { requireFreelancer } from "@/server/auth/session"; export default async function DashboardLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - const supabase = await createClient(); - const { - data: { user }, - } = await supabase.auth.getUser(); + const { user, profile } = await requireFreelancer(); + const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Neta Kullanıcısı"; - const { data: profile } = user - ? await supabase - .from("profiles") - .select("first_name, last_name, avatar_url, role") - .eq("id", user.id) - .maybeSingle() - : { data: null }; - - if (profile?.role === "client") { - const { redirect } = await import("next/navigation"); - redirect("/portal"); - } - - const fallbackName = user?.email?.split("@")[0] ?? "Neta Kullanıcısı"; - const displayName = - [profile?.first_name, profile?.last_name].filter(Boolean).join(" ") || - fallbackName; - - const shortName = displayName - .split(" ") - .filter(Boolean) - .slice(0, 2) - .map((part) => part[0]?.toUpperCase()) - .join("") - .slice(0, 2) || "MS"; + const shortName = + displayName + .split(" ") + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join("") + .slice(0, 2) || "MS"; return ( {children} diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..063dff7 --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,7 @@ +import { toNextJsHandler } from "better-auth/next-js"; +import { auth } from "@/server/auth/auth"; + +export const runtime = "nodejs"; + +export const { GET, POST } = toNextJsHandler(auth); + diff --git a/app/api/health/live/route.ts b/app/api/health/live/route.ts new file mode 100644 index 0000000..a47aee8 --- /dev/null +++ b/app/api/health/live/route.ts @@ -0,0 +1,8 @@ +export const runtime = "nodejs"; + +export function GET() { + return Response.json({ + status: "ok", + timestamp: new Date().toISOString(), + }); +} diff --git a/app/api/health/ready/route.ts b/app/api/health/ready/route.ts new file mode 100644 index 0000000..ccd0e8a --- /dev/null +++ b/app/api/health/ready/route.ts @@ -0,0 +1,16 @@ +import { checkReadiness } from "@/server/db/health"; + +export const runtime = "nodejs"; + +export function GET() { + const readiness = checkReadiness(); + + return Response.json( + { + status: readiness.ok ? "ok" : "unhealthy", + checks: readiness.checks, + timestamp: new Date().toISOString(), + }, + { status: readiness.ok ? 200 : 503 }, + ); +} diff --git a/app/login/actions.ts b/app/login/actions.ts index ab31a0d..e8d4a97 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -1,31 +1,56 @@ 'use server' import { revalidatePath } from 'next/cache' +import { headers } from 'next/headers' import { redirect } from 'next/navigation' -import { createClient } from '@/lib/supabase/server' -import { getFirstAdminSetupState } from '@/lib/auth/first-admin-setup' -import { createInternalAuthUser } from '@/lib/auth/internal-users' +import { auth } from '@/server/auth/auth' +import { getProfileByAuthUserId } from '@/server/auth/session' +import { getFirstFreelancerSetupState, recordAuthAuditEvent } from '@/server/auth/setup' +import { getDefaultDisplayName, parseAuthCredentials } from '@/server/auth/validation' + +const genericLoginError = 'E-posta veya şifre hatalı.' export async function login(formData: FormData) { - const supabase = await createClient() + const credentials = parseAuthCredentials(formData) + let redirectTarget = '/' - const data = { - email: formData.get('email') as string, - password: formData.get('password') as string, - } + try { + const result = await auth.api.signInEmail({ + body: { + email: credentials.email, + password: credentials.password, + rememberMe: true, + }, + }) + const profile = getProfileByAuthUserId(result.user.id) - const { error } = await supabase.auth.signInWithPassword(data) + if (!profile || profile.disabled) { + await auth.api.signOut({ headers: await headers() }) + await recordAuthAuditEvent({ + type: 'login_failed', + authUserId: result.user.id, + email: credentials.email, + metadata: { reason: 'missing_or_disabled_profile' }, + }) + redirect(`/login?error=true&message=${encodeURIComponent(genericLoginError)}`) + } - if (error) { - redirect(`/login?error=true&message=${encodeURIComponent(error.message)}`) + redirectTarget = profile.role === 'client' ? '/portal' : '/' + } catch { + await recordAuthAuditEvent({ + type: 'login_failed', + email: credentials.email, + metadata: { reason: 'invalid_credentials' }, + }) + redirect(`/login?error=true&message=${encodeURIComponent(genericLoginError)}`) } revalidatePath('/', 'layout') - redirect('/') + redirect(redirectTarget) } export async function signup(formData: FormData) { - const setupState = await getFirstAdminSetupState() + const setupState = await getFirstFreelancerSetupState() if (setupState.errorMessage) { redirect(`/register?error=true&message=${encodeURIComponent(setupState.errorMessage)}`) @@ -34,44 +59,33 @@ export async function signup(formData: FormData) { if (!setupState.available) { redirect( `/login?error=true&message=${encodeURIComponent( - 'Kayıt kapalı. Bu Neta kurulumunda ilk admin hesabı zaten oluşturulmuş.', + 'Kayıt kapalı. Bu Neta kurulumunda ilk freelancer hesabı zaten oluşturulmuş.', )}`, ) } - const data = { - email: formData.get('email') as string, - password: formData.get('password') as string, - } + const credentials = parseAuthCredentials(formData) try { - await createInternalAuthUser({ - email: data.email, - password: data.password, - role: 'freelancer', - reason: 'first_admin', + await auth.api.signUpEmail({ + body: { + name: getDefaultDisplayName(credentials.email), + email: credentials.email, + password: credentials.password, + rememberMe: true, + }, }) } catch (error) { - const message = - error instanceof Error ? error.message : 'Kullanıcı oluşturulamadı.' + const message = error instanceof Error ? error.message : 'Kullanıcı oluşturulamadı.' redirect(`/register?error=true&message=${encodeURIComponent(message)}`) } - const supabase = await createClient() - const { error } = await supabase.auth.signInWithPassword(data) - - if (error) { - redirect(`/login?error=true&message=${encodeURIComponent(error.message)}`) - } - revalidatePath('/', 'layout') redirect('/') } export async function signOut() { - const supabase = await createClient() - - await supabase.auth.signOut() + await auth.api.signOut({ headers: await headers() }) revalidatePath('/', 'layout') redirect('/login') diff --git a/app/portal/layout.tsx b/app/portal/layout.tsx index b569a5b..5e62d04 100644 --- a/app/portal/layout.tsx +++ b/app/portal/layout.tsx @@ -1,71 +1,32 @@ import { PortalShell } from "@/components/layout/portal-shell"; -import { createClient } from "@/lib/supabase/server"; -import { redirect } from "next/navigation"; +import { requireClientUser } from "@/server/auth/session"; export default async function PortalLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - const supabase = await createClient(); - const { - data: { user }, - } = await supabase.auth.getUser(); + const { user, profile } = await requireClientUser(); + const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Müşteri"; - if (!user) { - redirect("/login"); - } - - const { data: profile } = await supabase - .from("profiles") - .select("first_name, last_name, avatar_url, role") - .eq("id", user.id) - .maybeSingle(); - - if (profile?.role !== "client") { - redirect("/"); - } - - const fallbackName = user.email?.split("@")[0] ?? "Müşteri"; - const displayName = - [profile?.first_name, profile?.last_name].filter(Boolean).join(" ") || - fallbackName; - - const shortName = displayName - .split(" ") - .filter(Boolean) - .slice(0, 2) - .map((part) => part[0]?.toUpperCase()) - .join("") - .slice(0, 2) || "MS"; - - const { data: clientData } = await supabase - .from("clients") - .select("id") - .eq("client_auth_id", user.id) - .maybeSingle(); - - let avgProgress = 0; - if (clientData) { - const { data: projectsData } = await supabase - .from("projects") - .select("progress") - .eq("client_id", clientData.id) - .eq("status", "active"); - if (projectsData && projectsData.length > 0) { - avgProgress = Math.round(projectsData.reduce((sum, p) => sum + p.progress, 0) / projectsData.length); - } - } + const shortName = + displayName + .split(" ") + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join("") + .slice(0, 2) || "MS"; return ( {children} diff --git a/app/register/page.tsx b/app/register/page.tsx index 8a54c1e..43633ae 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -1,19 +1,21 @@ import { signup } from "@/app/login/actions"; import { AuthPageShell } from "@/components/auth/auth-page-shell"; import { ErrorToaster } from "@/components/error-toaster"; -import { getFirstAdminSetupState } from "@/lib/auth/first-admin-setup"; +import { getFirstFreelancerSetupState } from "@/server/auth/setup"; import { LockKeyhole, Mail, UserPlus } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; -import { Button, Input, Label } from "poyraz-ui/atoms"; +import { Input, Label } from "poyraz-ui/atoms"; import { SubmitButton } from "@/components/auth/submit-button"; +export const dynamic = "force-dynamic"; + export default async function RegisterPage({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { - const setupState = await getFirstAdminSetupState(); + const setupState = await getFirstFreelancerSetupState(); if (setupState.errorMessage) { redirect(`/login?error=true&message=${encodeURIComponent(setupState.errorMessage)}`); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9c169b3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + neta: + build: + context: . + ports: + - "3000:3000" + environment: + NODE_ENV: production + DATA_DIR: /app/data + NEXT_PUBLIC_SITE_URL: http://localhost:3000 + volumes: + - neta-data:/app/data + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3000/api/health/ready').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + restart: unless-stopped + +volumes: + neta-data: diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..32d00f2 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "drizzle-kit"; +import path from "node:path"; + +const dataDir = process.env.DATA_DIR + ? path.resolve(process.env.DATA_DIR) + : path.join(process.cwd(), ".data"); + +const databasePath = process.env.DATABASE_PATH + ? path.resolve(process.env.DATABASE_PATH) + : path.join(dataDir, "neta.db"); + +export default defineConfig({ + dialect: "sqlite", + schema: "./server/db/schema/index.ts", + out: "./server/db/migrations", + dbCredentials: { + url: databasePath, + }, + strict: true, +}); diff --git a/next.config.ts b/next.config.ts index f285a3a..10055c5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,7 @@ const withPWA = withPWAInit({ }); const nextConfig: NextConfig = { + output: "standalone", experimental: { serverActions: { bodySizeLimit: "8mb", diff --git a/package-lock.json b/package-lock.json index 15ce945..ee441fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,11 +32,14 @@ "@supabase/supabase-js": "^2.105.3", "@tailwindcss/postcss": "^4.3.0", "ai": "^6.0.197", + "better-auth": "^1.6.23", + "better-sqlite3": "^12.11.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", "dexie": "^4.4.3", "dexie-react-hooks": "^4.4.0", + "drizzle-orm": "^0.45.2", "framer-motion": "^11.18.2", "lucide-react": "^1.17.0", "next": "^16.2.7", @@ -54,11 +57,13 @@ "zod": "^4.4.3" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.19.19", "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", "@types/uuid": "^10.0.0", "autoprefixer": "^10.5.0", + "drizzle-kit": "^0.31.10", "eslint": "^9.39.4", "eslint-config-next": "^16.2.7", "postcss": "^8.5.15", @@ -1848,6 +1853,154 @@ } } }, + "node_modules/@better-auth/core": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.23.tgz", + "integrity": "sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.39.0", + "@standard-schema/spec": "^1.1.0", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", + "@cloudflare/workers-types": ">=4", + "@opentelemetry/api": "^1.9.0", + "better-call": "1.3.7", + "jose": "^6.1.0", + "kysely": "^0.28.5 || ^0.29.0", + "nanostores": "^1.0.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@better-auth/drizzle-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.23.tgz", + "integrity": "sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "drizzle-orm": "^0.45.2" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/@better-auth/kysely-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.23.tgz", + "integrity": "sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "kysely": "^0.28.17 || ^0.29.0" + }, + "peerDependenciesMeta": { + "kysely": { + "optional": true + } + } + }, + "node_modules/@better-auth/memory-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.23.tgz", + "integrity": "sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2" + } + }, + "node_modules/@better-auth/mongo-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.23.tgz", + "integrity": "sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "mongodb": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + } + } + }, + "node_modules/@better-auth/prisma-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.23.tgz", + "integrity": "sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.23.tgz", + "integrity": "sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1" + } + }, + "node_modules/@better-auth/utils": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz", + "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "^2.0.1" + } + }, + "node_modules/@better-auth/utils/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@better-fetch/fetch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.1.tgz", + "integrity": "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==", + "license": "MIT", + "peer": true + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -2087,6 +2240,13 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/@ducanh2912/next-pwa": { "version": "10.2.9", "resolved": "https://registry.npmjs.org/@ducanh2912/next-pwa/-/next-pwa-10.2.9.tgz", @@ -2190,6 +2350,884 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -3317,6 +4355,15 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -5794,6 +6841,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -7180,6 +8238,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.23", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", @@ -7192,6 +8270,191 @@ "node": ">=6.0.0" } }, + "node_modules/better-auth": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.23.tgz", + "integrity": "sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.6.23", + "@better-auth/drizzle-adapter": "1.6.23", + "@better-auth/kysely-adapter": "1.6.23", + "@better-auth/memory-adapter": "1.6.23", + "@better-auth/mongo-adapter": "1.6.23", + "@better-auth/prisma-adapter": "1.6.23", + "@better-auth/telemetry": "1.6.23", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", + "@noble/ciphers": "^2.1.1", + "@noble/hashes": "^2.0.1", + "better-call": "1.3.7", + "defu": "^6.1.4", + "jose": "^6.1.3", + "kysely": "^0.28.17 || ^0.29.0", + "nanostores": "^1.1.1", + "zod": "^4.3.6" + }, + "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.45.2", + "mongodb": "^6.0.0 || ^7.0.0", + "mysql2": "^3.0.0", + "next": "^14.0.0 || ^15.0.0 || ^16.0.0", + "pg": "^8.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "solid-js": "^1.0.0", + "svelte": "^4.0.0 || ^5.0.0", + "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "@tanstack/react-start": { + "optional": true + }, + "@tanstack/solid-start": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-kit": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "next": { + "optional": true + }, + "pg": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vitest": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/better-auth/node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/better-auth/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/better-call": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.7.tgz", + "integrity": "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@better-auth/utils": "^0.4.0", + "@better-fetch/fetch": "^1.1.21", + "rou3": "^0.7.12", + "set-cookie-parser": "^3.0.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -7272,6 +8535,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7395,6 +8682,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -7871,6 +9164,21 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -7885,6 +9193,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7975,6 +9292,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8059,6 +9382,149 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -8133,6 +9599,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.22.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", @@ -8351,6 +9826,48 @@ "benchmarks" ] }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -8894,6 +10411,15 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -9081,6 +10607,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -9268,6 +10800,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", @@ -9477,7 +11015,7 @@ "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -9486,6 +11024,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -9760,6 +11304,26 @@ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", "license": "ISC" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -9822,6 +11386,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -10480,6 +12050,7 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -10622,6 +12193,16 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.3.tgz", + "integrity": "sha512-VHtBdW6XB/pgoTSqraM3UAa2rYoYdNXqnNPpX+8XXP+cwYbVEFuAp3HyPt1vpNfU9l7Y2kpUrA9QDPsy8uUqOQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -11158,6 +12739,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -11179,6 +12772,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -11218,6 +12817,28 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanostores": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.4.0.tgz", + "integrity": "sha512-i0tloweeudshAEuddpDxcg9Ik6pkPfVsHIgKyf143JrgG7/MOh0+q7BypdLXZPoOP7fOYt1eTcwGkyiVmhJFkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -11348,6 +12969,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -12064,6 +13697,33 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12148,6 +13808,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12366,6 +14036,30 @@ "node": ">= 0.10" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -12506,6 +14200,20 @@ } } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -12721,7 +14429,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -12769,6 +14477,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rou3": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz", + "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -12989,7 +14703,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -13052,6 +14765,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -13383,6 +15102,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -13490,6 +15254,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -13797,6 +15570,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -14053,6 +15854,521 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 6a2807a..0a94f36 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,14 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint ." + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "db:generate": "drizzle-kit generate", + "db:migrate": "node scripts/migrate.mjs", + "db:backup": "node scripts/backup.mjs", + "db:restore": "node scripts/restore.mjs", + "phase1:smoke": "node scripts/phase1-smoke.mjs", + "phase2:smoke": "node scripts/phase2-auth-smoke.mjs" }, "dependencies": { "@ai-sdk/google": "^3.0.80", @@ -33,11 +40,14 @@ "@supabase/supabase-js": "^2.105.3", "@tailwindcss/postcss": "^4.3.0", "ai": "^6.0.197", + "better-auth": "^1.6.23", + "better-sqlite3": "^12.11.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", "dexie": "^4.4.3", "dexie-react-hooks": "^4.4.0", + "drizzle-orm": "^0.45.2", "framer-motion": "^11.18.2", "lucide-react": "^1.17.0", "next": "^16.2.7", @@ -55,11 +65,13 @@ "zod": "^4.4.3" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.19.19", "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", "@types/uuid": "^10.0.0", "autoprefixer": "^10.5.0", + "drizzle-kit": "^0.31.10", "eslint": "^9.39.4", "eslint-config-next": "^16.2.7", "postcss": "^8.5.15", diff --git a/proxy.ts b/proxy.ts index 49e966f..9e63f88 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,13 +1,13 @@ -import { type NextRequest } from 'next/server' +import { NextResponse, type NextRequest } from 'next/server' -import { updateSession } from '@/lib/supabase/middleware' - -export async function proxy(request: NextRequest) { - return updateSession(request) +export function proxy(request: NextRequest) { + return NextResponse.next({ + request, + }) } export const config = { matcher: [ - '/((?!api/health|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + '/((?!api/health|api/auth|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], } diff --git a/scripts/backup.mjs b/scripts/backup.mjs new file mode 100644 index 0000000..7a0199b --- /dev/null +++ b/scripts/backup.mjs @@ -0,0 +1,83 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import Database from "better-sqlite3"; +import { applySqlitePragmas, ensureDataLayout } from "./lib/data-dir.mjs"; + +const config = ensureDataLayout(); +const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); +const backupDir = path.join(config.backupsDir, `neta-${timestamp}`); +const uploadsBackupDir = path.join(backupDir, "uploads"); +const databaseBackupPath = path.join(backupDir, "neta.db"); + +fs.mkdirSync(backupDir, { recursive: true }); + +const sqlite = new Database(config.databasePath); + +try { + applySqlitePragmas(sqlite); + await sqlite.backup(databaseBackupPath); +} finally { + sqlite.close(); +} + +copyDirectoryIfExists(config.uploadsDir, uploadsBackupDir); + +const manifest = { + createdAt: new Date().toISOString(), + source: { + dataDir: config.dataDir, + databasePath: config.databasePath, + uploadsDir: config.uploadsDir, + }, + files: collectFiles(backupDir).map((filePath) => ({ + path: path.relative(backupDir, filePath).replace(/\\/g, "/"), + bytes: fs.statSync(filePath).size, + sha256: hashFile(filePath), + })), +}; + +fs.writeFileSync(path.join(backupDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); + +console.log(`Backup created at ${backupDir}`); + +function copyDirectoryIfExists(sourceDir, targetDir) { + if (!fs.existsSync(sourceDir)) { + return; + } + + fs.mkdirSync(targetDir, { recursive: true }); + + for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { + const sourcePath = path.join(sourceDir, entry.name); + const targetPath = path.join(targetDir, entry.name); + + if (entry.isDirectory()) { + copyDirectoryIfExists(sourcePath, targetPath); + } else if (entry.isFile()) { + fs.copyFileSync(sourcePath, targetPath); + } + } +} + +function collectFiles(rootDir) { + const files = []; + + for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) { + const entryPath = path.join(rootDir, entry.name); + + if (entry.isDirectory()) { + files.push(...collectFiles(entryPath)); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + + return files; +} + +function hashFile(filePath) { + const hash = crypto.createHash("sha256"); + hash.update(fs.readFileSync(filePath)); + return hash.digest("hex"); +} diff --git a/scripts/lib/data-dir.mjs b/scripts/lib/data-dir.mjs new file mode 100644 index 0000000..6f76c15 --- /dev/null +++ b/scripts/lib/data-dir.mjs @@ -0,0 +1,43 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function getDataConfig(env = process.env) { + const nodeEnv = env.NODE_ENV || "development"; + const dataDir = path.resolve( + env.DATA_DIR && env.DATA_DIR.trim().length > 0 + ? env.DATA_DIR + : nodeEnv === "production" + ? "/app/data" + : path.join(process.cwd(), ".data"), + ); + + const databasePath = path.resolve( + env.DATABASE_PATH && env.DATABASE_PATH.trim().length > 0 + ? env.DATABASE_PATH + : path.join(dataDir, "neta.db"), + ); + + return { + dataDir, + databasePath, + uploadsDir: path.join(dataDir, "uploads"), + backupsDir: path.join(dataDir, "backups"), + tmpDir: path.join(dataDir, "tmp"), + migrationsDir: path.join(process.cwd(), "server", "db", "migrations"), + }; +} + +export function ensureDataLayout(config = getDataConfig()) { + for (const dir of [config.dataDir, config.uploadsDir, config.backupsDir, config.tmpDir]) { + fs.mkdirSync(dir, { recursive: true }); + } + + return config; +} + +export function applySqlitePragmas(sqlite) { + sqlite.pragma("foreign_keys = ON"); + sqlite.pragma("journal_mode = WAL"); + sqlite.pragma("synchronous = NORMAL"); + sqlite.pragma("busy_timeout = 5000"); +} diff --git a/scripts/migrate.mjs b/scripts/migrate.mjs new file mode 100644 index 0000000..4280370 --- /dev/null +++ b/scripts/migrate.mjs @@ -0,0 +1,39 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { pathToFileURL } from "node:url"; +import { applySqlitePragmas, ensureDataLayout } from "./lib/data-dir.mjs"; + +export function runMigrations(databasePath) { + const config = ensureDataLayout(); + const sqlite = new Database(databasePath ?? config.databasePath); + + try { + applySqlitePragmas(sqlite); + const db = drizzle({ client: sqlite }); + + migrate(db, { migrationsFolder: config.migrationsDir }); + + const now = Date.now(); + sqlite + .prepare( + `insert into runtime_checks (key, value, created_at, updated_at) + values (@key, @value, @createdAt, @updatedAt) + on conflict(key) do update set value = excluded.value, updated_at = excluded.updated_at`, + ) + .run({ + key: "last_migration", + value: new Date(now).toISOString(), + createdAt: now, + updatedAt: now, + }); + + console.log(`Migrations applied to ${databasePath ?? config.databasePath}`); + } finally { + sqlite.close(); + } +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + runMigrations(); +} diff --git a/scripts/phase1-smoke.mjs b/scripts/phase1-smoke.mjs new file mode 100644 index 0000000..2c2f7c8 --- /dev/null +++ b/scripts/phase1-smoke.mjs @@ -0,0 +1,86 @@ +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import Database from "better-sqlite3"; + +const smokeRoot = path.join(process.cwd(), ".data", `phase1-smoke-${Date.now()}`); +const restoreRoot = `${smokeRoot}-restore`; +const env = { ...process.env, DATA_DIR: smokeRoot, DATABASE_PATH: "" }; + +fs.mkdirSync(smokeRoot, { recursive: true }); + +execFileSync(process.execPath, ["scripts/migrate.mjs"], { + cwd: process.cwd(), + env, + stdio: "inherit", +}); + +const dbPath = path.join(smokeRoot, "neta.db"); +const sqlite = new Database(dbPath); + +try { + const before = sqlite.prepare("select value from runtime_checks where key = ?").get("last_migration"); + + if (!before) { + throw new Error("Migration smoke check failed: runtime_checks row missing."); + } + + const now = Date.now(); + sqlite + .prepare( + `insert into runtime_checks (key, value, created_at, updated_at) + values (?, ?, ?, ?) + on conflict(key) do update set value = excluded.value, updated_at = excluded.updated_at`, + ) + .run("restart_probe", "persisted", now, now); +} finally { + sqlite.close(); +} + +const reopened = new Database(dbPath, { readonly: true }); + +try { + const row = reopened.prepare("select value from runtime_checks where key = ?").get("restart_probe"); + + if (!row || row.value !== "persisted") { + throw new Error("Restart persistence smoke check failed."); + } +} finally { + reopened.close(); +} + +execFileSync(process.execPath, ["scripts/backup.mjs"], { + cwd: process.cwd(), + env, + stdio: "inherit", +}); + +const backupDir = fs + .readdirSync(path.join(smokeRoot, "backups")) + .map((name) => path.join(smokeRoot, "backups", name)) + .sort() + .at(-1); + +if (!backupDir) { + throw new Error("Backup smoke check failed: no backup directory produced."); +} + +execFileSync(process.execPath, ["scripts/restore.mjs", "--from", backupDir, "--target", restoreRoot, "--force"], { + cwd: process.cwd(), + env: process.env, + stdio: "inherit", +}); + +const restored = new Database(path.join(restoreRoot, "neta.db"), { readonly: true }); + +try { + const row = restored.prepare("select value from runtime_checks where key = ?").get("restart_probe"); + + if (!row || row.value !== "persisted") { + throw new Error("Restore smoke check failed."); + } +} finally { + restored.close(); +} + +console.log("Phase 1 smoke checks passed."); diff --git a/scripts/phase2-auth-smoke.mjs b/scripts/phase2-auth-smoke.mjs new file mode 100644 index 0000000..012e070 --- /dev/null +++ b/scripts/phase2-auth-smoke.mjs @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import Database from "better-sqlite3"; +import { ensureDataLayout } from "./lib/data-dir.mjs"; +import { runMigrations } from "./migrate.mjs"; + +const requiredTables = [ + "user", + "session", + "account", + "verification", + "app_profiles", + "app_setup_state", + "portal_invitations", + "auth_audit_events", +]; + +const requiredIndexes = [ + "app_profiles_auth_user_id_unique", + "portal_invitations_token_hash_unique", + "session_user_id_idx", + "account_user_id_idx", +]; + +async function main() { + const paths = ensureDataLayout(); + + runMigrations(paths.databasePath); + + const sqlite = new Database(paths.databasePath, { readonly: true }); + + try { + const tables = sqlite + .prepare("select name from sqlite_master where type = 'table'") + .all() + .map((row) => row.name); + + const indexes = sqlite + .prepare("select name from sqlite_master where type = 'index'") + .all() + .map((row) => row.name); + + for (const table of requiredTables) { + assert.ok(tables.includes(table), `Missing auth table: ${table}`); + } + + for (const index of requiredIndexes) { + assert.ok(indexes.includes(index), `Missing auth index: ${index}`); + } + + const profileColumns = sqlite.prepare("pragma table_info(app_profiles)").all(); + const roleColumn = profileColumns.find((column) => column.name === "role"); + const disabledColumn = profileColumns.find((column) => column.name === "disabled"); + + assert.equal(roleColumn?.notnull, 1, "app_profiles.role must be required"); + assert.equal(disabledColumn?.notnull, 1, "app_profiles.disabled must be required"); + + console.log("Phase 2 auth smoke passed"); + } finally { + sqlite.close(); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/restore.mjs b/scripts/restore.mjs new file mode 100644 index 0000000..c9d89bd --- /dev/null +++ b/scripts/restore.mjs @@ -0,0 +1,72 @@ +import fs from "node:fs"; +import path from "node:path"; +import { ensureDataLayout, getDataConfig } from "./lib/data-dir.mjs"; + +const args = parseArgs(process.argv.slice(2)); + +if (!args.from) { + throw new Error("Usage: node scripts/restore.mjs --from [--target ] [--force]"); +} + +const targetEnv = { + ...process.env, + DATA_DIR: args.target || process.env.DATA_DIR, + DATABASE_PATH: undefined, +}; + +const config = ensureDataLayout(getDataConfig(targetEnv)); +const backupDir = path.resolve(args.from); +const backupDbPath = path.join(backupDir, "neta.db"); +const backupUploadsDir = path.join(backupDir, "uploads"); + +if (!fs.existsSync(backupDbPath)) { + throw new Error(`Backup database not found: ${backupDbPath}`); +} + +if (fs.existsSync(config.databasePath) && !args.force) { + throw new Error(`Target database exists: ${config.databasePath}. Pass --force to overwrite.`); +} + +fs.copyFileSync(backupDbPath, config.databasePath); + +if (fs.existsSync(backupUploadsDir)) { + fs.rmSync(config.uploadsDir, { recursive: true, force: true }); + copyDirectory(backupUploadsDir, config.uploadsDir); +} + +console.log(`Backup restored from ${backupDir} to ${config.dataDir}`); + +function parseArgs(values) { + const parsed = { force: false }; + + for (let index = 0; index < values.length; index += 1) { + const value = values[index]; + + if (value === "--force") { + parsed.force = true; + } else if (value === "--from") { + parsed.from = values[index + 1]; + index += 1; + } else if (value === "--target") { + parsed.target = values[index + 1]; + index += 1; + } + } + + return parsed; +} + +function copyDirectory(sourceDir, targetDir) { + fs.mkdirSync(targetDir, { recursive: true }); + + for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { + const sourcePath = path.join(sourceDir, entry.name); + const targetPath = path.join(targetDir, entry.name); + + if (entry.isDirectory()) { + copyDirectory(sourcePath, targetPath); + } else if (entry.isFile()) { + fs.copyFileSync(sourcePath, targetPath); + } + } +} diff --git a/server/auth/auth.ts b/server/auth/auth.ts new file mode 100644 index 0000000..2771bd3 --- /dev/null +++ b/server/auth/auth.ts @@ -0,0 +1,95 @@ +import "server-only"; + +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { nextCookies } from "better-auth/next-js"; +import { getServerConfig } from "@/server/config"; +import { getSqliteConnection } from "@/server/db/client"; +import * as schema from "@/server/db/schema"; +import { + completeFirstFreelancerSetup, + recordAuthAuditEvent, + reserveFirstFreelancerSetup, +} from "@/server/auth/setup"; + +const config = getServerConfig(); + +export const auth = betterAuth({ + appName: "Neta", + baseURL: config.appUrl, + trustedOrigins: config.trustedOrigins, + secret: config.betterAuthSecret, + database: drizzleAdapter(getSqliteConnection().db, { + provider: "sqlite", + schema, + transaction: true, + }), + emailAndPassword: { + enabled: true, + minPasswordLength: 8, + maxPasswordLength: 128, + requireEmailVerification: false, + }, + rateLimit: { + enabled: true, + window: 60, + max: 60, + customRules: { + "/sign-in/email": { + window: 60, + max: 10, + }, + "/sign-up/email": { + window: 300, + max: 3, + }, + }, + }, + advanced: { + useSecureCookies: config.nodeEnv === "production", + cookiePrefix: "neta", + defaultCookieAttributes: { + httpOnly: true, + sameSite: "lax", + secure: config.nodeEnv === "production", + path: "/", + }, + }, + databaseHooks: { + user: { + create: { + before: async (user) => { + const isReserved = await reserveFirstFreelancerSetup(user.email); + return isReserved; + }, + after: async (user) => { + await completeFirstFreelancerSetup(user); + }, + }, + }, + session: { + create: { + after: async (session) => { + await recordAuthAuditEvent({ + type: "login_succeeded", + authUserId: session.userId, + metadata: { source: "session_create" }, + }); + }, + }, + delete: { + after: async (session) => { + await recordAuthAuditEvent({ + type: "logout_succeeded", + authUserId: session.userId, + metadata: { source: "session_delete" }, + }); + }, + }, + }, + }, + plugins: [nextCookies()], +}); + +export type Auth = typeof auth; + diff --git a/server/auth/authorization.ts b/server/auth/authorization.ts new file mode 100644 index 0000000..591cb79 --- /dev/null +++ b/server/auth/authorization.ts @@ -0,0 +1,45 @@ +import "server-only"; + +import type { SessionContext } from "@/server/auth/session"; +import type { UserRole } from "@/server/auth/types"; + +export class AuthorizationError extends Error { + constructor( + message = "Bu işlem için yetkiniz yok.", + public readonly code: "UNAUTHENTICATED" | "FORBIDDEN" | "NOT_FOUND" = "FORBIDDEN", + ) { + super(message); + this.name = "AuthorizationError"; + } +} + +export function assertRole(context: SessionContext | null, allowedRoles: readonly UserRole[]): void { + if (!context) { + throw new AuthorizationError("Oturum gerekli.", "UNAUTHENTICATED"); + } + + if (!allowedRoles.includes(context.profile.role)) { + throw new AuthorizationError(); + } +} + +export function assertSameOwner(context: SessionContext | null, ownerAuthUserId: string): void { + if (!context) { + throw new AuthorizationError("Oturum gerekli.", "UNAUTHENTICATED"); + } + + if (context.user.id !== ownerAuthUserId) { + throw new AuthorizationError("Kaynak bulunamadı.", "NOT_FOUND"); + } +} + +export function assertEnabledUser(context: SessionContext | null): void { + if (!context) { + throw new AuthorizationError("Oturum gerekli.", "UNAUTHENTICATED"); + } + + if (context.profile.disabled) { + throw new AuthorizationError(); + } +} + diff --git a/server/auth/session.ts b/server/auth/session.ts new file mode 100644 index 0000000..6f07c4f --- /dev/null +++ b/server/auth/session.ts @@ -0,0 +1,99 @@ +import "server-only"; + +import { eq } from "drizzle-orm"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { cache } from "react"; +import { auth } from "@/server/auth/auth"; +import type { UserRole } from "@/server/auth/types"; +import { getSqliteConnection } from "@/server/db/client"; +import { appProfiles } from "@/server/db/schema"; + +type BetterAuthSession = NonNullable>>; + +export type SessionContext = { + session: BetterAuthSession["session"]; + user: BetterAuthSession["user"]; + profile: { + id: number; + authUserId: string; + email: string; + displayName: string; + role: UserRole; + disabled: boolean; + }; +}; + +export const getSessionContext = cache(async (): Promise => { + const session = await auth.api.getSession({ + headers: await headers(), + query: { + disableCookieCache: true, + }, + }); + + if (!session) { + return null; + } + + const profile = getProfileByAuthUserId(session.user.id); + + if (!profile || profile.disabled || profile.authUserId !== session.user.id) { + return null; + } + + return { + session: session.session, + user: session.user, + profile, + }; +}); + +export async function requireSession(): Promise { + const context = await getSessionContext(); + + if (!context) { + redirect("/login"); + } + + return context; +} + +export async function requireFreelancer(): Promise { + const context = await requireSession(); + + if (context.profile.role !== "freelancer") { + redirect("/portal"); + } + + return context; +} + +export async function requireClientUser(): Promise { + const context = await requireSession(); + + if (context.profile.role !== "client") { + redirect("/"); + } + + return context; +} + +export function getProfileByAuthUserId(authUserId: string): SessionContext["profile"] | null { + const { db } = getSqliteConnection(); + const [profile] = db + .select({ + id: appProfiles.id, + authUserId: appProfiles.authUserId, + email: appProfiles.email, + displayName: appProfiles.displayName, + role: appProfiles.role, + disabled: appProfiles.disabled, + }) + .from(appProfiles) + .where(eq(appProfiles.authUserId, authUserId)) + .limit(1) + .all(); + + return profile ?? null; +} diff --git a/server/auth/setup.ts b/server/auth/setup.ts new file mode 100644 index 0000000..fdfd951 --- /dev/null +++ b/server/auth/setup.ts @@ -0,0 +1,207 @@ +import "server-only"; + +import { count, eq } from "drizzle-orm"; +import { getSqliteConnection } from "@/server/db/client"; +import { appProfiles, appSetupState, authAuditEvents } from "@/server/db/schema"; +import type { AuthAuditEventType } from "@/server/auth/types"; +import { getDefaultDisplayName, normalizeAuthEmail } from "@/server/auth/validation"; + +const FIRST_FREELANCER_SETUP_KEY = "first_freelancer"; +const SETUP_LOCK_TTL_MS = 10 * 60 * 1000; + +export type FirstFreelancerSetupState = { + available: boolean; + locked: boolean; + errorMessage?: string; +}; + +export async function getFirstFreelancerSetupState(): Promise { + try { + return readFirstFreelancerSetupState(); + } catch (error) { + return { + available: false, + locked: false, + errorMessage: + error instanceof Error + ? error.message + : "İlk kurulum durumu okunamadı.", + }; + } +} + +export function readFirstFreelancerSetupState(): FirstFreelancerSetupState { + const { db } = getSqliteConnection(); + const [{ value: freelancerCount }] = db + .select({ value: count() }) + .from(appProfiles) + .where(eq(appProfiles.role, "freelancer")) + .all(); + + if (freelancerCount > 0) { + return { available: false, locked: false }; + } + + const [setupState] = db + .select() + .from(appSetupState) + .where(eq(appSetupState.key, FIRST_FREELANCER_SETUP_KEY)) + .limit(1) + .all(); + + if (!setupState) { + return { available: true, locked: false }; + } + + if (setupState.status === "completed") { + return { available: false, locked: false }; + } + + const lockedAt = setupState.lockedAt?.getTime() ?? 0; + const isStale = Date.now() - lockedAt > SETUP_LOCK_TTL_MS; + + return { + available: isStale, + locked: !isStale, + errorMessage: isStale ? undefined : "İlk kurulum şu anda başka bir istek tarafından işleniyor.", + }; +} + +export async function reserveFirstFreelancerSetup(email: string): Promise { + const normalizedEmail = normalizeAuthEmail(email); + const { db } = getSqliteConnection(); + + return db.transaction((tx) => { + const [{ value: freelancerCount }] = tx + .select({ value: count() }) + .from(appProfiles) + .where(eq(appProfiles.role, "freelancer")) + .all(); + + if (freelancerCount > 0) { + return false; + } + + const [setupState] = tx + .select() + .from(appSetupState) + .where(eq(appSetupState.key, FIRST_FREELANCER_SETUP_KEY)) + .limit(1) + .all(); + + const now = new Date(); + + if (setupState?.status === "completed") { + return false; + } + + if (setupState?.status === "pending") { + const lockedAt = setupState.lockedAt?.getTime() ?? 0; + + if (Date.now() - lockedAt <= SETUP_LOCK_TTL_MS) { + return false; + } + } + + tx.insert(appSetupState) + .values({ + key: FIRST_FREELANCER_SETUP_KEY, + status: "pending", + lockedBy: normalizedEmail, + lockedAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: appSetupState.key, + set: { + status: "pending", + lockedBy: normalizedEmail, + lockedAt: now, + updatedAt: now, + }, + }) + .run(); + + tx.insert(authAuditEvents) + .values({ + type: "setup_started", + email: normalizedEmail, + metadata: { source: "better_auth_user_create" }, + }) + .run(); + + return true; + }); +} + +export async function completeFirstFreelancerSetup(user: { + id: string; + email: string; + name?: string | null; +}): Promise { + const normalizedEmail = normalizeAuthEmail(user.email); + const now = new Date(); + const { db } = getSqliteConnection(); + + db.transaction((tx) => { + tx.insert(appProfiles) + .values({ + authUserId: user.id, + email: normalizedEmail, + displayName: user.name || getDefaultDisplayName(normalizedEmail), + role: "freelancer", + disabled: false, + createdAt: now, + updatedAt: now, + }) + .onConflictDoNothing() + .run(); + + tx.insert(appSetupState) + .values({ + key: FIRST_FREELANCER_SETUP_KEY, + status: "completed", + lockedBy: normalizedEmail, + lockedAt: now, + completedAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: appSetupState.key, + set: { + status: "completed", + lockedBy: normalizedEmail, + completedAt: now, + updatedAt: now, + }, + }) + .run(); + + tx.insert(authAuditEvents) + .values({ + type: "setup_completed", + authUserId: user.id, + email: normalizedEmail, + metadata: { role: "freelancer" }, + }) + .run(); + }); +} + +export async function recordAuthAuditEvent(input: { + type: AuthAuditEventType; + authUserId?: string | null; + email?: string | null; + metadata?: Record | null; +}): Promise { + const { db } = getSqliteConnection(); + + db.insert(authAuditEvents) + .values({ + type: input.type, + authUserId: input.authUserId ?? null, + email: input.email ? normalizeAuthEmail(input.email) : null, + metadata: input.metadata ?? null, + }) + .run(); +} diff --git a/server/auth/types.ts b/server/auth/types.ts new file mode 100644 index 0000000..8771d75 --- /dev/null +++ b/server/auth/types.ts @@ -0,0 +1,13 @@ +export const userRoles = ["freelancer", "client"] as const; + +export type UserRole = (typeof userRoles)[number]; + +export type SetupStatus = "pending" | "completed"; + +export type AuthAuditEventType = + | "setup_started" + | "setup_completed" + | "login_succeeded" + | "login_failed" + | "logout_succeeded"; + diff --git a/server/auth/validation.ts b/server/auth/validation.ts new file mode 100644 index 0000000..c74abe9 --- /dev/null +++ b/server/auth/validation.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const authCredentialsSchema = z.object({ + email: z.email().transform((value) => normalizeAuthEmail(value)), + password: z.string().min(8).max(128), +}); + +export type AuthCredentials = z.infer; + +export function parseAuthCredentials(formData: FormData): AuthCredentials { + return authCredentialsSchema.parse({ + email: formData.get("email"), + password: formData.get("password"), + }); +} + +export function normalizeAuthEmail(value: string): string { + return value.trim().toLowerCase(); +} + +export function getDefaultDisplayName(email: string): string { + return email.split("@")[0] || "Neta Kullanıcısı"; +} + diff --git a/server/config.ts b/server/config.ts new file mode 100644 index 0000000..a9ff6e4 --- /dev/null +++ b/server/config.ts @@ -0,0 +1,128 @@ +import "server-only"; + +import fs from "node:fs"; +import path from "node:path"; +import { z } from "zod"; + +const envSchema = z.object({ + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + APP_URL: z.string().trim().optional(), + NEXT_PUBLIC_SITE_URL: z.string().trim().optional(), + BETTER_AUTH_URL: z.string().trim().optional(), + BETTER_AUTH_SECRET: z.string().trim().optional(), + TRUSTED_ORIGINS: z.string().trim().optional(), + DATA_DIR: z.string().trim().optional(), + DATABASE_PATH: z.string().trim().optional(), +}); + +export type ServerConfig = { + nodeEnv: "development" | "test" | "production"; + dataDir: string; + databasePath: string; + uploadsDir: string; + backupsDir: string; + tmpDir: string; + appUrl: string; + trustedOrigins: string[]; + betterAuthSecret?: string; +}; + +let cachedConfig: ServerConfig | undefined; + +export function getServerConfig(): ServerConfig { + if (cachedConfig) { + return cachedConfig; + } + + const parsed = envSchema.parse(process.env); + const dataDir = path.resolve( + parsed.DATA_DIR && parsed.DATA_DIR.length > 0 + ? parsed.DATA_DIR + : parsed.NODE_ENV === "production" + ? "/app/data" + : path.join(process.cwd(), ".data"), + ); + + const databasePath = path.resolve( + parsed.DATABASE_PATH && parsed.DATABASE_PATH.length > 0 + ? parsed.DATABASE_PATH + : path.join(dataDir, "neta.db"), + ); + + const appUrl = normalizeOrigin( + parsed.BETTER_AUTH_URL || + parsed.APP_URL || + parsed.NEXT_PUBLIC_SITE_URL || + "http://localhost:3000", + ); + const trustedOrigins = normalizeTrustedOrigins(parsed.TRUSTED_ORIGINS, appUrl); + const betterAuthSecret = normalizeAuthSecret(parsed.BETTER_AUTH_SECRET, parsed.NODE_ENV); + + cachedConfig = { + nodeEnv: parsed.NODE_ENV, + dataDir, + databasePath, + uploadsDir: path.join(dataDir, "uploads"), + backupsDir: path.join(dataDir, "backups"), + tmpDir: path.join(dataDir, "tmp"), + appUrl, + trustedOrigins, + betterAuthSecret, + }; + + return cachedConfig; +} + +function normalizeOrigin(value: string): string { + const url = new URL(value); + return url.origin; +} + +function normalizeTrustedOrigins(value: string | undefined, appUrl: string): string[] { + const origins = new Set([appUrl]); + + for (const rawOrigin of value?.split(",") ?? []) { + const origin = rawOrigin.trim(); + + if (!origin) { + continue; + } + + if (origin.includes("*")) { + throw new Error("TRUSTED_ORIGINS wildcard icermemelidir."); + } + + origins.add(normalizeOrigin(origin)); + } + + return [...origins]; +} + +function normalizeAuthSecret( + value: string | undefined, + nodeEnv: ServerConfig["nodeEnv"], +): string | undefined { + if (value && value.length < 32) { + throw new Error("BETTER_AUTH_SECRET en az 32 karakter olmalidir."); + } + + if (value) { + return value; + } + + if (nodeEnv === "production" && process.env.NEXT_PHASE !== "phase-production-build") { + throw new Error("BETTER_AUTH_SECRET production runtime icin zorunludur."); + } + + if (process.env.NEXT_PHASE === "phase-production-build") { + return "build-time-placeholder-do-not-use-at-runtime"; + } + + return undefined; +} + +export function ensureDataDirectories(config = getServerConfig()): void { + for (const dir of [config.dataDir, config.uploadsDir, config.backupsDir, config.tmpDir]) { + fs.mkdirSync(dir, { recursive: true }); + } +} diff --git a/server/db/client.ts b/server/db/client.ts new file mode 100644 index 0000000..cff618f --- /dev/null +++ b/server/db/client.ts @@ -0,0 +1,64 @@ +import "server-only"; + +import Database from "better-sqlite3"; +import { drizzle, type BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; +import { ensureDataDirectories, getServerConfig } from "@/server/config"; +import * as schema from "@/server/db/schema"; + +export type SqliteConnection = { + sqlite: Database.Database; + db: BetterSQLite3Database; +}; + +const globalForSqlite = globalThis as typeof globalThis & { + __netaSqliteConnection?: SqliteConnection; + __netaSqliteCloseHandlersRegistered?: boolean; +}; + +export function getSqliteConnection(): SqliteConnection { + if (globalForSqlite.__netaSqliteConnection) { + return globalForSqlite.__netaSqliteConnection; + } + + const config = getServerConfig(); + ensureDataDirectories(config); + + const sqlite = new Database(config.databasePath); + applyPragmas(sqlite); + + const connection = { + sqlite, + db: drizzle({ client: sqlite, schema }), + }; + + globalForSqlite.__netaSqliteConnection = connection; + registerCloseHandlers(); + return connection; +} + +export function closeSqliteConnection(): void { + const connection = globalForSqlite.__netaSqliteConnection; + + if (!connection) { + return; + } + + connection.sqlite.close(); + globalForSqlite.__netaSqliteConnection = undefined; +} + +export function applyPragmas(sqlite: Database.Database): void { + sqlite.pragma("foreign_keys = ON"); + sqlite.pragma("journal_mode = WAL"); + sqlite.pragma("synchronous = NORMAL"); + sqlite.pragma("busy_timeout = 5000"); +} + +function registerCloseHandlers(): void { + if (globalForSqlite.__netaSqliteCloseHandlersRegistered || process.env.NODE_ENV !== "production") { + return; + } + + process.once("beforeExit", closeSqliteConnection); + globalForSqlite.__netaSqliteCloseHandlersRegistered = true; +} diff --git a/server/db/health.ts b/server/db/health.ts new file mode 100644 index 0000000..15afef7 --- /dev/null +++ b/server/db/health.ts @@ -0,0 +1,59 @@ +import "server-only"; + +import fs from "node:fs"; +import path from "node:path"; +import { ensureDataDirectories, getServerConfig } from "@/server/config"; +import { getSqliteConnection } from "@/server/db/client"; + +export type ReadinessStatus = { + ok: boolean; + checks: { + dataDirWritable: boolean; + databaseReachable: boolean; + migrationsApplied: boolean; + }; + error?: string; +}; + +export function checkReadiness(): ReadinessStatus { + const config = getServerConfig(); + const checks = { + dataDirWritable: false, + databaseReachable: false, + migrationsApplied: false, + }; + + try { + ensureDataDirectories(config); + assertWritableDirectory(config.dataDir); + checks.dataDirWritable = true; + + const { sqlite } = getSqliteConnection(); + sqlite.prepare("select 1 as ok").get(); + checks.databaseReachable = true; + + const migrationRow = sqlite + .prepare("select name from sqlite_master where type = 'table' and name = 'runtime_checks'") + .get(); + checks.migrationsApplied = Boolean(migrationRow); + + return { + ok: Boolean(migrationRow), + checks, + error: migrationRow ? undefined : "Migrations have not been applied.", + }; + } catch (error) { + return { + ok: false, + checks, + error: error instanceof Error ? error.message : "Unknown readiness error.", + }; + } +} + +function assertWritableDirectory(dir: string): void { + const probePath = path.join(dir, `.neta-write-${process.pid}-${Date.now()}`); + + fs.writeFileSync(probePath, "ok", { encoding: "utf8", flag: "wx" }); + fs.unlinkSync(probePath); +} diff --git a/server/db/migrations/0000_wise_reaper.sql b/server/db/migrations/0000_wise_reaper.sql new file mode 100644 index 0000000..61460ed --- /dev/null +++ b/server/db/migrations/0000_wise_reaper.sql @@ -0,0 +1,13 @@ +CREATE TABLE `runtime_checks` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `runtime_events` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `type` text NOT NULL, + `message` text NOT NULL, + `created_at` integer NOT NULL +); diff --git a/server/db/migrations/0001_silky_jetstream.sql b/server/db/migrations/0001_silky_jetstream.sql new file mode 100644 index 0000000..6b6d4a5 --- /dev/null +++ b/server/db/migrations/0001_silky_jetstream.sql @@ -0,0 +1,104 @@ +CREATE TABLE `account` ( + `id` text PRIMARY KEY NOT NULL, + `account_id` text NOT NULL, + `provider_id` text NOT NULL, + `user_id` text NOT NULL, + `access_token` text, + `refresh_token` text, + `id_token` text, + `access_token_expires_at` integer, + `refresh_token_expires_at` integer, + `scope` text, + `password` text, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `account_user_id_idx` ON `account` (`user_id`);--> statement-breakpoint +CREATE TABLE `app_profiles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `auth_user_id` text NOT NULL, + `email` text NOT NULL, + `display_name` text NOT NULL, + `role` text NOT NULL, + `disabled` integer DEFAULT false NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + FOREIGN KEY (`auth_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `app_profiles_auth_user_id_unique` ON `app_profiles` (`auth_user_id`);--> statement-breakpoint +CREATE INDEX `app_profiles_role_idx` ON `app_profiles` (`role`);--> statement-breakpoint +CREATE TABLE `app_setup_state` ( + `key` text PRIMARY KEY NOT NULL, + `status` text NOT NULL, + `locked_by` text, + `locked_at` integer, + `completed_at` integer, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `auth_audit_events` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `type` text NOT NULL, + `auth_user_id` text, + `email` text, + `metadata` text, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + FOREIGN KEY (`auth_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE INDEX `auth_audit_events_type_idx` ON `auth_audit_events` (`type`);--> statement-breakpoint +CREATE INDEX `auth_audit_events_auth_user_id_idx` ON `auth_audit_events` (`auth_user_id`);--> statement-breakpoint +CREATE TABLE `portal_invitations` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `token_hash` text NOT NULL, + `client_id` text NOT NULL, + `email` text NOT NULL, + `status` text DEFAULT 'pending' NOT NULL, + `expires_at` integer NOT NULL, + `accepted_at` integer, + `created_by_user_id` text, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + FOREIGN KEY (`created_by_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `portal_invitations_token_hash_unique` ON `portal_invitations` (`token_hash`);--> statement-breakpoint +CREATE INDEX `portal_invitations_client_id_idx` ON `portal_invitations` (`client_id`);--> statement-breakpoint +CREATE INDEX `portal_invitations_email_idx` ON `portal_invitations` (`email`);--> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY NOT NULL, + `expires_at` integer NOT NULL, + `token` text NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer NOT NULL, + `ip_address` text, + `user_agent` text, + `user_id` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint +CREATE INDEX `session_user_id_idx` ON `session` (`user_id`);--> statement-breakpoint +CREATE TABLE `user` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `email_verified` integer DEFAULT false NOT NULL, + `image` text, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint +CREATE TABLE `verification` ( + `id` text PRIMARY KEY NOT NULL, + `identifier` text NOT NULL, + `value` text NOT NULL, + `expires_at` integer NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`); \ No newline at end of file diff --git a/server/db/migrations/meta/0000_snapshot.json b/server/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..9e0d9ba --- /dev/null +++ b/server/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,94 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "edc570f7-a411-4b4b-901e-94d53532fd37", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "runtime_checks": { + "name": "runtime_checks", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_events": { + "name": "runtime_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/server/db/migrations/meta/0001_snapshot.json b/server/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..8b59cda --- /dev/null +++ b/server/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,790 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "cb8d4285-951f-4bbf-b848-953bdf769836", + "prevId": "edc570f7-a411-4b4b-901e-94d53532fd37", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_profiles": { + "name": "app_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_user_id": { + "name": "auth_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disabled": { + "name": "disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "app_profiles_auth_user_id_unique": { + "name": "app_profiles_auth_user_id_unique", + "columns": [ + "auth_user_id" + ], + "isUnique": true + }, + "app_profiles_role_idx": { + "name": "app_profiles_role_idx", + "columns": [ + "role" + ], + "isUnique": false + } + }, + "foreignKeys": { + "app_profiles_auth_user_id_user_id_fk": { + "name": "app_profiles_auth_user_id_user_id_fk", + "tableFrom": "app_profiles", + "tableTo": "user", + "columnsFrom": [ + "auth_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_setup_state": { + "name": "app_setup_state", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locked_at": { + "name": "locked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_audit_events": { + "name": "auth_audit_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_user_id": { + "name": "auth_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "auth_audit_events_type_idx": { + "name": "auth_audit_events_type_idx", + "columns": [ + "type" + ], + "isUnique": false + }, + "auth_audit_events_auth_user_id_idx": { + "name": "auth_audit_events_auth_user_id_idx", + "columns": [ + "auth_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "auth_audit_events_auth_user_id_user_id_fk": { + "name": "auth_audit_events_auth_user_id_user_id_fk", + "tableFrom": "auth_audit_events", + "tableTo": "user", + "columnsFrom": [ + "auth_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "portal_invitations": { + "name": "portal_invitations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "portal_invitations_token_hash_unique": { + "name": "portal_invitations_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "portal_invitations_client_id_idx": { + "name": "portal_invitations_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "portal_invitations_email_idx": { + "name": "portal_invitations_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "portal_invitations_created_by_user_id_user_id_fk": { + "name": "portal_invitations_created_by_user_id_user_id_fk", + "tableFrom": "portal_invitations", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_checks": { + "name": "runtime_checks", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "runtime_events": { + "name": "runtime_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/server/db/migrations/meta/_journal.json b/server/db/migrations/meta/_journal.json new file mode 100644 index 0000000..6bbebbc --- /dev/null +++ b/server/db/migrations/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1783708523046, + "tag": "0000_wise_reaper", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1783709956320, + "tag": "0001_silky_jetstream", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/server/db/schema/auth.ts b/server/db/schema/auth.ts new file mode 100644 index 0000000..39c350c --- /dev/null +++ b/server/db/schema/auth.ts @@ -0,0 +1,151 @@ +import { sql } from "drizzle-orm"; +import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import type { AuthAuditEventType, SetupStatus, UserRole } from "@/server/auth/types"; + +const nowMs = sql`(cast(unixepoch('subsecond') * 1000 as integer))`; + +export const user = sqliteTable("user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: integer("email_verified", { mode: "boolean" }).default(false).notNull(), + image: text("image"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .default(nowMs) + .$onUpdate(() => new Date()) + .notNull(), +}); + +export const session = sqliteTable( + "session", + { + id: text("id").primaryKey(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + token: text("token").notNull().unique(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + }, + (table) => [index("session_user_id_idx").on(table.userId)], +); + +export const account = sqliteTable( + "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: integer("access_token_expires_at", { mode: "timestamp_ms" }), + refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp_ms" }), + scope: text("scope"), + password: text("password"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [index("account_user_id_idx").on(table.userId)], +); + +export const verification = sqliteTable( + "verification", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .default(nowMs) + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [index("verification_identifier_idx").on(table.identifier)], +); + +export const appProfiles = sqliteTable( + "app_profiles", + { + id: integer("id").primaryKey({ autoIncrement: true }), + authUserId: text("auth_user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + email: text("email").notNull(), + displayName: text("display_name").notNull(), + role: text("role").$type().notNull(), + disabled: integer("disabled", { mode: "boolean" }).default(false).notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .default(nowMs) + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [ + uniqueIndex("app_profiles_auth_user_id_unique").on(table.authUserId), + index("app_profiles_role_idx").on(table.role), + ], +); + +export const appSetupState = sqliteTable("app_setup_state", { + key: text("key").primaryKey(), + status: text("status").$type().notNull(), + lockedBy: text("locked_by"), + lockedAt: integer("locked_at", { mode: "timestamp_ms" }), + completedAt: integer("completed_at", { mode: "timestamp_ms" }), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .default(nowMs) + .$onUpdate(() => new Date()) + .notNull(), +}); + +export const portalInvitations = sqliteTable( + "portal_invitations", + { + id: integer("id").primaryKey({ autoIncrement: true }), + tokenHash: text("token_hash").notNull(), + clientId: text("client_id").notNull(), + email: text("email").notNull(), + status: text("status", { enum: ["pending", "accepted", "revoked", "expired"] }) + .default("pending") + .notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + acceptedAt: integer("accepted_at", { mode: "timestamp_ms" }), + createdByUserId: text("created_by_user_id").references(() => user.id, { onDelete: "set null" }), + createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(), + }, + (table) => [ + uniqueIndex("portal_invitations_token_hash_unique").on(table.tokenHash), + index("portal_invitations_client_id_idx").on(table.clientId), + index("portal_invitations_email_idx").on(table.email), + ], +); + +export const authAuditEvents = sqliteTable( + "auth_audit_events", + { + id: integer("id").primaryKey({ autoIncrement: true }), + type: text("type").$type().notNull(), + authUserId: text("auth_user_id").references(() => user.id, { onDelete: "set null" }), + email: text("email"), + metadata: text("metadata", { mode: "json" }).$type | null>(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(), + }, + (table) => [ + index("auth_audit_events_type_idx").on(table.type), + index("auth_audit_events_auth_user_id_idx").on(table.authUserId), + ], +); + diff --git a/server/db/schema/index.ts b/server/db/schema/index.ts new file mode 100644 index 0000000..e5241f1 --- /dev/null +++ b/server/db/schema/index.ts @@ -0,0 +1,2 @@ +export * from "./auth"; +export * from "./runtime"; diff --git a/server/db/schema/runtime.ts b/server/db/schema/runtime.ts new file mode 100644 index 0000000..c90ce4e --- /dev/null +++ b/server/db/schema/runtime.ts @@ -0,0 +1,15 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const runtimeChecks = sqliteTable("runtime_checks", { + key: text("key").primaryKey(), + value: text("value").notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(), +}); + +export const runtimeEvents = sqliteTable("runtime_events", { + id: integer("id").primaryKey({ autoIncrement: true }), + type: text("type").notNull(), + message: text("message").notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), +}); diff --git a/server/db/transaction.ts b/server/db/transaction.ts new file mode 100644 index 0000000..077a1db --- /dev/null +++ b/server/db/transaction.ts @@ -0,0 +1,25 @@ +import "server-only"; + +import { getSqliteConnection, type SqliteConnection } from "@/server/db/client"; + +let transactionDepth = 0; + +export function runInTransaction(operation: (connection: SqliteConnection) => T): T { + const connection = getSqliteConnection(); + + if (transactionDepth > 0) { + return operation(connection); + } + + const execute = connection.sqlite.transaction(() => { + transactionDepth += 1; + + try { + return operation(connection); + } finally { + transactionDepth -= 1; + } + }); + + return execute(); +}