diff --git a/.gitignore b/.gitignore index 6aff4e36..e078189b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /node_modules /.pnp .pnp.js +.pnpm-store/ # testing /coverage diff --git a/web/app/two-factor/page.tsx b/web/app/two-factor/page.tsx new file mode 100644 index 00000000..922bc945 --- /dev/null +++ b/web/app/two-factor/page.tsx @@ -0,0 +1,17 @@ +import { Suspense } from "react"; +import { TwoFactorChallengePage } from "@/components/auth/two-factor-challenge-page"; +import { Spinner } from "@/components/ui/spinner"; + +export default function Page() { + return ( + + + + } + > + + + ); +} diff --git a/web/components/auth/sign-in-page.tsx b/web/components/auth/sign-in-page.tsx index 5e3da5a5..bad5a027 100644 --- a/web/components/auth/sign-in-page.tsx +++ b/web/components/auth/sign-in-page.tsx @@ -17,12 +17,13 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { signIn, useSession } from "@/lib/auth-client"; +import { getSafeAuthRedirect } from "@/lib/two-factor"; export function SignInPage() { const router = useRouter(); const searchParams = useSearchParams(); const { data: session, isPending } = useSession(); - const redirectTo = searchParams.get("redirect") || "/dashboard"; + const redirectTo = getSafeAuthRedirect(searchParams.get("redirect")); useEffect(() => { if (!isPending && session) { @@ -40,15 +41,22 @@ export function SignInPage() { setError(""); setLoading(true); - const { error } = await signIn.email({ + const response = await signIn.email({ email, password, }); setLoading(false); - if (error) { - setError(error.message || "Failed to sign in"); + if (response.error) { + setError(response.error.message || "Failed to sign in"); + return; + } + + if ( + (response.data as { twoFactorRedirect?: boolean } | null) + ?.twoFactorRedirect + ) { return; } diff --git a/web/components/auth/two-factor-challenge-page.tsx b/web/components/auth/two-factor-challenge-page.tsx new file mode 100644 index 00000000..f9eb22f0 --- /dev/null +++ b/web/components/auth/two-factor-challenge-page.tsx @@ -0,0 +1,192 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Spinner } from "@/components/ui/spinner"; +import { Switch } from "@/components/ui/switch"; +import { authClient, useSession } from "@/lib/auth-client"; +import { getSafeAuthRedirect } from "@/lib/two-factor"; + +type AuthClientError = { + message?: string; + error_description?: string; +} | null; + +function getErrorMessage(error: AuthClientError, fallback: string) { + return error?.message || error?.error_description || fallback; +} + +function normalizeCode(value: string) { + return value.replace(/\s/g, ""); +} + +export function TwoFactorChallengePage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { data: session, isPending } = useSession(); + const redirectTo = getSafeAuthRedirect(searchParams.get("redirect")); + const [mode, setMode] = useState<"totp" | "backup">("totp"); + const [code, setCode] = useState(""); + const [trustDevice, setTrustDevice] = useState(false); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const normalizedCode = useMemo(() => normalizeCode(code), [code]); + + useEffect(() => { + if (!isPending && session) { + router.replace(redirectTo); + } + }, [isPending, redirectTo, router, session]); + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + setError(""); + setLoading(true); + + const response = + mode === "totp" + ? await authClient.twoFactor.verifyTotp({ + code: normalizedCode, + trustDevice, + }) + : await authClient.twoFactor.verifyBackupCode({ + code: normalizedCode, + trustDevice, + }); + + setLoading(false); + + if (response.error) { + setError( + getErrorMessage( + response.error, + mode === "totp" + ? "Invalid authenticator code" + : "Invalid backup code", + ), + ); + return; + } + + router.push(redirectTo); + } + + if (isPending || session) { + return ( +
+ +
+ ); + } + + return ( +
+ Logo + + + Two-Factor Authentication + + Enter the code from your authenticator app to continue + + +
void handleSubmit(event)}> + + {error && ( +
+ {error} +
+ )} +
+ + +
+
+ + setCode(event.target.value)} + placeholder={mode === "totp" ? "123456" : "XXXX-XXXX"} + required + autoFocus + /> +
+
+
+ +

+ Skip 2FA on this browser for 30 days. +

+
+ +
+
+ + +

+ + Back to sign in + +

+
+
+
+
+ ); +} diff --git a/web/components/settings/global-settings.tsx b/web/components/settings/global-settings.tsx index 7b3d570b..189ef3ce 100644 --- a/web/components/settings/global-settings.tsx +++ b/web/components/settings/global-settings.tsx @@ -28,6 +28,7 @@ import { import { ApiKeySettings } from "@/components/settings/api-key-settings"; import { EmailSettings } from "@/components/settings/email-settings"; import { MemberSettings } from "@/components/settings/member-settings"; +import { TwoFactorSettings } from "@/components/settings/two-factor-settings"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -323,6 +324,9 @@ export function GlobalSettings({ Email + + Security + API Keys @@ -537,6 +541,10 @@ export function GlobalSettings({ /> + + + + diff --git a/web/components/settings/two-factor-settings.tsx b/web/components/settings/two-factor-settings.tsx new file mode 100644 index 00000000..eed12b1a --- /dev/null +++ b/web/components/settings/two-factor-settings.tsx @@ -0,0 +1,544 @@ +"use client"; + +import { + Copy, + KeyRound, + RefreshCw, + ShieldCheck, + ShieldOff, +} from "lucide-react"; +import Image from "next/image"; +import QRCode from "qrcode"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Item, ItemContent, ItemMedia, ItemTitle } from "@/components/ui/item"; +import { Label } from "@/components/ui/label"; +import { Spinner } from "@/components/ui/spinner"; +import { authClient, useSession } from "@/lib/auth-client"; +import { getTotpSecret } from "@/lib/two-factor"; + +type AuthClientError = { + message?: string; + error_description?: string; +} | null; + +type TwoFactorSessionUser = { + twoFactorEnabled?: boolean | null; +}; + +type PendingSetup = { + totpURI: string; + secret: string; + backupCodes: string[]; +}; + +function getErrorMessage(error: AuthClientError, fallback: string) { + return error?.message || error?.error_description || fallback; +} + +function normalizeCode(value: string) { + return value.replace(/\s/g, ""); +} + +async function copyToClipboard(label: string, value: string) { + try { + await navigator.clipboard.writeText(value); + toast.success(`${label} copied`); + } catch { + toast.error(`Failed to copy ${label.toLowerCase()}`); + } +} + +function BackupCodes({ + codes, + title = "Backup codes", +}: { + codes: string[]; + title?: string; +}) { + if (codes.length === 0) return null; + + return ( + + + {title} + +

+ Store these codes somewhere safe. Each code can be used once if your + authenticator app is unavailable. +

+
+ {codes.map((code) => ( +
+ {code} +
+ ))} +
+ +
+
+ ); +} + +export function TwoFactorSettings() { + const { data: session, refetch } = useSession(); + const sessionUser = session?.user as TwoFactorSessionUser | undefined; + const [setupPassword, setSetupPassword] = useState(""); + const [verificationCode, setVerificationCode] = useState(""); + const [pendingSetup, setPendingSetup] = useState(null); + const [setupQrCode, setSetupQrCode] = useState(""); + const [backupPassword, setBackupPassword] = useState(""); + const [disablePassword, setDisablePassword] = useState(""); + const [recoveryCodes, setRecoveryCodes] = useState([]); + const [isStartingSetup, setIsStartingSetup] = useState(false); + const [isVerifying, setIsVerifying] = useState(false); + const [isGeneratingCodes, setIsGeneratingCodes] = useState(false); + const [isDisabling, setIsDisabling] = useState(false); + + const isEnabled = Boolean( + sessionUser?.twoFactorEnabled || recoveryCodes.length, + ); + const formattedVerificationCode = useMemo( + () => normalizeCode(verificationCode), + [verificationCode], + ); + + useEffect(() => { + if (!pendingSetup?.totpURI) { + setSetupQrCode(""); + return; + } + + let shouldUseResult = true; + + QRCode.toDataURL(pendingSetup.totpURI, { + errorCorrectionLevel: "M", + margin: 2, + width: 192, + color: { + dark: "#000000", + light: "#ffffff", + }, + }) + .then((dataUrl) => { + if (shouldUseResult) setSetupQrCode(dataUrl); + }) + .catch(() => { + if (shouldUseResult) setSetupQrCode(""); + }); + + return () => { + shouldUseResult = false; + }; + }, [pendingSetup?.totpURI]); + + function refreshSession() { + return refetch({ query: { disableCookieCache: true } }); + } + + async function handleStartSetup(event: React.FormEvent) { + event.preventDefault(); + setIsStartingSetup(true); + + try { + const response = await authClient.twoFactor.enable({ + password: setupPassword, + }); + + if (response.error || !response.data?.totpURI) { + throw new Error( + getErrorMessage(response.error, "Failed to start 2FA setup"), + ); + } + + setPendingSetup({ + totpURI: response.data.totpURI, + secret: getTotpSecret(response.data.totpURI), + backupCodes: response.data.backupCodes ?? [], + }); + setSetupPassword(""); + setVerificationCode(""); + setRecoveryCodes([]); + toast.success("Authenticator setup started"); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to start 2FA setup", + ); + } finally { + setIsStartingSetup(false); + } + } + + async function handleVerifySetup(event: React.FormEvent) { + event.preventDefault(); + if (!pendingSetup) return; + + setIsVerifying(true); + try { + const response = await authClient.twoFactor.verifyTotp({ + code: formattedVerificationCode, + }); + + if (response.error) { + throw new Error( + getErrorMessage( + response.error, + "Failed to verify authenticator code", + ), + ); + } + + setRecoveryCodes(pendingSetup.backupCodes); + setPendingSetup(null); + setVerificationCode(""); + await refreshSession(); + toast.success("Two-factor authentication enabled"); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to verify authenticator code", + ); + } finally { + setIsVerifying(false); + } + } + + async function handleGenerateBackupCodes( + event: React.FormEvent, + ) { + event.preventDefault(); + setIsGeneratingCodes(true); + + try { + const response = await authClient.twoFactor.generateBackupCodes({ + password: backupPassword, + }); + + if (response.error || !response.data?.backupCodes) { + throw new Error( + getErrorMessage(response.error, "Failed to generate backup codes"), + ); + } + + setRecoveryCodes(response.data.backupCodes); + setBackupPassword(""); + toast.success("Backup codes regenerated"); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to generate backup codes", + ); + } finally { + setIsGeneratingCodes(false); + } + } + + async function handleDisable(event: React.FormEvent) { + event.preventDefault(); + setIsDisabling(true); + + try { + const response = await authClient.twoFactor.disable({ + password: disablePassword, + }); + + if (response.error || response.data?.status !== true) { + throw new Error( + getErrorMessage(response.error, "Failed to disable 2FA"), + ); + } + + setDisablePassword(""); + setBackupPassword(""); + setRecoveryCodes([]); + setPendingSetup(null); + await refreshSession(); + toast.success("Two-factor authentication disabled"); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to disable 2FA", + ); + } finally { + setIsDisabling(false); + } + } + + return ( +
+ + + + + +
+ Two-Factor Authentication + + {isEnabled ? "Enabled" : "Off"} + +
+

+ Use an authenticator app to protect your account with a 6-digit + code. +

+
+
+ +
+ {pendingSetup ? ( +
+
+
+

Set up your app

+

+ Scan the QR code or add the setup key to your authenticator + app, then enter the current 6-digit code. +

+
+ +
+
+
+ {setupQrCode ? ( + Authenticator setup QR code + ) : ( + + )} +
+
+

Scan QR code

+

+ Use Google Authenticator, 1Password, Authy, or another + TOTP-compatible app. +

+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
void handleVerifySetup(event)}> +
+
+ + + setVerificationCode(event.target.value) + } + placeholder="123456" + required + /> +
+
+ + +
+
+
+
+ ) : isEnabled ? ( +
+ + + 2FA is enabled + + Your next password sign-in will require a code from your + authenticator app unless this device is trusted. + + + + + +
void handleGenerateBackupCodes(event)} + className="rounded-lg border p-4" + > +
+
+

Regenerate backup codes

+

+ This replaces any existing backup codes. +

+
+
+ + setBackupPassword(event.target.value)} + required + /> +
+ +
+
+ +
void handleDisable(event)} + className="rounded-lg border border-destructive/40 p-4" + > +
+
+

Disable 2FA

+

+ Password-only sign-in will be allowed again for this + account. +

+
+
+ + setDisablePassword(event.target.value)} + required + /> +
+ +
+
+
+ ) : ( +
void handleStartSetup(event)}> +
+
+

Enable authenticator app

+

+ You will enter your password, add a setup key to your + authenticator app, then verify the current code. +

+
+
+ + setSetupPassword(event.target.value)} + required + /> +
+ +
+
+ )} +
+
+ ); +} diff --git a/web/db/schema.ts b/web/db/schema.ts index 2a9137a4..86da744c 100644 --- a/web/db/schema.ts +++ b/web/db/schema.ts @@ -18,6 +18,7 @@ export const user = pgTable("user", { email: text("email").notNull().unique(), emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), + twoFactorEnabled: boolean("two_factor_enabled").default(false).notNull(), role: text("role", { enum: ["admin", "developer", "reader"] }) .notNull() .default("reader"), @@ -91,6 +92,27 @@ export const verification = pgTable( (table) => [index("verification_identifier_idx").on(table.identifier)], ); +export const twoFactor = pgTable( + "twoFactor", + { + id: text("id").primaryKey(), + secret: text("secret").notNull(), + backupCodes: text("backup_codes").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + verified: boolean("verified").default(true), + failedVerificationCount: integer("failed_verification_count") + .default(0) + .notNull(), + lockedUntil: timestamp("locked_until"), + }, + (table) => [ + index("twoFactor_secret_idx").on(table.secret), + index("twoFactor_userId_idx").on(table.userId), + ], +); + export const deviceCode = pgTable( "deviceCode", { @@ -187,11 +209,12 @@ export const memberInvitations = pgTable( ], ); -export const userRelations = relations(user, ({ many }) => ({ +export const userRelations = relations(user, ({ many, one }) => ({ sessions: many(session), accounts: many(account), apiKeys: many(apikey), deviceCodes: many(deviceCode), + twoFactor: one(twoFactor), sentMemberInvitations: many(memberInvitations, { relationName: "sentMemberInvitations", }), @@ -214,6 +237,13 @@ export const accountRelations = relations(account, ({ one }) => ({ }), })); +export const twoFactorRelations = relations(twoFactor, ({ one }) => ({ + user: one(user, { + fields: [twoFactor.userId], + references: [user.id], + }), +})); + export const deviceCodeRelations = relations(deviceCode, ({ one }) => ({ user: one(user, { fields: [deviceCode.userId], diff --git a/web/lib/auth-client.ts b/web/lib/auth-client.ts index 87dcaf42..9ec15831 100644 --- a/web/lib/auth-client.ts +++ b/web/lib/auth-client.ts @@ -1,9 +1,32 @@ import { apiKeyClient } from "@better-auth/api-key/client"; -import { deviceAuthorizationClient } from "better-auth/client/plugins"; +import { + deviceAuthorizationClient, + twoFactorClient, +} from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; +import { getSafeAuthRedirect } from "@/lib/two-factor"; + +function getTwoFactorRedirectUrl() { + if (typeof window === "undefined") return "/two-factor"; + + const redirectTo = getSafeAuthRedirect( + new URLSearchParams(window.location.search).get("redirect"), + ); + const target = new URL("/two-factor", window.location.origin); + target.searchParams.set("redirect", redirectTo); + return target.toString(); +} export const authClient = createAuthClient({ - plugins: [apiKeyClient(), deviceAuthorizationClient()], + plugins: [ + apiKeyClient(), + deviceAuthorizationClient(), + twoFactorClient({ + onTwoFactorRedirect: () => { + window.location.href = getTwoFactorRedirectUrl(); + }, + }), + ], }); export const { signIn, signUp, signOut, useSession } = authClient; diff --git a/web/lib/auth.ts b/web/lib/auth.ts index 1e373fc0..73bc27fb 100644 --- a/web/lib/auth.ts +++ b/web/lib/auth.ts @@ -1,7 +1,7 @@ import { apiKey } from "@better-auth/api-key"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { bearer, deviceAuthorization } from "better-auth/plugins"; +import { bearer, deviceAuthorization, twoFactor } from "better-auth/plugins"; import { admin } from "better-auth/plugins/admin"; import { userAc } from "better-auth/plugins/admin/access"; import { headers } from "next/headers"; @@ -11,8 +11,10 @@ import type { MemberRole } from "@/db/types"; import { getUserRole, hasAnyRole } from "@/lib/members"; const TECHULUS_CLI_CLIENT_ID = "techulus-cli"; +const APP_NAME = "Techulus Cloud"; export const auth = betterAuth({ + appName: APP_NAME, database: drizzleAdapter(db, { provider: "pg", schema, @@ -47,6 +49,9 @@ export const auth = betterAuth({ reader: userAc, }, }), + twoFactor({ + issuer: APP_NAME, + }), bearer(), ], }); diff --git a/web/lib/two-factor.ts b/web/lib/two-factor.ts new file mode 100644 index 00000000..c45e0502 --- /dev/null +++ b/web/lib/two-factor.ts @@ -0,0 +1,39 @@ +export const DEFAULT_AUTH_REDIRECT = "/dashboard"; + +const AUTH_REDIRECT_ORIGIN = "https://auth.local"; + +function hasUnsafeAuthRedirectCharacter(value: string) { + return Array.from(value).some((character) => { + const charCode = character.charCodeAt(0); + return character === "\\" || charCode <= 31 || charCode === 127; + }); +} + +export function getSafeAuthRedirect(value: string | null | undefined) { + if (!value) return DEFAULT_AUTH_REDIRECT; + if ( + value !== value.trim() || + hasUnsafeAuthRedirectCharacter(value) || + !value.startsWith("/") || + value.startsWith("//") + ) { + return DEFAULT_AUTH_REDIRECT; + } + + try { + const url = new URL(value, AUTH_REDIRECT_ORIGIN); + if (url.origin !== AUTH_REDIRECT_ORIGIN) return DEFAULT_AUTH_REDIRECT; + + return `${url.pathname}${url.search}${url.hash}`; + } catch { + return DEFAULT_AUTH_REDIRECT; + } +} + +export function getTotpSecret(totpURI: string) { + try { + return new URL(totpURI).searchParams.get("secret") ?? ""; + } catch { + return ""; + } +} diff --git a/web/package.json b/web/package.json index d62fff4b..69ff99b6 100644 --- a/web/package.json +++ b/web/package.json @@ -17,12 +17,12 @@ "dependencies": { "@aws-sdk/client-s3": "^3.968.0", "@base-ui/react": "^1.0.0", - "@better-auth/api-key": "^1.6.19", + "@better-auth/api-key": "1.6.23", "@bprogress/next": "^3.2.12", "@react-email/components": "^1.0.4", "@react-email/render": "^2.0.2", "acme-client": "^5.4.0", - "better-auth": "^1.4.9", + "better-auth": "1.6.23", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cron-parser": "^5.4.0", @@ -37,6 +37,7 @@ "nodemailer": "^7.0.12", "nuqs": "^2.8.6", "pg": "^8.16.3", + "qrcode": "^1.5.4", "react": "19.2.7", "react-dom": "19.2.7", "recharts": "^3.8.1", @@ -56,6 +57,7 @@ "@types/node": "^24", "@types/nodemailer": "^7.0.5", "@types/pg": "^8.16.0", + "@types/qrcode": "^1.5.6", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@types/validator": "^13.15.10", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 00a9689c..37609859 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: specifier: ^1.0.0 version: 1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@better-auth/api-key': - specifier: ^1.6.19 - version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))))(better-call@1.3.6(zod@4.4.3)) + specifier: 1.6.23 + version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))))(better-call@1.3.7(zod@4.4.3)) '@bprogress/next': specifier: ^3.2.12 version: 3.2.12(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -34,8 +34,8 @@ importers: specifier: ^5.4.0 version: 5.4.0 better-auth: - specifier: ^1.4.9 - version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))) + specifier: 1.6.23 + version: 1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -78,6 +78,9 @@ importers: pg: specifier: ^8.16.3 version: 8.21.0 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 react: specifier: 19.2.7 version: 19.2.7 @@ -130,6 +133,9 @@ importers: '@types/pg': specifier: ^8.16.0 version: 8.20.0 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 '@types/react': specifier: 19.2.17 version: 19.2.17 @@ -434,22 +440,22 @@ packages: '@types/react': optional: true - '@better-auth/api-key@1.6.19': - resolution: {integrity: sha512-mSZhMu6tVZ6bxnlDnYL2bdxkEpfI3CeDW7l2tPSdZC12hupTowqgWsZvrWBcgc4t4arYuS2PoXxg7lVTetrgig==} + '@better-auth/api-key@1.6.23': + resolution: {integrity: sha512-HQMTv1GkY5FzE8BlUXdNhuKPBPUvusBYlAtIQElQjfsmGiPiBie7BdzuhcbXhAqJerunU299yvEge2WlZusp+Q==} peerDependencies: - '@better-auth/core': ^1.6.19 + '@better-auth/core': ^1.6.23 '@better-auth/utils': 0.4.2 - better-auth: ^1.6.19 - better-call: 1.3.6 + better-auth: ^1.6.23 + better-call: 1.3.7 - '@better-auth/core@1.6.19': - resolution: {integrity: sha512-ddE3Y9MoQ8t32QSO5Y8mV7pmnDAv5LdJjX1SPWUH6JUUmuOc7YEy3B5JfXZIJbRaXFdnAitN7pHPVa5u/dYAZA==} + '@better-auth/core@1.6.23': + resolution: {integrity: sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ==} 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.6 + better-call: 1.3.7 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 @@ -459,46 +465,46 @@ packages: '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.6.19': - resolution: {integrity: sha512-57C9ePorPmIEez6dHuQMz3hCTkYim0lfVRIoRtX7PiVfiRFB2bjXseQwrCJfQmkgMFlkp1s/c9nKgAjc2EvAIg==} + '@better-auth/drizzle-adapter@1.6.23': + resolution: {integrity: sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ==} peerDependencies: - '@better-auth/core': ^1.6.19 + '@better-auth/core': ^1.6.23 '@better-auth/utils': 0.4.2 drizzle-orm: ^0.45.2 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.6.19': - resolution: {integrity: sha512-DlmvllEd0nv8JL+plX3JB3WTmqDFnGFOmjmIiUDHo8R3PTAvC0ZaJq3Jk+LQLN5PyVQSUzXZKtvTQYaqRHzBaw==} + '@better-auth/kysely-adapter@1.6.23': + resolution: {integrity: sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg==} peerDependencies: - '@better-auth/core': ^1.6.19 + '@better-auth/core': ^1.6.23 '@better-auth/utils': 0.4.2 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.6.19': - resolution: {integrity: sha512-cZ8iLRG/T8Oi/CqE9FTHj3z8pIOqRsINi50trWxPNwyY/Eyb7YCljrBi0PuqgIdyVs7BWfrrtEYTpO4ddfuwEw==} + '@better-auth/memory-adapter@1.6.23': + resolution: {integrity: sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ==} peerDependencies: - '@better-auth/core': ^1.6.19 + '@better-auth/core': ^1.6.23 '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.6.19': - resolution: {integrity: sha512-8AReXqhMGiGQIPEpGbmAhh+R4g70TsAVvzwdd6Aj4q+LTSwd3tqC89TFJ4eX8KSplxm9PFBZ6g6gsRmDd7urQg==} + '@better-auth/mongo-adapter@1.6.23': + resolution: {integrity: sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA==} peerDependencies: - '@better-auth/core': ^1.6.19 + '@better-auth/core': ^1.6.23 '@better-auth/utils': 0.4.2 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.6.19': - resolution: {integrity: sha512-pXZBhR7/bzJb48IUHlGMyz9SM9h1OCO5GIIuHEllJYt8MKgrjtsnXfUkwZh6pAUEIp3WxBEYMMK96bfkqHiWEg==} + '@better-auth/prisma-adapter@1.6.23': + resolution: {integrity: sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==} peerDependencies: - '@better-auth/core': ^1.6.19 + '@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 @@ -508,10 +514,10 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.6.19': - resolution: {integrity: sha512-bBaB6SMIsrD3WutDdm5YQ1bQyinANTimHD8RtpLWhNh/jIXvzgwVVCrDFqA256vcGC/ZRCydmtW8ZrUqNMW9Og==} + '@better-auth/telemetry@1.6.23': + resolution: {integrity: sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA==} peerDependencies: - '@better-auth/core': ^1.6.19 + '@better-auth/core': ^1.6.23 '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 @@ -2602,6 +2608,9 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3008,8 +3017,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - better-auth@1.6.19: - resolution: {integrity: sha512-68eXWKj0sxa0xW4+n4tENd6Co94UCynPKe1fncmO6kIB3XhSXWgwDEpiUouJV2dmLBrHM1FPkoI6Q5597zCGpQ==} + better-auth@1.6.23: + resolution: {integrity: sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -3070,8 +3079,8 @@ packages: vue: optional: true - better-call@1.3.6: - resolution: {integrity: sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA==} + better-call@1.3.7: + resolution: {integrity: sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -3134,6 +3143,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} @@ -3176,6 +3189,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3360,6 +3376,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -3425,6 +3445,9 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -3903,6 +3926,10 @@ packages: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -4600,6 +4627,10 @@ packages: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -4937,6 +4968,10 @@ packages: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -5065,6 +5100,10 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -5148,6 +5187,11 @@ packages: resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} engines: {node: '>=16.0.0'} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -5234,6 +5278,9 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -5333,6 +5380,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} @@ -5847,6 +5897,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.22: resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} @@ -5870,6 +5923,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5893,6 +5950,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5905,10 +5965,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -6394,21 +6462,21 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@better-auth/api-key@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))))(better-call@1.3.6(zod@4.4.3))': + '@better-auth/api-key@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))))(better-call@1.3.7(zod@4.4.3))': dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 - better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))) - better-call: 1.3.6(zod@4.4.3) + better-auth: 1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))) + better-call: 1.3.7(zod@4.4.3) zod: 4.4.3 - '@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)': + '@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)': dependencies: '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 '@opentelemetry/semantic-conventions': 1.41.1 '@standard-schema/spec': 1.1.0 - better-call: 1.3.6(zod@4.4.3) + better-call: 1.3.7(zod@4.4.3) jose: 6.2.3 kysely: 0.29.2 nanostores: 1.3.0 @@ -6416,38 +6484,38 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))': + '@better-auth/drizzle-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))': dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 optionalDependencies: drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0) - '@better-auth/kysely-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2)': + '@better-auth/kysely-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2)': dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 optionalDependencies: kysely: 0.29.2 - '@better-auth/memory-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 - '@better-auth/prisma-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 - '@better-auth/telemetry@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + '@better-auth/telemetry@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 @@ -8425,6 +8493,10 @@ snapshots: pg-protocol: 1.14.0 pg-types: 2.2.0 + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.13.2 + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -8838,20 +8910,20 @@ snapshots: baseline-browser-mapping@2.10.37: {} - better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))): + better-auth@1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(msw@2.14.6(@types/node@24.13.2)(typescript@5.9.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))): dependencies: - '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/drizzle-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0)) - '@better-auth/kysely-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2) - '@better-auth/memory-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/drizzle-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0)) + '@better-auth/kysely-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2) + '@better-auth/memory-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 '@noble/ciphers': 2.2.0 '@noble/hashes': 2.2.0 - better-call: 1.3.6(zod@4.4.3) + better-call: 1.3.7(zod@4.4.3) defu: 6.1.7 jose: 6.2.3 kysely: 0.29.2 @@ -8869,7 +8941,7 @@ snapshots: - '@cloudflare/workers-types' - '@opentelemetry/api' - better-call@1.3.6(zod@4.4.3): + better-call@1.3.7(zod@4.4.3): dependencies: '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 @@ -8948,6 +9020,8 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + caniuse-lite@1.0.30001799: {} canonicalize@1.0.8: {} @@ -8979,6 +9053,12 @@ snapshots: client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -9140,6 +9220,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decimal.js-light@2.5.1: {} dedent@1.7.2: {} @@ -9183,6 +9265,8 @@ snapshots: diff@8.0.4: {} + dijkstrajs@1.0.3: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -9849,6 +9933,11 @@ snapshots: dependencies: locate-path: 3.0.0 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -10493,6 +10582,10 @@ snapshots: p-locate: 3.0.0 path-exists: 3.0.0 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -10811,6 +10904,10 @@ snapshots: dependencies: p-limit: 2.3.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -10915,6 +11012,8 @@ snapshots: dependencies: find-up: 3.0.0 + pngjs@5.0.0: {} + possible-typed-array-names@1.1.0: {} postcss-selector-parser@7.1.4: @@ -10996,6 +11095,12 @@ snapshots: pvutils@1.1.5: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.15.2: dependencies: side-channel: 1.1.1 @@ -11104,6 +11209,8 @@ snapshots: transitivePeerDependencies: - supports-color + require-main-filename@2.0.0: {} + reselect@5.1.1: {} reselect@5.2.0: {} @@ -11238,6 +11345,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + set-cookie-parser@3.1.0: {} set-function-length@1.2.2: @@ -11846,6 +11955,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.22: dependencies: available-typed-arrays: 1.0.7 @@ -11871,6 +11982,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -11894,14 +12011,35 @@ snapshots: xtend@4.0.2: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} yaml@2.9.0: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/web/tests/two-factor.test.ts b/web/tests/two-factor.test.ts new file mode 100644 index 00000000..2a25755f --- /dev/null +++ b/web/tests/two-factor.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { + DEFAULT_AUTH_REDIRECT, + getSafeAuthRedirect, + getTotpSecret, +} from "@/lib/two-factor"; + +describe("two-factor helpers", () => { + it("keeps internal auth redirects", () => { + expect(getSafeAuthRedirect("/dashboard/projects/demo")).toBe( + "/dashboard/projects/demo", + ); + }); + + it("falls back for missing or external auth redirects", () => { + expect(getSafeAuthRedirect(null)).toBe(DEFAULT_AUTH_REDIRECT); + expect(getSafeAuthRedirect("https://example.com")).toBe( + DEFAULT_AUTH_REDIRECT, + ); + expect(getSafeAuthRedirect("//example.com")).toBe(DEFAULT_AUTH_REDIRECT); + expect(getSafeAuthRedirect("/\\example.com")).toBe(DEFAULT_AUTH_REDIRECT); + expect(getSafeAuthRedirect("/\\/example.com")).toBe(DEFAULT_AUTH_REDIRECT); + expect(getSafeAuthRedirect(" /dashboard")).toBe(DEFAULT_AUTH_REDIRECT); + expect(getSafeAuthRedirect("/dashboard\n")).toBe(DEFAULT_AUTH_REDIRECT); + }); + + it("extracts the secret from a TOTP URI", () => { + expect( + getTotpSecret( + "otpauth://totp/Techulus%20Cloud:agent@example.com?secret=ABC123&issuer=Techulus%20Cloud", + ), + ).toBe("ABC123"); + }); + + it("returns an empty secret for malformed TOTP URIs", () => { + expect(getTotpSecret("not a uri")).toBe(""); + }); +});