{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "supabase-auth", "title": "Supabase Auth", "description": "Supabase email, password, and TOTP authentication module with session provider, route guards, sign-up, and authenticator enrollment screens.", "dependencies": [ "@supabase/supabase-js@^2.110.7", "@tanstack/react-query@^5.101.2", "react-router@^8.3.0" ], "registryDependencies": [ "askyourpolicy/ss-registry/stitch-base", "askyourpolicy/ss-registry/alert", "askyourpolicy/ss-registry/auth-shell", "askyourpolicy/ss-registry/button", "askyourpolicy/ss-registry/card", "askyourpolicy/ss-registry/collapsible", "askyourpolicy/ss-registry/copy-button", "askyourpolicy/ss-registry/input", "askyourpolicy/ss-registry/input-otp", "askyourpolicy/ss-registry/label", "askyourpolicy/ss-registry/spinner", "askyourpolicy/ss-registry/stitch-brand", "askyourpolicy/ss-registry/supabase-client" ], "files": [ { "path": "src/auth/auth.tsx", "content": "export { AuthPage, type AuthPageMode } from \"@/auth/auth-page\";\nexport { AuthProvider, useAuth } from \"@/auth/auth-provider\";\nexport { isStitchEmail } from \"@/auth/authorization\";\nexport { ProtectedRoute, StitchEmailGate, StitchOnlyRoute } from \"@/auth/guards\";\n", "type": "registry:component", "target": "src/auth/auth.tsx" }, { "path": "src/auth/auth-error.tsx", "content": "import { Alert, AlertDescription } from \"@/components/ui/alert\";\n\nexport function AuthError({ message }: { message: string }) {\n return (\n \n {message}\n \n );\n}\n", "type": "registry:component", "target": "src/auth/auth-error.tsx" }, { "path": "src/auth/auth-page.tsx", "content": "\"use client\";\n\nimport { type FormEvent, useState } from \"react\";\nimport { Link, Navigate, useLocation } from \"react-router\";\n\nimport { AuthError } from \"@/auth/auth-error\";\nimport { useAuth } from \"@/auth/auth-provider\";\nimport { FullPageSpinner } from \"@/auth/guards\";\nimport { SignUpForm } from \"@/auth/sign-up-form\";\nimport { TotpEnrollmentForm } from \"@/auth/totp-enrollment-form\";\nimport {\n AuthShell,\n AuthShellBrand,\n AuthShellCard,\n AuthShellContent,\n AuthShellFooter,\n AuthShellHeader,\n} from \"@/components/ui/auth-shell\";\nimport { Button } from \"@/components/ui/button\";\nimport { CardDescription } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { InputOTP, InputOTPGroup, InputOTPSlot } from \"@/components/ui/input-otp\";\nimport { Label } from \"@/components/ui/label\";\nimport { BrandLockup } from \"@/components/ui/stitch-brand\";\n\nexport type AuthPageMode = \"sign-in\" | \"sign-up\";\n\nexport function AuthPage({ appName, mode = \"sign-in\" }: { appName: string; mode?: AuthPageMode }) {\n const auth = useAuth();\n const location = useLocation();\n const from = getReturnPath(location.state);\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [code, setCode] = useState(\"\");\n const [error, setError] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n\n if (auth.loading) return ;\n if (auth.status === \"authenticated\") return ;\n\n async function submitCredentials(event: FormEvent) {\n event.preventDefault();\n setError(\"\");\n setSubmitting(true);\n try {\n const message = await auth.signIn(email.trim(), password);\n if (message) setError(message);\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"Sign in failed.\");\n } finally {\n setSubmitting(false);\n }\n }\n\n async function submitCode(event: FormEvent) {\n event.preventDefault();\n setError(\"\");\n setSubmitting(true);\n try {\n const message = await auth.verifyMfa(code);\n if (message) setError(message);\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"MFA verification failed.\");\n } finally {\n setSubmitting(false);\n }\n }\n\n async function signOut() {\n setError(\"\");\n setSubmitting(true);\n try {\n const message = await auth.signOut();\n if (message) setError(message);\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"Sign out failed.\");\n } finally {\n setSubmitting(false);\n }\n }\n\n const content =\n auth.status === \"mfa-required\" ? (\n void signOut()}\n onSubmit={submitCode}\n submitting={submitting}\n />\n ) : auth.status === \"mfa-setup-required\" ? (\n void signOut()}\n signingOut={submitting}\n />\n ) : auth.status === \"error\" ? (\n \n ) : mode === \"sign-up\" ? (\n \n ) : (\n \n );\n\n return (\n \n \n \n \n \n \n

{getTitle(auth.status, mode)}

\n {auth.status === \"error\" ? null : (\n {getDescription(auth.status, mode, appName)}\n )}\n
\n {content}\n
\n {auth.status === \"signed-out\" ? (\n \n ) : null}\n
\n );\n}\n\nfunction AuthModeSwitch({ mode, returnState }: { mode: AuthPageMode; returnState: unknown }) {\n const signingUp = mode === \"sign-up\";\n return (\n \n {signingUp ? \"Already have an account? \" : \"Need an account? \"}\n \n {signingUp ? \"Sign in\" : \"Create one\"}\n \n \n );\n}\n\nfunction CredentialsForm({\n email,\n error,\n onEmailChange,\n onPasswordChange,\n onSubmit,\n password,\n submitting,\n}: {\n email: string;\n error: string;\n onEmailChange: (value: string) => void;\n onPasswordChange: (value: string) => void;\n onSubmit: (event: FormEvent) => void;\n password: string;\n submitting: boolean;\n}) {\n return (\n
\n
\n \n onEmailChange(event.target.value)}\n required\n type=\"email\"\n value={email}\n />\n
\n
\n \n onPasswordChange(event.target.value)}\n required\n type=\"password\"\n value={password}\n />\n
\n {error ? : null}\n \n \n );\n}\n\nfunction MfaChallengeForm({\n code,\n error,\n onCodeChange,\n onSignOut,\n onSubmit,\n submitting,\n}: {\n code: string;\n error: string;\n onCodeChange: (value: string) => void;\n onSignOut: () => void;\n onSubmit: (event: FormEvent) => void;\n submitting: boolean;\n}) {\n return (\n
\n
\n \n
\n \n \n {Array.from({ length: 6 }, (_, index) => (\n \n ))}\n \n \n
\n
\n {error ? : null}\n
\n \n Start over\n \n \n
\n \n );\n}\n\nfunction getReturnPath(state: unknown) {\n if (!state || typeof state !== \"object\" || !(\"from\" in state)) return \"/\";\n const from = state.from;\n if (!from || typeof from !== \"object\" || !(\"pathname\" in from)) return \"/\";\n return typeof from.pathname === \"string\" && from.pathname.startsWith(\"/\") ? from.pathname : \"/\";\n}\n\nfunction getTitle(status: string, mode: AuthPageMode) {\n if (status === \"mfa-required\") return \"Verify your identity\";\n if (status === \"mfa-setup-required\") return \"Set up authentication\";\n if (status === \"error\") return \"Session verification failed\";\n return mode === \"sign-up\" ? \"Create your account\" : \"Sign in\";\n}\n\nfunction getDescription(status: string, mode: AuthPageMode, appName: string) {\n if (status === \"mfa-required\") return \"Enter the code from your authenticator.\";\n if (status === \"mfa-setup-required\") {\n return `${appName} requires a TOTP authenticator.`;\n }\n return mode === \"sign-up\"\n ? \"Use the email a Stitch administrator added to your organization.\"\n : \"Use your Stitch platform credentials.\";\n}\n", "type": "registry:component", "target": "src/auth/auth-page.tsx" }, { "path": "src/auth/auth-provider.tsx", "content": "\"use client\";\n\nimport type { Session, User } from \"@supabase/supabase-js\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport {\n type AuthState,\n type AuthStatus,\n challengeAndVerifyTotp,\n createAuthErrorState,\n describeAuthError,\n initialAuthState,\n resolveSessionState,\n type SignUpInput,\n type SignUpOutcome,\n toError,\n} from \"@/auth/auth-state\";\nimport { getSupabaseClient } from \"@/lib/supabase\";\n\ntype AuthContextValue = {\n confirmTotpEnrollment: (factorId: string, code: string) => Promise;\n error: Error | null;\n loading: boolean;\n mfaRequired: boolean;\n mfaSetupRequired: boolean;\n retry: () => Promise;\n session: Session | null;\n signIn: (email: string, password: string) => Promise;\n signOut: () => Promise;\n signUp: (input: SignUpInput) => Promise;\n status: AuthStatus;\n user: User | null;\n verifyMfa: (code: string) => Promise;\n};\n\nconst AuthContext = createContext(null);\n\n// A before-user-created hook rejects anyone who is not already a member of the organization, and\n// Supabase relays that as an empty error, so state the remedy instead of the missing detail.\nconst signUpRejected =\n \"This account could not be created. Your email has to be added to the organization first. Ask a Stitch administrator, then sign up again.\";\n\nexport function AuthProvider({ children }: { children: ReactNode }) {\n const queryClient = useQueryClient();\n const [state, setState] = useState(initialAuthState);\n const mounted = useRef(false);\n const mfaRequest = useRef | null>(null);\n const principal = useRef(undefined);\n const requestSequence = useRef(0);\n const stateRef = useRef(initialAuthState);\n\n const commit = useCallback((requestId: number, nextState: AuthState) => {\n if (mounted.current && requestId === requestSequence.current) {\n stateRef.current = nextState;\n setState(nextState);\n }\n }, []);\n\n const updatePrincipal = useCallback(\n (requestId: number, session: Session | null) => {\n if (requestId !== requestSequence.current) return;\n const nextPrincipal = session?.user.id ?? null;\n if (principal.current === undefined || principal.current !== nextPrincipal) {\n queryClient.clear();\n }\n principal.current = nextPrincipal;\n },\n [queryClient],\n );\n\n const inspectSession = useCallback(\n async (session: Session | null, requestId = ++requestSequence.current, showLoading = true) => {\n updatePrincipal(requestId, session);\n if (showLoading) {\n commit(requestId, {\n error: null,\n factorId: null,\n session,\n status: \"loading\",\n });\n }\n\n let nextState = createAuthErrorState(\n new Error(\"Your session could not be verified.\"),\n session,\n );\n try {\n nextState = await resolveSessionState(session);\n } catch (error) {\n nextState = createAuthErrorState(error, session);\n } finally {\n commit(requestId, nextState);\n }\n return nextState;\n },\n [commit, updatePrincipal],\n );\n\n const refreshSession = useCallback(async () => {\n const requestId = ++requestSequence.current;\n commit(requestId, {\n error: null,\n factorId: null,\n session: null,\n status: \"loading\",\n });\n\n let nextState: AuthState | null = null;\n try {\n const { data, error } = await getSupabaseClient().auth.getSession();\n if (error) {\n throw new Error(`Your session could not be loaded: ${error.message}`, { cause: error });\n }\n nextState = await inspectSession(data.session, requestId);\n } catch (error) {\n nextState = createAuthErrorState(error, null);\n } finally {\n if (nextState) commit(requestId, nextState);\n }\n }, [commit, inspectSession]);\n\n useEffect(() => {\n mounted.current = true;\n let unsubscribe: () => void = () => undefined;\n\n try {\n const {\n data: { subscription },\n } = getSupabaseClient().auth.onAuthStateChange((event, nextSession) => {\n globalThis.setTimeout(() => {\n if (!mounted.current) return;\n const current = stateRef.current;\n const isSamePrincipalRefresh =\n nextSession !== null &&\n current.status !== \"loading\" &&\n current.session?.user.id === nextSession.user.id &&\n (event === \"SIGNED_IN\" || event === \"TOKEN_REFRESHED\");\n void inspectSession(nextSession, undefined, !isSamePrincipalRefresh);\n }, 0);\n });\n unsubscribe = () => subscription.unsubscribe();\n } catch (error) {\n const requestId = ++requestSequence.current;\n commit(requestId, createAuthErrorState(error, null));\n }\n\n void refreshSession();\n return () => {\n mounted.current = false;\n requestSequence.current += 1;\n unsubscribe();\n };\n }, [commit, inspectSession, refreshSession]);\n\n const signIn = useCallback(\n async (email: string, password: string) => {\n const requestId = ++requestSequence.current;\n commit(requestId, {\n error: null,\n factorId: null,\n session: null,\n status: \"loading\",\n });\n\n let nextState: AuthState | null = null;\n let message: string | null = null;\n try {\n const { data, error } = await getSupabaseClient().auth.signInWithPassword({\n email,\n password,\n });\n if (error) {\n message = describeAuthError(error.message);\n nextState = {\n error: null,\n factorId: null,\n session: null,\n status: \"signed-out\",\n };\n } else {\n nextState = await inspectSession(data.session, requestId);\n if (nextState.status === \"error\") message = nextState.error?.message ?? \"Sign in failed.\";\n }\n } catch (error) {\n message = toError(error, \"Sign in failed.\").message;\n nextState = {\n error: null,\n factorId: null,\n session: null,\n status: \"signed-out\",\n };\n } finally {\n if (nextState) commit(requestId, nextState);\n }\n return message;\n },\n [commit, inspectSession],\n );\n\n // A failed sign up leaves the signed-out state untouched so the form keeps what was typed.\n const signUp = useCallback(\n async ({ email, firstName, lastName, password }: SignUpInput): Promise => {\n try {\n const { data, error } = await getSupabaseClient().auth.signUp({\n email,\n options: {\n data: { first_name: firstName, has_password: true, last_name: lastName },\n },\n password,\n });\n if (error) {\n return { message: describeAuthError(error.message, signUpRejected), status: \"error\" };\n }\n // Supabase withholds the session until the address is confirmed.\n if (!data.session) return { message: null, status: \"confirmation-required\" };\n\n const nextState = await inspectSession(data.session, ++requestSequence.current);\n return nextState.status === \"error\"\n ? { message: nextState.error?.message ?? \"Sign up failed.\", status: \"error\" }\n : { message: null, status: \"signed-up\" };\n } catch (error) {\n return { message: toError(error, \"Sign up failed.\").message, status: \"error\" };\n }\n },\n [inspectSession],\n );\n\n const signOut = useCallback(async () => {\n const currentSession = state.session;\n const requestId = ++requestSequence.current;\n commit(requestId, {\n error: null,\n factorId: null,\n session: currentSession,\n status: \"loading\",\n });\n\n let nextState: AuthState | null = null;\n let message: string | null = null;\n try {\n const { error } = await getSupabaseClient().auth.signOut();\n if (error) {\n throw new Error(`Sign out failed: ${error.message}`, { cause: error });\n }\n updatePrincipal(requestId, null);\n queryClient.clear();\n nextState = await inspectSession(null, requestId);\n } catch (error) {\n const normalized = toError(error, \"Sign out failed.\");\n message = normalized.message;\n nextState = createAuthErrorState(normalized, currentSession);\n } finally {\n if (nextState) commit(requestId, nextState);\n }\n return message;\n }, [commit, inspectSession, queryClient, state.session, updatePrincipal]);\n\n const reloadVerifiedSession = useCallback(\n async (requestId: number) => {\n const { data, error } = await getSupabaseClient().auth.getSession();\n if (error) {\n throw new Error(`Your verified session could not be loaded: ${error.message}`, {\n cause: error,\n });\n }\n return inspectSession(data.session, requestId);\n },\n [inspectSession],\n );\n\n // Enrollment verification keeps the caller's state so a rejected code does not discard the\n // in-progress QR code, which Supabase cannot reissue for the same factor.\n const confirmTotpEnrollment = useCallback(\n async (factorId: string, code: string) => {\n const currentSession = stateRef.current.session;\n try {\n const verificationMessage = await challengeAndVerifyTotp(factorId, code);\n if (verificationMessage) return verificationMessage;\n\n const nextState = await reloadVerifiedSession(++requestSequence.current);\n return nextState.status === \"error\"\n ? (nextState.error?.message ?? \"Authenticator verification failed.\")\n : null;\n } catch (error) {\n const normalized = toError(error, \"Authenticator verification failed.\");\n commit(++requestSequence.current, createAuthErrorState(normalized, currentSession));\n return normalized.message;\n }\n },\n [commit, reloadVerifiedSession],\n );\n\n const verifyMfa = useCallback(\n (code: string) => {\n if (state.status !== \"mfa-required\" || !state.factorId || !state.session) {\n return Promise.resolve(\"No verified authenticator is available.\");\n }\n if (mfaRequest.current) return mfaRequest.current;\n const factorId = state.factorId;\n\n const request = (async () => {\n const previousState = state;\n const requestId = ++requestSequence.current;\n commit(requestId, { ...state, status: \"loading\" });\n\n let nextState: AuthState = previousState;\n let message: string | null = null;\n try {\n message = await challengeAndVerifyTotp(factorId, code);\n if (!message) {\n nextState = await reloadVerifiedSession(requestId);\n if (nextState.status === \"error\") {\n message = nextState.error?.message ?? \"MFA verification failed.\";\n }\n }\n } catch (error) {\n const normalized = toError(error, \"MFA verification failed.\");\n message = normalized.message;\n nextState = createAuthErrorState(normalized, previousState.session);\n } finally {\n commit(requestId, nextState);\n }\n return message;\n })();\n const trackedRequest = request.finally(() => {\n if (mfaRequest.current === trackedRequest) mfaRequest.current = null;\n });\n mfaRequest.current = trackedRequest;\n return trackedRequest;\n },\n [commit, reloadVerifiedSession, state],\n );\n\n const value = useMemo(\n () => ({\n confirmTotpEnrollment,\n error: state.error,\n loading: state.status === \"loading\",\n mfaRequired: state.status === \"mfa-required\",\n mfaSetupRequired: state.status === \"mfa-setup-required\",\n retry: refreshSession,\n session: state.session,\n signIn,\n signOut,\n signUp,\n status: state.status,\n user: state.session?.user ?? null,\n verifyMfa,\n }),\n [confirmTotpEnrollment, refreshSession, signIn, signOut, signUp, state, verifyMfa],\n );\n\n return {children};\n}\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (!context) throw new Error(\"useAuth must be used within AuthProvider\");\n return context;\n}\n", "type": "registry:component", "target": "src/auth/auth-provider.tsx" }, { "path": "src/auth/auth-state.ts", "content": "import type { Session } from \"@supabase/supabase-js\";\n\nimport { getSupabaseClient } from \"@/lib/supabase\";\n\nexport type AuthStatus =\n | \"authenticated\"\n | \"error\"\n | \"loading\"\n | \"mfa-required\"\n | \"mfa-setup-required\"\n | \"signed-out\";\n\nexport type AuthState = {\n error: Error | null;\n factorId: string | null;\n session: Session | null;\n status: AuthStatus;\n};\n\nexport type SignUpInput = {\n email: string;\n firstName: string;\n lastName: string;\n password: string;\n};\n\nexport type SignUpOutcome =\n | { message: null; status: \"confirmation-required\" }\n | { message: null; status: \"signed-up\" }\n | { message: string; status: \"error\" };\n\nexport type TotpEnrollment = {\n factorId: string;\n qrCode: string;\n secret: string;\n};\n\nexport const initialAuthState: AuthState = {\n error: null,\n factorId: null,\n session: null,\n status: \"loading\",\n};\n\nexport function createAuthErrorState(error: unknown, session: Session | null): AuthState {\n return {\n error: toError(error, \"Your session could not be verified.\"),\n factorId: null,\n session,\n status: \"error\",\n };\n}\n\nexport async function resolveSessionState(session: Session | null): Promise {\n if (!session) {\n return {\n error: null,\n factorId: null,\n session: null,\n status: \"signed-out\",\n };\n }\n\n const { data: factors, error: factorsError } = await getSupabaseClient().auth.mfa.listFactors();\n if (factorsError) {\n throw new Error(`Authenticator factors could not be checked: ${factorsError.message}`, {\n cause: factorsError,\n });\n }\n\n const verifiedTotp = factors.totp\n .filter((factor) => factor.status === \"verified\")\n .sort((first, second) => first.id.localeCompare(second.id))[0];\n if (!verifiedTotp) {\n return {\n error: null,\n factorId: null,\n session,\n status: \"mfa-setup-required\",\n };\n }\n\n const { data: assurance, error: assuranceError } =\n await getSupabaseClient().auth.mfa.getAuthenticatorAssuranceLevel();\n if (assuranceError) {\n throw new Error(`Authenticator assurance could not be checked: ${assuranceError.message}`, {\n cause: assuranceError,\n });\n }\n\n if (assurance.currentLevel === \"aal2\") {\n return {\n error: null,\n factorId: null,\n session,\n status: \"authenticated\",\n };\n }\n\n if (assurance.currentLevel === \"aal1\" && assurance.nextLevel === \"aal2\") {\n return {\n error: null,\n factorId: verifiedTotp.id,\n session,\n status: \"mfa-required\",\n };\n }\n\n throw new Error(\"The session did not provide the required MFA assurance level.\");\n}\n\nexport async function startTotpEnrollment(friendlyName: string): Promise {\n const { data: factors, error: factorsError } = await getSupabaseClient().auth.mfa.listFactors();\n if (factorsError) {\n throw new Error(\n `Authenticator factors could not be checked: ${describeAuthError(factorsError.message)}`,\n { cause: factorsError },\n );\n }\n\n // Supabase refuses a new enrollment while abandoned unverified factors remain on the account.\n const unverified = factors.all.filter((factor) => factor.status === \"unverified\");\n await Promise.all(\n unverified.map(async (factor) => {\n const { error } = await getSupabaseClient().auth.mfa.unenroll({ factorId: factor.id });\n if (error) {\n throw new Error(\n `A previous authenticator could not be removed: ${describeAuthError(error.message)}`,\n { cause: error },\n );\n }\n }),\n );\n\n const { data, error } = await getSupabaseClient().auth.mfa.enroll({\n factorType: \"totp\",\n friendlyName,\n });\n if (error) {\n throw new Error(`Authenticator enrollment failed: ${describeAuthError(error.message)}`, {\n cause: error,\n });\n }\n\n return { factorId: data.id, qrCode: data.totp.qr_code, secret: data.totp.secret };\n}\n\nexport async function challengeAndVerifyTotp(factorId: string, code: string) {\n const { data: challenge, error: challengeError } = await getSupabaseClient().auth.mfa.challenge({\n factorId,\n });\n if (challengeError) return describeAuthError(challengeError.message);\n\n const { error: verifyError } = await getSupabaseClient().auth.mfa.verify({\n challengeId: challenge.id,\n code,\n factorId,\n });\n return verifyError ? describeAuthError(verifyError.message) : null;\n}\n\n// Supabase reports 5xx replies by stringifying the whole Response object, so the server's real\n// message is lost and `error.message` arrives as \"{}\". Never show that to an operator.\nconst opaqueAuthFailure =\n \"The authentication service rejected the request without explaining why. Try again, or contact a Stitch administrator if it keeps happening.\";\n\nexport function describeAuthError(\n message: string | null | undefined,\n fallback = opaqueAuthFailure,\n) {\n const described = message?.trim();\n return !described || described === \"{}\" ? fallback : described;\n}\n\nexport function toError(error: unknown, fallback: string) {\n return error instanceof Error ? error : new Error(fallback, { cause: error });\n}\n", "type": "registry:component", "target": "src/auth/auth-state.ts" }, { "path": "src/auth/authorization.ts", "content": "export function isStitchEmail(email: string | null | undefined) {\n return email?.trim().toLowerCase().endsWith(\"@stitchstudio.ai\") ?? false;\n}\n", "type": "registry:component", "target": "src/auth/authorization.ts" }, { "path": "src/auth/guards.tsx", "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Navigate, Outlet, useLocation } from \"react-router\";\n\nimport { isStitchEmail } from \"@/auth/authorization\";\nimport { useAuth } from \"@/auth/auth-provider\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\nexport function ProtectedRoute() {\n const auth = useAuth();\n const location = useLocation();\n\n if (auth.loading) return ;\n if (auth.status !== \"authenticated\") {\n return ;\n }\n return ;\n}\n\nexport function StitchOnlyRoute() {\n const auth = useAuth();\n return (\n \n \n \n );\n}\n\nexport function StitchEmailGate({\n children,\n email,\n}: {\n children: ReactNode;\n email: string | null | undefined;\n}) {\n return isStitchEmail(email) ? children : ;\n}\n\nexport function FullPageSpinner({ label }: { label: string }) {\n return (\n
\n \n \n {label}\n
\n \n );\n}\n", "type": "registry:component", "target": "src/auth/guards.tsx" }, { "path": "src/auth/sign-up-form.tsx", "content": "\"use client\";\n\nimport { type FormEvent, useState } from \"react\";\n\nimport { AuthError } from \"@/auth/auth-error\";\nimport { useAuth } from \"@/auth/auth-provider\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\n\nconst minimumPasswordLength = 8;\n\nexport function SignUpForm() {\n const auth = useAuth();\n const [confirmationEmail, setConfirmationEmail] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [error, setError] = useState(\"\");\n const [firstName, setFirstName] = useState(\"\");\n const [lastName, setLastName] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n\n async function submit(event: FormEvent) {\n event.preventDefault();\n setError(\"\");\n\n const trimmedFirstName = firstName.trim();\n const trimmedLastName = lastName.trim();\n if (!trimmedFirstName || !trimmedLastName) {\n setError(\"Enter your first and last name.\");\n return;\n }\n if (password.length < minimumPasswordLength) {\n setError(`Password must be at least ${minimumPasswordLength} characters.`);\n return;\n }\n\n const trimmedEmail = email.trim();\n setSubmitting(true);\n try {\n const outcome = await auth.signUp({\n email: trimmedEmail,\n firstName: trimmedFirstName,\n lastName: trimmedLastName,\n password,\n });\n if (outcome.status === \"error\") setError(outcome.message);\n if (outcome.status === \"confirmation-required\") setConfirmationEmail(trimmedEmail);\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"Sign up failed.\");\n } finally {\n setSubmitting(false);\n }\n }\n\n if (confirmationEmail) {\n return (\n
\n \n Confirm your email\n \n We sent a confirmation link to {confirmationEmail}. Open it, then sign in to set up your\n authenticator.\n \n \n
\n );\n }\n\n return (\n
\n
\n
\n \n setFirstName(event.target.value)}\n required\n value={firstName}\n />\n
\n
\n \n setLastName(event.target.value)}\n required\n value={lastName}\n />\n
\n
\n
\n \n setEmail(event.target.value)}\n required\n type=\"email\"\n value={email}\n />\n
\n
\n \n setPassword(event.target.value)}\n required\n type=\"password\"\n value={password}\n />\n

\n At least {minimumPasswordLength} characters.\n

\n
\n {error ? : null}\n \n \n );\n}\n", "type": "registry:component", "target": "src/auth/sign-up-form.tsx" }, { "path": "src/auth/totp-enrollment-form.tsx", "content": "\"use client\";\n\nimport { CaretDownIcon, CaretRightIcon } from \"@phosphor-icons/react\";\nimport { type FormEvent, useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { AuthError } from \"@/auth/auth-error\";\nimport { useAuth } from \"@/auth/auth-provider\";\nimport { startTotpEnrollment, type TotpEnrollment } from \"@/auth/auth-state\";\nimport { Button } from \"@/components/ui/button\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\nimport { InputOTP, InputOTPGroup, InputOTPSlot } from \"@/components/ui/input-otp\";\nimport { Label } from \"@/components/ui/label\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\nexport function TotpEnrollmentForm({\n appName,\n onSignOut,\n signingOut,\n}: {\n appName: string;\n onSignOut: () => void;\n signingOut: boolean;\n}) {\n const auth = useAuth();\n const [code, setCode] = useState(\"\");\n const [enrolling, setEnrolling] = useState(true);\n const [enrollment, setEnrollment] = useState(null);\n const [error, setError] = useState(\"\");\n const [secretVisible, setSecretVisible] = useState(false);\n const [submitting, setSubmitting] = useState(false);\n const requested = useRef(false);\n\n const beginEnrollment = useCallback(async () => {\n setEnrolling(true);\n setError(\"\");\n try {\n setEnrollment(await startTotpEnrollment(appName));\n } catch (cause) {\n setEnrollment(null);\n setError(cause instanceof Error ? cause.message : \"Authenticator setup failed.\");\n } finally {\n setEnrolling(false);\n }\n }, [appName]);\n\n // Enrolling twice would strand the secret the operator already scanned.\n useEffect(() => {\n if (requested.current) return;\n requested.current = true;\n void beginEnrollment();\n }, [beginEnrollment]);\n\n async function submit(event: FormEvent) {\n event.preventDefault();\n if (!enrollment) return;\n setError(\"\");\n setSubmitting(true);\n try {\n const message = await auth.confirmTotpEnrollment(enrollment.factorId, code);\n if (message) {\n setError(message);\n setCode(\"\");\n }\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"Authenticator verification failed.\");\n } finally {\n setSubmitting(false);\n }\n }\n\n if (enrolling) {\n return (\n \n \n Preparing your authenticator\n \n );\n }\n\n if (!enrollment) {\n return (\n
\n \n
\n \n {signingOut ? \"Signing out…\" : \"Sign out\"}\n \n \n
\n
\n );\n }\n\n return (\n
\n
\n
\n \"Authenticator\n
\n \n
\n }>\n {secretVisible ? : }\n Can’t scan? Enter this key instead.\n \n
\n \n \n {enrollment.secret}\n \n \n \n
\n
\n \n
\n \n \n {Array.from({ length: 6 }, (_, index) => (\n \n ))}\n \n \n
\n
\n {error ? : null}\n
\n \n Sign out\n \n \n
\n \n );\n}\n", "type": "registry:component", "target": "src/auth/totp-enrollment-form.tsx" } ], "docs": "Wrap the application in QueryClientProvider and AuthProvider. Route /auth to , /auth/sign-up to , and nest authenticated routes under . Supabase must have TOTP multi-factor enabled, and StitchEmailGate restricts routes to @stitchstudio.ai accounts.", "type": "registry:block" }