@reblu/site-next 1.5.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth/login-form.tsx","../src/auth/safe-redirect.ts","../src/auth/auth-callback.tsx","../src/auth/login-errors.ts","../src/auth/auth-logo.tsx","../src/auth/reset-password-request.tsx","../src/auth/reset-password-confirm.tsx","../src/auth/set-password.tsx","../src/auth/auth-confirm.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@reblu/site-ui/tabs'\nimport { isSafeRedirectUrl } from './safe-redirect'\n\nexport interface LoginFormCopy {\n // Tab labels\n magicLinkTabLabel: string\n passwordTabLabel: string\n\n // Shared\n emailLabel: string\n emailPlaceholder: string\n\n // Password tab\n passwordLabel: string\n passwordPlaceholder: string\n passwordSubmitCta: string\n forgotPasswordCta: string\n\n // Magic-link tab\n magicLinkSubmitCta: string\n\n // Magic-link success\n magicLinkSuccessTitle: string\n magicLinkSuccessMessage: string\n\n // MFA prompt\n mfaTitle: string\n mfaMessage: string\n mfaCodeLabel: string\n mfaCodePlaceholder: string\n mfaSubmitCta: string\n\n // Errors\n invalidEmailError: string\n networkError: string\n invalidCredentialsError: string\n mfaInvalidCodeError: string\n}\n\nexport interface LoginFormProps {\n // Endpoint URLs (all required)\n passwordLoginUrl: string\n mfaVerifyUrl: string\n magicLinkRequestUrl: string\n resetPasswordUrl: string\n\n // UX\n copy?: Partial<LoginFormCopy>\n className?: string\n defaultMethod?: 'magic-link' | 'password'\n\n // Hooks\n onPasswordSubmitted?: (email: string) => void\n onMagicLinkSubmitted?: (email: string) => void\n onMfaSubmitted?: () => void\n}\n\nconst DEFAULT_COPY: LoginFormCopy = {\n magicLinkTabLabel: 'Enlace mágico',\n passwordTabLabel: 'Contraseña',\n emailLabel: 'Correo electrónico',\n emailPlaceholder: 'tu@email.com',\n passwordLabel: 'Contraseña',\n passwordPlaceholder: '••••••••',\n passwordSubmitCta: 'Iniciar sesión',\n forgotPasswordCta: '¿Olvidaste tu contraseña?',\n magicLinkSubmitCta: 'Enviar enlace de acceso',\n magicLinkSuccessTitle: '¡Enlace enviado!',\n magicLinkSuccessMessage: 'Revisa tu correo para acceder.',\n mfaTitle: 'Verificación en dos pasos',\n mfaMessage: 'Introduce el código de 6 dígitos de tu aplicación autenticadora.',\n mfaCodeLabel: 'Código de verificación',\n mfaCodePlaceholder: '123456',\n mfaSubmitCta: 'Verificar código',\n invalidEmailError: 'Introduce un correo electrónico válido.',\n networkError: 'No se pudo enviar el enlace. Inténtalo de nuevo.',\n invalidCredentialsError: 'Email o contraseña incorrectos',\n mfaInvalidCodeError: 'Código incorrecto. Por favor, inténtalo de nuevo.',\n}\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isValidEmail(value: string): boolean {\n return EMAIL_REGEX.test(value)\n}\n\ntype FormState = 'idle' | 'submitting' | 'magic-link-success' | 'mfa-required' | 'error'\n\ninterface MfaState {\n factorId: string\n accessToken: string\n refreshToken: string\n}\n\ninterface PasswordLoginResponse {\n data?: {\n mfaRequired: boolean\n sessionExchangeUrl?: string\n factorId?: string\n accessToken?: string\n refreshToken?: string\n }\n error?: string\n}\n\ninterface MfaVerifyResponse {\n data?: { sessionExchangeUrl: string }\n error?: string\n}\n\nexport function LoginForm(props: LoginFormProps) {\n const {\n passwordLoginUrl,\n mfaVerifyUrl,\n magicLinkRequestUrl,\n resetPasswordUrl,\n copy,\n className,\n defaultMethod = 'password',\n onPasswordSubmitted,\n onMagicLinkSubmitted,\n onMfaSubmitted,\n } = props\n\n const c = { ...DEFAULT_COPY, ...copy }\n\n const [method, setMethod] = React.useState<'magic-link' | 'password'>(defaultMethod)\n const [state, setState] = React.useState<FormState>('idle')\n const [error, setError] = React.useState<string | null>(null)\n\n // Password & magic-link inputs\n const [email, setEmail] = React.useState('')\n const [password, setPassword] = React.useState('')\n\n // MFA state\n const [mfa, setMfa] = React.useState<MfaState | null>(null)\n const [mfaCode, setMfaCode] = React.useState('')\n\n const resetTransientState = React.useCallback(() => {\n setState('idle')\n setError(null)\n setMfa(null)\n setMfaCode('')\n }, [])\n\n const handleTabChange = (next: string) => {\n if (next !== 'magic-link' && next !== 'password') return\n setMethod(next)\n resetTransientState()\n }\n\n const handleMagicLinkSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n\n const trimmed = email.trim().toLowerCase()\n if (!isValidEmail(trimmed)) {\n setError(c.invalidEmailError)\n setState('error')\n return\n }\n\n setState('submitting')\n try {\n const res = await fetch(magicLinkRequestUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: trimmed }),\n })\n\n if (res.ok) {\n setState('magic-link-success')\n onMagicLinkSubmitted?.(trimmed)\n return\n }\n\n setState('error')\n setError(c.networkError)\n } catch {\n setState('error')\n setError(c.networkError)\n }\n }\n\n const handlePasswordSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n\n const trimmed = email.trim().toLowerCase()\n if (!isValidEmail(trimmed)) {\n setError(c.invalidEmailError)\n setState('error')\n return\n }\n if (!password) {\n setError(c.invalidCredentialsError)\n setState('error')\n return\n }\n\n setState('submitting')\n try {\n const res = await fetch(passwordLoginUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: trimmed, password }),\n })\n\n const json = (await res.json().catch(() => ({}))) as PasswordLoginResponse\n\n if (!res.ok || !json.data) {\n setState('error')\n setError(json.error ?? c.invalidCredentialsError)\n return\n }\n\n onPasswordSubmitted?.(trimmed)\n\n if (json.data.mfaRequired) {\n setMfa({\n factorId: json.data.factorId ?? '',\n accessToken: json.data.accessToken ?? '',\n refreshToken: json.data.refreshToken ?? '',\n })\n setState('mfa-required')\n return\n }\n\n const exchangeUrl = json.data.sessionExchangeUrl\n if (!exchangeUrl || !isSafeRedirectUrl(exchangeUrl)) {\n setState('error')\n setError(c.networkError)\n return\n }\n window.location.assign(exchangeUrl)\n } catch {\n setState('error')\n setError(c.networkError)\n }\n }\n\n const handleMfaSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n if (!mfa) return\n setError(null)\n setState('submitting')\n\n try {\n const res = await fetch(mfaVerifyUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n factorId: mfa.factorId,\n code: mfaCode,\n accessToken: mfa.accessToken,\n refreshToken: mfa.refreshToken,\n }),\n })\n\n const json = (await res.json().catch(() => ({}))) as MfaVerifyResponse\n\n if (\n !res.ok ||\n !json.data?.sessionExchangeUrl ||\n !isSafeRedirectUrl(json.data.sessionExchangeUrl)\n ) {\n // Stay in mfa-required state so user can retry the code\n setState('mfa-required')\n setError(json.error ?? c.mfaInvalidCodeError)\n setMfaCode('')\n return\n }\n\n onMfaSubmitted?.()\n window.location.assign(json.data.sessionExchangeUrl)\n } catch {\n setState('mfa-required')\n setError(c.networkError)\n }\n }\n\n // MFA prompt replaces the form entirely\n if (state === 'mfa-required' || (mfa && state === 'submitting')) {\n return (\n <div className={cn('space-y-6', className)}>\n <div className='space-y-1'>\n <h2 className='text-xl font-semibold'>{c.mfaTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.mfaMessage}</p>\n </div>\n <form onSubmit={(e) => void handleMfaSubmit(e)} className='space-y-4' noValidate>\n <div className='space-y-2'>\n <Label htmlFor='login-mfa-code'>{c.mfaCodeLabel}</Label>\n <Input\n id='login-mfa-code'\n type='text'\n inputMode='numeric'\n pattern='\\d{6}'\n maxLength={6}\n autoComplete='one-time-code'\n placeholder={c.mfaCodePlaceholder}\n value={mfaCode}\n onChange={(ev) => setMfaCode(ev.target.value.replace(/\\D/g, '').slice(0, 6))}\n disabled={state === 'submitting'}\n aria-describedby={error ? 'login-mfa-error' : undefined}\n autoFocus\n />\n {error && (\n <p id='login-mfa-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button\n type='submit'\n className='w-full'\n disabled={state === 'submitting' || mfaCode.length !== 6}\n >\n {c.mfaSubmitCta}\n </Button>\n <button\n type='button'\n onClick={resetTransientState}\n className='text-muted-foreground hover:text-foreground w-full text-sm transition-colors'\n disabled={state === 'submitting'}\n >\n ← Volver\n </button>\n </form>\n </div>\n )\n }\n\n // Magic-link success replaces the form\n if (state === 'magic-link-success') {\n return (\n <div className={cn('space-y-2 text-center', className)}>\n <h2 className='text-xl font-semibold'>{c.magicLinkSuccessTitle}</h2>\n <p className='text-muted-foreground'>{c.magicLinkSuccessMessage}</p>\n </div>\n )\n }\n\n return (\n <div className={cn('space-y-6', className)}>\n <Tabs value={method} onValueChange={handleTabChange}>\n <TabsList className='grid w-full grid-cols-2'>\n <TabsTrigger value='password'>{c.passwordTabLabel}</TabsTrigger>\n <TabsTrigger value='magic-link'>{c.magicLinkTabLabel}</TabsTrigger>\n </TabsList>\n\n <TabsContent value='password' className='pt-4'>\n <form onSubmit={(e) => void handlePasswordSubmit(e)} className='space-y-4' noValidate>\n <div className='space-y-2'>\n <Label htmlFor='login-email-pwd'>{c.emailLabel}</Label>\n <Input\n id='login-email-pwd'\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(ev) => setEmail(ev.target.value)}\n disabled={state === 'submitting'}\n />\n </div>\n <div className='space-y-2'>\n <div className='flex items-center justify-between'>\n <Label htmlFor='login-password'>{c.passwordLabel}</Label>\n <a\n href={resetPasswordUrl}\n className='text-muted-foreground hover:text-foreground text-xs transition-colors'\n >\n {c.forgotPasswordCta}\n </a>\n </div>\n <Input\n id='login-password'\n type='password'\n autoComplete='current-password'\n placeholder={c.passwordPlaceholder}\n value={password}\n onChange={(ev) => setPassword(ev.target.value)}\n disabled={state === 'submitting'}\n aria-describedby={error ? 'login-pwd-error' : undefined}\n />\n {error && (\n <p id='login-pwd-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button type='submit' className='w-full' disabled={state === 'submitting'}>\n {c.passwordSubmitCta}\n </Button>\n </form>\n </TabsContent>\n\n <TabsContent value='magic-link' className='pt-4'>\n <form onSubmit={(e) => void handleMagicLinkSubmit(e)} className='space-y-4' noValidate>\n <div className='space-y-2'>\n <Label htmlFor='login-email-magic'>{c.emailLabel}</Label>\n <Input\n id='login-email-magic'\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(ev) => setEmail(ev.target.value)}\n disabled={state === 'submitting'}\n aria-describedby={error ? 'login-magic-error' : undefined}\n />\n {error && (\n <p id='login-magic-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button type='submit' className='w-full' disabled={state === 'submitting'}>\n {c.magicLinkSubmitCta}\n </Button>\n </form>\n </TabsContent>\n </Tabs>\n </div>\n )\n}\n","/**\n * Scheme + URL-trick allowlist for a server-returned redirect target\n * (`sessionExchangeUrl`, `redirectTo`, etc.). Shared by `AuthCallback`,\n * `AuthConfirm`, and `SetPassword` — see `CLAUDE.md` Regla A: a predicate\n * applied more than 3 times gets one home, not N inline copies that drift.\n *\n * IMPORTANT — this is NOT full open-redirect confinement: it does NOT\n * restrict absolute `http`/`https` URLs to a specific origin.\n * `isSafeRedirectUrl('https://evil.com')` returns `true`. What it DOES\n * reject:\n * - dangerous schemes (`javascript:`, `data:`, `vbscript:`, ...) — the\n * classic XSS-via-redirect vector\n * - protocol-relative URLs (`//evil.com`) — these start with `/` but\n * browsers resolve them as an absolute, CROSS-ORIGIN URL (the scheme is\n * inherited from the current page), so a naive \"starts with /\" check\n * would wrongly treat them as same-origin\n * - backslash-authority URLs (`/\\evil.com`, `\\\\evil.com`) — browsers\n * normalize `\\` to `/` for special schemes, so these resolve the same way\n * - the same tricks with an ASCII tab/LF/CR spliced between the slashes\n * (`/\\t/evil.com`) — the WHATWG URL parser strips ALL tab/LF/CR from the\n * input BEFORE parsing (anywhere in the string, not just leading/trailing),\n * so `/\\t/evil.com` is indistinguishable from `//evil.com` to the browser\n * even though a naive regex over the raw string wouldn't see two adjacent\n * slashes\n *\n * Full origin-pinning (rejecting any absolute URL whose origin isn't the\n * actual expected one) is a SEPARATE, not-yet-implemented layer. It can't\n * simply pin to `REBLU_API_BASE_URL`: that's a server-only env var\n * unavailable in these `'use client'` components, and per REB-218's\n * subdomain redirect architecture it would be the WRONG origin anyway\n * (production session-exchange lives at `nexus.<tenantDomain>`, not the\n * SITE's own `REBLU_API_BASE_URL`). It would need a dedicated prop\n * conveying the actual expected origin per deploy/routing mode.\n */\nexport function isSafeRedirectUrl(url: string): boolean {\n if (!url) return false\n\n // Match the WHATWG URL parser's own first step: strip ALL ASCII\n // tab/LF/CR from the input, anywhere in the string — not just\n // leading/trailing — before evaluating what the browser will actually\n // resolve. Without this, '/\\t/evil.com' looks like a single-'/' relative\n // path to a naive check but the browser sees '//evil.com'.\n const clean = url.replace(/[\\t\\n\\r]/g, '')\n if (!clean) return false\n\n // A relative path must start with a SINGLE '/' — '//' or '/\\' resolve as\n // an absolute, cross-origin URL in the browser despite \"starting with /\".\n if (/^[/\\\\]{2}/.test(clean)) return false\n\n const scheme = clean.split(':')[0].toLowerCase()\n return ['http', 'https'].includes(scheme) || clean.startsWith('/')\n}\n","'use client'\n\nimport * as React from 'react'\nimport { Button, cn } from '@reblu/site-ui'\nimport { isSafeRedirectUrl } from './safe-redirect'\n\nexport interface AuthCallbackCopy {\n loadingTitle: string\n loadingMessage: string\n successMessage: string\n expiredTitle: string\n expiredMessage: string\n expiredCta: string\n networkErrorTitle: string\n networkErrorMessage: string\n networkErrorRetryCta: string\n networkErrorBackCta: string\n malformedTitle: string\n malformedMessage: string\n malformedCta: string\n}\n\nexport interface AuthCallbackProps {\n exchangeUrl: string\n loginUrl: string\n copy?: Partial<AuthCallbackCopy>\n onSuccess?: (redirectTo: string) => void\n onError?: (state: 'expired' | 'network' | 'malformed', error?: Error) => void\n className?: string\n}\n\ntype State = 'mounting' | 'parsing' | 'exchanging' | 'success' | 'expired' | 'network' | 'malformed'\n\nconst DEFAULT_COPY: AuthCallbackCopy = {\n loadingTitle: 'Autenticando...',\n loadingMessage: 'Estamos verificando tu identidad.',\n successMessage: 'Iniciando sesión...',\n expiredTitle: 'Enlace caducado',\n expiredMessage: 'El enlace ha expirado o ya fue utilizado. Solicita uno nuevo.',\n expiredCta: 'Volver al login',\n networkErrorTitle: 'Error de conexión',\n networkErrorMessage: 'No pudimos completar el inicio de sesión. Inténtalo de nuevo.',\n networkErrorRetryCta: 'Reintentar',\n networkErrorBackCta: 'Volver al login',\n malformedTitle: 'Enlace inválido',\n malformedMessage: 'Enlace inválido. Solicita uno nuevo.',\n malformedCta: 'Volver al login',\n}\n\nfunction parseHash(hash: string): { access_token: string; refresh_token: string } | null {\n const raw = hash.startsWith('#') ? hash.slice(1) : hash\n if (!raw) return null\n const params = new URLSearchParams(raw)\n const access_token = params.get('access_token') ?? ''\n const refresh_token = params.get('refresh_token') ?? ''\n const type = params.get('type') ?? ''\n if (!access_token || !refresh_token || type !== 'magiclink') return null\n return { access_token, refresh_token }\n}\n\n/**\n * On failure Supabase redirects to the callback with the reason in the hash\n * fragment (e.g. `#error=access_denied&error_code=otp_expired`), never in the\n * query — the server cannot see it (REB-177). Detect it client-side and tell an\n * expired/used link (recoverable → 'expired') apart from any other failure\n * (→ 'malformed').\n */\nfunction parseHashError(hash: string): 'expired' | 'malformed' | null {\n const raw = hash.startsWith('#') ? hash.slice(1) : hash\n if (!raw) return null\n const params = new URLSearchParams(raw)\n const error = params.get('error')\n const errorCode = params.get('error_code')\n if (!error && !errorCode) return null\n // Expired or already-used magic links come back as access_denied / otp_expired.\n return errorCode === 'otp_expired' || error === 'access_denied' ? 'expired' : 'malformed'\n}\n\nexport function AuthCallback({\n exchangeUrl,\n loginUrl,\n copy,\n onSuccess,\n onError,\n className,\n}: AuthCallbackProps) {\n const [state, setState] = React.useState<State>('mounting')\n const [tokens, setTokens] = React.useState<{\n access_token: string\n refresh_token: string\n } | null>(null)\n const retryRef = React.useRef<HTMLButtonElement>(null)\n const linkRef = React.useRef<HTMLAnchorElement>(null)\n const c = { ...DEFAULT_COPY, ...copy }\n\n const exchange = React.useCallback(\n async (t: { access_token: string; refresh_token: string }) => {\n setState('exchanging')\n try {\n const res = await fetch(exchangeUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(t),\n })\n\n if (res.ok) {\n const raw = (await res.json()) as unknown\n const redirectTo =\n raw !== null &&\n typeof raw === 'object' &&\n 'redirectTo' in raw &&\n typeof (raw as { redirectTo: unknown }).redirectTo === 'string'\n ? (raw as { redirectTo: string }).redirectTo\n : null\n\n if (!redirectTo) {\n setState('network')\n onError?.('network', undefined)\n return\n }\n\n // Guard against dangerous schemes (javascript:, data:, etc.) AND\n // protocol-relative/backslash-authority URLs (//evil.com, /\\evil.com)\n // — see safe-redirect.ts.\n if (!isSafeRedirectUrl(redirectTo)) {\n setState('network')\n onError?.('network', undefined)\n return\n }\n\n history.replaceState({}, '', window.location.pathname)\n window.location.replace(redirectTo)\n setState('success')\n onSuccess?.(redirectTo)\n return\n }\n\n if (res.status === 401) {\n setState('expired')\n onError?.('expired')\n return\n }\n\n setState('network')\n onError?.('network', undefined)\n } catch (err) {\n setState('network')\n onError?.('network', err instanceof Error ? err : undefined)\n }\n },\n [exchangeUrl, onError, onSuccess],\n )\n\n React.useEffect(() => {\n setState('parsing')\n // A failure reported in the hash (expired/used/invalid link) is not a token\n // to exchange — surface it directly without hitting the network (REB-177).\n const hashError = parseHashError(window.location.hash)\n if (hashError) {\n setState(hashError)\n onError?.(hashError)\n return\n }\n const parsed = parseHash(window.location.hash)\n if (!parsed) {\n setState('malformed')\n onError?.('malformed')\n return\n }\n setTokens(parsed)\n void exchange(parsed)\n // `exchange`/`onError` are intentionally excluded: this effect must run\n // EXACTLY ONCE on mount to parse `window.location.hash` — re-running it\n // on every `exchange` identity change (which itself changes whenever\n // `onSuccess`/`onError` props change identity) would re-parse an\n // already-cleared hash and misfire.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n React.useEffect(() => {\n if (state === 'network') retryRef.current?.focus()\n if (state === 'expired' || state === 'malformed') linkRef.current?.focus()\n }, [state])\n\n const isLoading = state === 'mounting' || state === 'parsing' || state === 'exchanging'\n\n return (\n <main className={cn('flex min-h-screen items-center justify-center', className)}>\n <div aria-live='polite' className='w-full max-w-md space-y-4 p-6 text-center'>\n {isLoading && (\n <section>\n <h2 className='text-xl font-semibold'>{c.loadingTitle}</h2>\n <p className='text-muted-foreground mt-2'>{c.loadingMessage}</p>\n </section>\n )}\n\n {state === 'success' && (\n <section>\n <p>{c.successMessage}</p>\n </section>\n )}\n\n {state === 'expired' && (\n <section className='space-y-4'>\n <h2 className='text-xl font-semibold'>{c.expiredTitle}</h2>\n <p className='text-muted-foreground'>{c.expiredMessage}</p>\n <Button asChild variant='outline'>\n <a href={loginUrl} ref={linkRef}>\n {c.expiredCta}\n </a>\n </Button>\n </section>\n )}\n\n {state === 'network' && (\n <section className='space-y-4'>\n <h2 className='text-xl font-semibold'>{c.networkErrorTitle}</h2>\n <p className='text-muted-foreground'>{c.networkErrorMessage}</p>\n <div className='flex flex-col gap-2'>\n <Button type='button' ref={retryRef} onClick={() => tokens && void exchange(tokens)}>\n {c.networkErrorRetryCta}\n </Button>\n <Button asChild variant='outline'>\n <a href={loginUrl}>{c.networkErrorBackCta}</a>\n </Button>\n </div>\n </section>\n )}\n\n {state === 'malformed' && (\n <section className='space-y-4'>\n <h2 className='text-xl font-semibold'>{c.malformedTitle}</h2>\n <p className='text-muted-foreground'>{c.malformedMessage}</p>\n <Button asChild variant='outline'>\n <a href={loginUrl} ref={linkRef}>\n {c.malformedCta}\n </a>\n </Button>\n </section>\n )}\n </div>\n </main>\n )\n}\n","/**\n * Maps the raw `?error=` query param that Supabase appends when a magic link is\n * already consumed or expired to a user-facing Spanish message.\n *\n * Supabase redirects to /login?error=... with a raw English message (e.g.\n * \"Email link is invalid or has expired\"). Showing that verbatim is confusing,\n * so we translate the common cases and signal whether the \"Solicitar otro\n * enlace\" CTA should be offered.\n *\n * Matching is case-insensitive and whitespace-tolerant so it survives small\n * variations in how the error text arrives.\n *\n * Relocated from apps/demo-site/src/lib/utils/login-errors.ts (REB-510 slice\n * 2a) as a copy-injectable helper — Spanish defaults are unchanged, and a SITE\n * MAY override individual messages via `Partial<LoginErrorCopy>`.\n */\n\nexport interface LoginErrorInfo {\n /** Message shown in the error card. */\n message: string\n /** Whether to show the \"Solicitar otro enlace\" CTA. */\n showRequestLink: boolean\n}\n\nexport interface LoginErrorCopy {\n expiredMessage: string\n missingTokenMessage: string\n genericMessage: string\n}\n\nconst DEFAULT_COPY: LoginErrorCopy = {\n expiredMessage: 'El enlace ha expirado o ya fue utilizado.',\n missingTokenMessage: 'El enlace no es válido. Solicita uno nuevo.',\n genericMessage: 'No se pudo completar el inicio de sesión. Intenta de nuevo.',\n}\n\nexport function mapLoginError(\n error: string | null | undefined,\n copy?: Partial<LoginErrorCopy>,\n): LoginErrorInfo | null {\n if (!error) return null\n\n const normalized = error.trim().toLowerCase()\n if (normalized === '') return null\n\n const c = { ...DEFAULT_COPY, ...copy }\n\n if (\n normalized.includes('email link is invalid or has expired') ||\n normalized.includes('token has expired or is invalid')\n ) {\n return { message: c.expiredMessage, showRequestLink: true }\n }\n\n if (normalized.includes('missing_token')) {\n return { message: c.missingTokenMessage, showRequestLink: true }\n }\n\n return { message: c.genericMessage, showRequestLink: true }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useTheme } from 'next-themes'\nimport { cn } from '@reblu/site-ui'\n\nexport interface AuthLogoCopy {\n /**\n * Overrides the rendered `<img>` alt text. Pass `''` explicitly to mark the\n * logo as decorative (e.g. when an adjacent heading already announces\n * `siteName`, so screen readers shouldn't read it twice) — `undefined`\n * (the key simply omitted from `copy`) is what falls back to `siteName`.\n */\n altText?: string\n /** Shown when neither a logo URL nor `siteName` is available. */\n fallbackLabel: string\n}\n\nexport interface AuthLogoProps {\n logoLightExpandedUrl: string | null\n logoDarkExpandedUrl: string | null\n logoLightSmallUrl: string | null\n logoDarkSmallUrl: string | null\n siteName: string\n /** @default 'expanded' */\n variant?: 'expanded' | 'compact'\n className?: string\n copy?: Partial<AuthLogoCopy>\n /**\n * Heading level for the `siteName`/fallback-label text rendered when no logo\n * resolves. AuthLogo owns the auth page's heading, so this defaults to the\n * top-level `1`; pass a deeper level only if the page already has an `<h1>`\n * elsewhere that AuthLogo must sit beneath.\n * @default 1\n */\n headingLevel?: 1 | 2 | 3 | 4 | 5 | 6\n /**\n * When `true`, renders `null` instead of the `siteName`/fallback-label\n * heading whenever no logo URL resolves for the active variant+theme, rather\n * than showing the text. Use this ONLY when the consumer intentionally\n * renders `siteName` elsewhere itself — by default AuthLogo IS the heading\n * (\"logo XOR name\"), so the consumer should NOT add its own\n * `<h1>{siteName}</h1>`.\n * @default false\n */\n hideTextFallback?: boolean\n}\n\nconst DEFAULT_COPY: Required<Pick<AuthLogoCopy, 'fallbackLabel'>> = {\n fallbackLabel: 'Inicio de sesión',\n}\n\nconst VARIANT_CLASS: Record<'expanded' | 'compact', string> = {\n expanded: 'h-8 w-auto',\n compact: 'h-8 w-8',\n}\n\n// Intrinsic `width`/`height` attributes so the browser reserves layout space\n// via its built-in aspect-ratio-from-attributes heuristic before the real\n// image loads (avoids vertical CLS at minimum; horizontal width still\n// depends on the tenant's actual logo aspect ratio, which `/api/site/config`\n// doesn't expose — an arbitrary-but-reasonable box, not a promise of zero\n// shift). `VARIANT_CLASS`'s `h-8` always wins for height regardless.\nconst VARIANT_INTRINSIC_SIZE: Record<'expanded' | 'compact', { width: number; height: number }> = {\n expanded: { width: 160, height: 32 },\n compact: { width: 32, height: 32 },\n}\n\n/**\n * Dynamic tenant logo for auth pages (REB-510 R1). Resolves expanded/compact ×\n * light/dark from the 4 `/api/site/config` logo URLs, switching light/dark\n * CLIENT-SIDE via `next-themes`. Falls back to `siteName` text, then a generic\n * `copy.fallbackLabel`, whenever the resolved URL for the active\n * variant+theme combination is `null` — it never silently swaps in the OTHER\n * theme's logo.\n *\n * Hydration-safe: `next-themes`' `resolvedTheme` is `undefined` until it\n * mounts client-side, so pre-mount `isDark` is always `false` — the same\n * light-first markup the server rendered, avoiding a hydration mismatch.\n *\n * Renders a plain `<img>`, not `next/image`: this is a distributable SDK\n * consumed by external SITE repos, so it cannot assume the consuming app's\n * `next.config` `images.remotePatterns` allowlists the tenant's logo CDN.\n */\nexport function AuthLogo(props: AuthLogoProps) {\n const {\n logoLightExpandedUrl,\n logoDarkExpandedUrl,\n logoLightSmallUrl,\n logoDarkSmallUrl,\n siteName,\n variant = 'expanded',\n className,\n copy,\n headingLevel = 1,\n hideTextFallback = false,\n } = props\n const fallbackLabel = copy?.fallbackLabel ?? DEFAULT_COPY.fallbackLabel\n const { resolvedTheme } = useTheme()\n const isDark = resolvedTheme === 'dark'\n\n const lightUrl = variant === 'compact' ? logoLightSmallUrl : logoLightExpandedUrl\n const darkUrl = variant === 'compact' ? logoDarkSmallUrl : logoDarkExpandedUrl\n const logoUrl = isDark ? darkUrl : lightUrl\n\n // Tracks a broken/stale logo URL (404, CDN outage, etc.) — degrades to the\n // text fallback instead of a broken-image glyph. Reset whenever `logoUrl`\n // itself changes (e.g. a theme toggle resolves a DIFFERENT URL) so a past\n // failure doesn't permanently block a future, valid attempt.\n const [imgError, setImgError] = React.useState(false)\n React.useEffect(() => {\n setImgError(false)\n }, [logoUrl])\n\n const fallbackText = siteName || fallbackLabel\n // `copy.altText` — a VALUE-defined key (including `''`) wins outright,\n // that's how a consumer marks the logo decorative. A value-undefined key\n // (whether omitted from `copy` entirely, or present with an explicit\n // `undefined` value) falls back to siteName/fallbackLabel — checking value\n // definedness, not `'altText' in copy` key presence, is what makes\n // `copy={{ altText: undefined }}` behave the same as omitting the key.\n const altText = copy?.altText !== undefined ? copy.altText : siteName || fallbackLabel\n\n // AuthLogo is \"logo XOR name\": exactly ONE of the brand mark (logo image) or\n // the `siteName` text is shown, never both — and by default AuthLogo IS the\n // auth page's heading, so the consuming page must NOT add its own separate\n // `<h1>{siteName}</h1>` (which is what double-rendered the name before).\n const Heading = `h${headingLevel}` as const\n\n if (logoUrl && !imgError) {\n const { width, height } = VARIANT_INTRINSIC_SIZE[variant]\n // Decorative escape hatch: when the consumer hides the text fallback OR\n // marks the logo decorative (`alt=''`), it owns the page heading itself, so\n // render a bare image — wrapping a nameless image in a heading would emit\n // an empty, accessible-name-less heading.\n const bare = hideTextFallback || altText === ''\n const image = (\n <img\n src={logoUrl}\n alt={altText}\n width={width}\n height={height}\n className={cn(VARIANT_CLASS[variant], 'max-w-full', bare && className)}\n onError={() => setImgError(true)}\n />\n )\n if (bare) return image\n // Default: wrap the logo in the heading so the auth page ALWAYS has a\n // heading — even when only the brand image is visible — whose accessible\n // name is the logo's `alt` (= `siteName`). No visible, duplicated name text.\n return <Heading className={cn('inline-flex items-center max-w-full', className)}>{image}</Heading>\n }\n\n if (hideTextFallback) return null\n\n // No logo resolved → render `siteName` (or the generic fallback label) as the\n // page's heading text.\n return (\n <Heading className={cn('text-2xl font-bold tracking-tight text-foreground', className)}>\n {fallbackText}\n </Heading>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\n\nexport interface ResetPasswordRequestCopy {\n emailLabel: string\n emailPlaceholder: string\n submitCta: string\n submittingCta: string\n successTitle: string\n successMessage: string\n backToLoginCta: string\n invalidEmailError: string\n networkError: string\n}\n\nexport interface ResetPasswordRequestProps {\n requestUrl: string\n loginUrl: string\n copy?: Partial<ResetPasswordRequestCopy>\n className?: string\n onSubmitted?: (email: string) => void\n}\n\nconst DEFAULT_COPY: ResetPasswordRequestCopy = {\n emailLabel: 'Correo electrónico',\n emailPlaceholder: 'tu@email.com',\n submitCta: 'Enviar enlace de recuperación',\n submittingCta: 'Enviando...',\n successTitle: 'Revisa tu email',\n successMessage:\n 'Si el email existe en nuestra plataforma, recibirás un enlace para restablecer tu contraseña en los próximos minutos.',\n backToLoginCta: '← Volver al inicio de sesión',\n invalidEmailError: 'Introduce un correo electrónico válido.',\n networkError: 'Error de conexión. Por favor, inténtalo de nuevo.',\n}\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Step 1 of the password-reset flow — anti-enumeration: always shows the same\n * generic success message on a 2xx response, regardless of whether the email\n * exists (the backend never leaks that).\n */\nexport function ResetPasswordRequest(props: ResetPasswordRequestProps) {\n const { requestUrl, loginUrl, copy, className, onSubmitted } = props\n const c = { ...DEFAULT_COPY, ...copy }\n\n const [email, setEmail] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [submitted, setSubmitted] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n\n const trimmed = email.trim().toLowerCase()\n if (!EMAIL_REGEX.test(trimmed)) {\n setError(c.invalidEmailError)\n return\n }\n\n setLoading(true)\n // `succeeded` isolates the consumer's `onSubmitted` callback from this\n // try/catch: if we called it INSIDE the try and it threw, that throw\n // would land in the network `catch` below and misreport a successful\n // request as a connection error.\n let succeeded = false\n try {\n const res = await fetch(requestUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: trimmed }),\n })\n\n if (!res.ok) {\n const json = (await res.json().catch(() => ({}))) as { error?: string }\n setError(json.error ?? c.networkError)\n setLoading(false)\n return\n }\n\n succeeded = true\n setSubmitted(true)\n setLoading(false)\n } catch {\n setError(c.networkError)\n setLoading(false)\n return\n }\n\n if (succeeded) {\n try {\n onSubmitted?.(trimmed)\n } catch (err) {\n console.error('[ResetPasswordRequest] onSubmitted callback threw:', err)\n }\n }\n }\n\n if (submitted) {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.successTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.successMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.backToLoginCta}\n </a>\n </div>\n )\n }\n\n return (\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className={cn('space-y-5', className)}\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='reset-request-email'>{c.emailLabel}</Label>\n <Input\n id='reset-request-email'\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n disabled={loading}\n aria-describedby={error ? 'reset-request-error' : undefined}\n autoFocus\n />\n {error && (\n <p id='reset-request-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button type='submit' className='w-full' disabled={loading}>\n {loading ? c.submittingCta : c.submitCta}\n </Button>\n <div className='text-center'>\n <a\n href={loginUrl}\n className='text-muted-foreground hover:text-foreground text-sm transition-colors'\n >\n {c.backToLoginCta}\n </a>\n </div>\n </form>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\n\n/**\n * Appends `key=value` to `url` without producing a malformed double `?` when\n * `url` (consumer-supplied `loginUrl`, e.g. `/login?tenant=acme`) already\n * carries a query string. Deliberately NOT `new URL(url)` — `loginUrl` is\n * typically a relative path and the WHATWG `URL` constructor requires a base\n * for those.\n */\nfunction withQueryParam(url: string, key: string, value: string): string {\n const separator = url.includes('?') ? '&' : '?'\n return `${url}${separator}${key}=${encodeURIComponent(value)}`\n}\n\nexport interface ResetPasswordConfirmCopy {\n passwordLabel: string\n passwordPlaceholder: string\n confirmPasswordLabel: string\n confirmPasswordPlaceholder: string\n mfaCodeLabel: string\n mfaCodePlaceholder: string\n mfaHelpMessage: string\n submitCta: string\n submittingCta: string\n passwordTooShortError: string\n passwordMismatchError: string\n mfaInvalidCodeError: string\n genericError: string\n missingTokensTitle: string\n missingTokensMessage: string\n requestNewLinkCta: string\n}\n\nexport interface ResetPasswordConfirmProps {\n confirmUrl: string\n loginUrl: string\n resetRequestUrl: string\n copy?: Partial<ResetPasswordConfirmCopy>\n className?: string\n onSuccess?: () => void\n}\n\nconst DEFAULT_COPY: ResetPasswordConfirmCopy = {\n passwordLabel: 'Nueva contraseña',\n passwordPlaceholder: 'Mínimo 8 caracteres',\n confirmPasswordLabel: 'Confirmar contraseña',\n confirmPasswordPlaceholder: 'Repite la contraseña',\n mfaCodeLabel: 'Código de verificación (2FA)',\n mfaCodePlaceholder: '6 dígitos',\n mfaHelpMessage:\n 'Tu cuenta tiene verificación en dos pasos. Introduce el código de tu app de autenticación para completar el cambio.',\n submitCta: 'Establecer nueva contraseña',\n submittingCta: 'Actualizando contraseña...',\n passwordTooShortError: 'La contraseña debe tener al menos 8 caracteres',\n passwordMismatchError: 'Las contraseñas no coinciden',\n mfaInvalidCodeError: 'Introduce el código de 6 dígitos de tu app de autenticación',\n genericError: 'Error al restablecer la contraseña',\n missingTokensTitle: 'Enlace inválido',\n missingTokensMessage: 'El enlace ha expirado o no es válido. Solicita uno nuevo.',\n requestNewLinkCta: 'Solicitar nuevo enlace',\n}\n\n/**\n * Step 2 of the password-reset flow. Reads `access_token`/`refresh_token`\n * straight off `useSearchParams` — no `window.location.search` + effect-timing\n * workaround needed here because, unlike a page-level default export, this is\n * a component the SITE mounts inside its own `<Suspense>` boundary.\n *\n * REB-378: when the account has a verified MFA factor, the recovery session\n * (AAL1) cannot change the password on its own. The backend responds\n * `{ data: { mfaRequired: true } }` and this reveals a 6-digit TOTP field so\n * the user can resubmit with the code (the recovery tokens stay valid).\n */\nexport function ResetPasswordConfirm(props: ResetPasswordConfirmProps) {\n const { confirmUrl, loginUrl, resetRequestUrl, copy, className, onSuccess } = props\n const c = { ...DEFAULT_COPY, ...copy }\n const router = useRouter()\n const searchParams = useSearchParams()\n\n const accessToken = searchParams.get('access_token')\n const refreshToken = searchParams.get('refresh_token')\n const hasTokens = Boolean(accessToken && refreshToken)\n\n const [password, setPassword] = React.useState('')\n const [confirmPassword, setConfirmPassword] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [mfaRequired, setMfaRequired] = React.useState(false)\n const [mfaCode, setMfaCode] = React.useState('')\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n if (!hasTokens) return\n\n if (password.length < 8) {\n setError(c.passwordTooShortError)\n return\n }\n if (password !== confirmPassword) {\n setError(c.passwordMismatchError)\n return\n }\n if (mfaRequired && !/^\\d{6}$/.test(mfaCode)) {\n setError(c.mfaInvalidCodeError)\n return\n }\n\n setLoading(true)\n // `succeeded` isolates the consumer's `onSuccess` callback (and the\n // navigation) from this try/catch: calling either INSIDE the try means a\n // throwing callback lands in the network `catch` below and misreports an\n // already-successful password change as a failure — the user retries\n // with recovery tokens the backend has already consumed.\n let succeeded = false\n try {\n const res = await fetch(confirmUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n access_token: accessToken,\n refresh_token: refreshToken,\n new_password: password,\n ...(mfaRequired ? { mfa_code: mfaCode } : {}),\n }),\n })\n\n const json = (await res.json().catch(() => ({}))) as {\n error?: string\n data?: { success?: boolean; mfaRequired?: boolean }\n }\n\n if (!res.ok) {\n setError(json.error ?? c.genericError)\n setLoading(false)\n return\n }\n\n if (json.data?.mfaRequired) {\n setMfaRequired(true)\n setLoading(false)\n return\n }\n\n succeeded = true\n setLoading(false)\n } catch {\n setError(c.genericError)\n setLoading(false)\n return\n }\n\n if (succeeded) {\n // Navigate FIRST, consumer callback SECOND (isolated in its own\n // try/catch) — same reasoning as set-password.tsx: a throwing\n // `onSuccess` can no longer block the redirect or escape as an\n // unhandled rejection out of this fire-and-forget async handler.\n router.replace(withQueryParam(loginUrl, 'reset', 'success'))\n try {\n onSuccess?.()\n } catch (err) {\n console.error('[ResetPasswordConfirm] onSuccess callback threw:', err)\n }\n }\n }\n\n if (!hasTokens) {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.missingTokensTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.missingTokensMessage}</p>\n <a href={resetRequestUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.requestNewLinkCta}\n </a>\n </div>\n )\n }\n\n return (\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className={cn('space-y-5', className)}\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='reset-confirm-password'>{c.passwordLabel}</Label>\n <Input\n id='reset-confirm-password'\n type='password'\n autoComplete='new-password'\n placeholder={c.passwordPlaceholder}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'reset-confirm-error' : undefined}\n autoFocus\n />\n </div>\n <div className='space-y-1.5'>\n <Label htmlFor='reset-confirm-password-2'>{c.confirmPasswordLabel}</Label>\n <Input\n id='reset-confirm-password-2'\n type='password'\n autoComplete='new-password'\n placeholder={c.confirmPasswordPlaceholder}\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'reset-confirm-error' : undefined}\n />\n </div>\n {mfaRequired && (\n <div className='space-y-1.5'>\n <Label htmlFor='reset-confirm-mfa'>{c.mfaCodeLabel}</Label>\n <Input\n id='reset-confirm-mfa'\n type='text'\n inputMode='numeric'\n autoComplete='one-time-code'\n placeholder={c.mfaCodePlaceholder}\n value={mfaCode}\n onChange={(e) => setMfaCode(e.target.value.replace(/\\D/g, '').slice(0, 6))}\n disabled={loading}\n maxLength={6}\n aria-describedby={error ? 'reset-confirm-error' : undefined}\n autoFocus\n />\n <p className='text-muted-foreground text-xs'>{c.mfaHelpMessage}</p>\n </div>\n )}\n {error && (\n <p id='reset-confirm-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n <Button\n type='submit'\n className='w-full'\n disabled={\n loading || !password || !confirmPassword || (mfaRequired && mfaCode.length !== 6)\n }\n >\n {loading ? c.submittingCta : c.submitCta}\n </Button>\n </form>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\nimport { isSafeRedirectUrl } from './safe-redirect'\n\nexport interface SetPasswordCopy {\n passwordLabel: string\n passwordPlaceholder: string\n confirmPasswordLabel: string\n confirmPasswordPlaceholder: string\n submitCta: string\n submittingCta: string\n passwordTooShortError: string\n passwordMismatchError: string\n genericError: string\n missingTokenTitle: string\n missingTokenMessage: string\n backToLoginCta: string\n}\n\nexport interface SetPasswordProps {\n submitUrl: string\n loginUrl: string\n copy?: Partial<SetPasswordCopy>\n className?: string\n onSuccess?: (sessionExchangeUrl: string) => void\n}\n\nconst DEFAULT_COPY: SetPasswordCopy = {\n passwordLabel: 'Contraseña',\n passwordPlaceholder: 'Mínimo 8 caracteres',\n confirmPasswordLabel: 'Confirmar contraseña',\n confirmPasswordPlaceholder: 'Repite la contraseña',\n submitCta: 'Activar cuenta',\n submittingCta: 'Activando cuenta...',\n passwordTooShortError: 'La contraseña debe tener al menos 8 caracteres',\n passwordMismatchError: 'Las contraseñas no coinciden',\n genericError: 'Error al establecer la contraseña',\n missingTokenTitle: 'Enlace inválido',\n missingTokenMessage: 'El enlace de invitación ha expirado o no es válido.',\n backToLoginCta: '← Volver al inicio de sesión',\n}\n\n/**\n * Invite flow — for a user setting their initial password. Reads\n * `session_exchange_token` off `useSearchParams`; on success, redirects via\n * `window.location.assign` (NOT the Next router) because the returned\n * `sessionExchangeUrl` points at base-api's own domain to set cookies, not a\n * same-origin SITE route.\n */\nexport function SetPassword(props: SetPasswordProps) {\n const { submitUrl, loginUrl, copy, className, onSuccess } = props\n const c = { ...DEFAULT_COPY, ...copy }\n const searchParams = useSearchParams()\n const sessionExchangeToken = searchParams.get('session_exchange_token')\n\n const [password, setPassword] = React.useState('')\n const [confirmPassword, setConfirmPassword] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n if (!sessionExchangeToken) return\n\n if (password.length < 8) {\n setError(c.passwordTooShortError)\n return\n }\n if (password !== confirmPassword) {\n setError(c.passwordMismatchError)\n return\n }\n\n setLoading(true)\n // `sessionExchangeUrl` isolates the consumer's `onSuccess` callback (and\n // the actual navigation) from this try/catch: calling either INSIDE the\n // try means a throwing callback lands in the network `catch` below and\n // misreports an already-successful password-set as a connection error —\n // stranding an activated user on a form that tells them it failed.\n let sessionExchangeUrl: string | null = null\n try {\n const res = await fetch(submitUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n session_exchange_token: sessionExchangeToken,\n new_password: password,\n }),\n })\n\n const json = (await res.json().catch(() => ({}))) as {\n data?: { sessionExchangeUrl?: string }\n error?: string\n }\n\n if (!res.ok || !json.data?.sessionExchangeUrl) {\n setError(json.error ?? c.genericError)\n setLoading(false)\n return\n }\n\n if (!isSafeRedirectUrl(json.data.sessionExchangeUrl)) {\n setError(c.genericError)\n setLoading(false)\n return\n }\n\n sessionExchangeUrl = json.data.sessionExchangeUrl\n } catch {\n setError(c.genericError)\n setLoading(false)\n return\n }\n\n // Navigate FIRST, consumer callback SECOND (isolated in its own\n // try/catch): if `onSuccess` throws (ad blocker on an analytics call,\n // etc.) the navigation has already been triggered and is unaffected, and\n // the throw doesn't escape as an unhandled rejection out of this\n // fire-and-forget async handler — the user is never stranded on this\n // form by code that isn't even this component's own.\n window.location.assign(sessionExchangeUrl)\n try {\n onSuccess?.(sessionExchangeUrl)\n } catch (err) {\n console.error('[SetPassword] onSuccess callback threw:', err)\n }\n }\n\n if (!sessionExchangeToken) {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.missingTokenTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.missingTokenMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.backToLoginCta}\n </a>\n </div>\n )\n }\n\n return (\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className={cn('space-y-5', className)}\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='set-password-password'>{c.passwordLabel}</Label>\n <Input\n id='set-password-password'\n type='password'\n autoComplete='new-password'\n placeholder={c.passwordPlaceholder}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'set-password-error' : undefined}\n // Single-purpose activation page (this IS the whole page's task,\n // there's no earlier content competing for attention) — same\n // pattern as reset-password-confirm.tsx/reset-password-request.tsx.\n autoFocus\n />\n </div>\n <div className='space-y-1.5'>\n <Label htmlFor='set-password-confirm'>{c.confirmPasswordLabel}</Label>\n <Input\n id='set-password-confirm'\n type='password'\n autoComplete='new-password'\n placeholder={c.confirmPasswordPlaceholder}\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'set-password-error' : undefined}\n />\n </div>\n {error && (\n <p id='set-password-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n <Button type='submit' className='w-full' disabled={loading || !password || !confirmPassword}>\n {loading ? c.submittingCta : c.submitCta}\n </Button>\n </form>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport { cn } from '@reblu/site-ui'\nimport { isSafeRedirectUrl } from './safe-redirect'\n\nexport interface AuthConfirmCopy {\n loadingMessage: string\n successTitle: string\n successMessage: string\n successCta: string\n errorTitle: string\n errorMessage: string\n errorCta: string\n}\n\nexport interface AuthConfirmProps {\n confirmUrl: string\n loginUrl: string\n copy?: Partial<AuthConfirmCopy>\n className?: string\n}\n\nconst DEFAULT_COPY: AuthConfirmCopy = {\n loadingMessage: 'Confirmando tu email...',\n successTitle: 'Email confirmado',\n successMessage: 'Tu dirección de email ha sido verificada correctamente.',\n successCta: 'Iniciar sesión',\n errorTitle: 'Error al confirmar',\n errorMessage: 'El enlace de confirmación no es válido o ha expirado.',\n errorCta: 'Volver al inicio de sesión',\n}\n\ntype Status = 'loading' | 'success' | 'error'\n\n/**\n * Dual-mode email confirmation (signup / email-change):\n *\n * 1. `?success=true` — base-api already ran `verifyOtp` server-side and\n * redirected here. Shows the confirmed message with no API call.\n * 2. `?token_hash=&type=` — client-initiated verify: POSTs to `confirmUrl`\n * and follows the returned `sessionExchangeUrl`, or shows success/error\n * copy based on the response.\n *\n * Neither param present → shown as an error state with a link back to login,\n * rather than a silent client-side redirect.\n */\nexport function AuthConfirm(props: AuthConfirmProps) {\n const { confirmUrl, loginUrl, copy, className } = props\n const c = { ...DEFAULT_COPY, ...copy }\n const searchParams = useSearchParams()\n\n const success = searchParams.get('success')\n const tokenHash = searchParams.get('token_hash')\n const type = searchParams.get('type')\n\n const [status, setStatus] = React.useState<Status>(success === 'true' ? 'success' : 'loading')\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (success === 'true') {\n setStatus('success')\n return\n }\n\n if (tokenHash && type) {\n // `token_hash` is single-use server-side — verifying it twice makes\n // the SECOND call fail even though the first already succeeded. React\n // Strict Mode's dev-only mount→cleanup→mount double-invoke would\n // otherwise fire this fetch twice for the same token. Aborting the\n // in-flight request on cleanup stops the CLIENT from processing that\n // first response and swallows its `AbortError` below instead of\n // surfacing a false \"confirmation failed\" — but `abort()` cannot\n // retroactively un-send a request that already reached base-api\n // before cleanup ran, so this reduces rather than deterministically\n // eliminates the double-POST race (in practice React's dev\n // double-invoke runs both phases synchronously in the same commit, so\n // the abort typically wins, but that is not a spec guarantee). A\n // fully deterministic fix needs server-side idempotency on\n // `verify-otp`, which is out of this package's control. The same\n // abort also guards a genuine unmount mid-request.\n const controller = new AbortController()\n setStatus('loading')\n fetch(confirmUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token_hash: tokenHash, type }),\n signal: controller.signal,\n })\n .then(async (res) => {\n const json = (await res.json().catch(() => ({}))) as {\n data?: { sessionExchangeUrl?: string; success?: boolean }\n error?: string\n }\n\n if (!res.ok) {\n setStatus('error')\n setErrorMessage(json.error ?? c.errorMessage)\n return\n }\n\n if (json.data?.sessionExchangeUrl) {\n if (!isSafeRedirectUrl(json.data.sessionExchangeUrl)) {\n setStatus('error')\n setErrorMessage(c.errorMessage)\n return\n }\n window.location.assign(json.data.sessionExchangeUrl)\n return\n }\n\n setStatus('success')\n })\n .catch((err: unknown) => {\n if (err instanceof Error && err.name === 'AbortError') return\n setStatus('error')\n setErrorMessage(c.errorMessage)\n })\n\n return () => controller.abort()\n }\n\n // Neither success=true nor token_hash+type — nothing to confirm.\n setStatus('error')\n // `c.errorMessage` (derived from the `copy` prop) is intentionally\n // excluded: re-running this effect on every `copy` object identity\n // change would re-fire the fetch. The URL params are the only real\n // trigger for this effect.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [success, tokenHash, type, confirmUrl])\n\n if (status === 'loading') {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <p className='text-muted-foreground text-sm'>{c.loadingMessage}</p>\n </div>\n )\n }\n\n if (status === 'error') {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.errorTitle}</h2>\n <p className='text-muted-foreground text-sm'>{errorMessage ?? c.errorMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.errorCta}\n </a>\n </div>\n )\n }\n\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.successTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.successMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.successCta}\n </a>\n </div>\n )\n}\n"],"mappings":"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","names":["React","Button","cn","jsx","jsxs","DEFAULT_COPY","cn","Button","DEFAULT_COPY","React","cn","jsx","DEFAULT_COPY","React","Button","Input","cn","Label","jsx","jsxs","DEFAULT_COPY","EMAIL_REGEX","React","Button","Input","cn","Label","jsx","jsxs","DEFAULT_COPY","React","useSearchParams","Button","Input","cn","Label","jsx","jsxs","DEFAULT_COPY","useSearchParams","cn","Label","Input","Button","React","useSearchParams","cn","jsx","jsxs","DEFAULT_COPY","useSearchParams","cn"]}
1
+ {"version":3,"sources":["../src/auth/login-form.tsx","../src/auth/safe-redirect.ts","../src/auth/magic-link-cooldown.ts","../src/auth/magic-link-events.ts","../src/auth/auth-callback.tsx","../src/auth/login-errors.ts","../src/auth/auth-logo.tsx","../src/auth/reset-password-request.tsx","../src/auth/reset-password-confirm.tsx","../src/auth/set-password.tsx","../src/auth/auth-confirm.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@reblu/site-ui/tabs'\nimport { isSafeRedirectUrl } from './safe-redirect'\nimport { MAGIC_LINK_COOLDOWN_SECONDS } from './magic-link-cooldown'\nimport { emitMagicLinkRequested } from './magic-link-events'\n\nexport interface LoginFormCopy {\n // Tab labels\n magicLinkTabLabel: string\n passwordTabLabel: string\n\n // Shared\n emailLabel: string\n emailPlaceholder: string\n\n // Password tab\n passwordLabel: string\n passwordPlaceholder: string\n passwordSubmitCta: string\n forgotPasswordCta: string\n\n // Magic-link tab\n magicLinkSubmitCta: string\n\n // Magic-link success\n magicLinkSuccessTitle: string\n magicLinkSuccessMessage: string\n /** Countdown line. `{seconds}` is replaced with the seconds remaining. */\n magicLinkCooldownMessage: string\n magicLinkResendCta: string\n\n // MFA prompt\n mfaTitle: string\n mfaMessage: string\n mfaCodeLabel: string\n mfaCodePlaceholder: string\n mfaSubmitCta: string\n\n // Errors\n invalidEmailError: string\n networkError: string\n invalidCredentialsError: string\n mfaInvalidCodeError: string\n}\n\nexport interface LoginFormProps {\n // Endpoint URLs (all required)\n passwordLoginUrl: string\n mfaVerifyUrl: string\n magicLinkRequestUrl: string\n resetPasswordUrl: string\n\n // UX\n copy?: Partial<LoginFormCopy>\n className?: string\n defaultMethod?: 'magic-link' | 'password'\n /**\n * Seconds the form refuses to request another magic link after a successful\n * request. Mirrors Supabase Auth's per-project \"minimum interval per user\",\n * which silently drops the second link — see `MAGIC_LINK_COOLDOWN_SECONDS`\n * for where the authoritative value lives and why this is a mirror. Override\n * only for a SITE whose Supabase project uses a different interval.\n */\n cooldownSeconds?: number\n\n // Hooks\n onPasswordSubmitted?: (email: string) => void\n onMagicLinkSubmitted?: (email: string) => void\n onMfaSubmitted?: () => void\n}\n\nconst DEFAULT_COPY: LoginFormCopy = {\n magicLinkTabLabel: 'Enlace mágico',\n passwordTabLabel: 'Contraseña',\n emailLabel: 'Correo electrónico',\n emailPlaceholder: 'tu@email.com',\n passwordLabel: 'Contraseña',\n passwordPlaceholder: '••••••••',\n passwordSubmitCta: 'Iniciar sesión',\n forgotPasswordCta: '¿Olvidaste tu contraseña?',\n magicLinkSubmitCta: 'Enviar enlace de acceso',\n magicLinkSuccessTitle: 'Revisa tu correo',\n // True in every branch, and leaks nothing: the request path answers an\n // identical 200 whether or not the account exists, and whether or not an\n // email actually went out (REB-762). \"¡Enlace enviado!\" asserted a send that\n // may never have happened — the user then waited for something that was not\n // coming.\n magicLinkSuccessMessage:\n 'Si la cuenta existe, recibirás un enlace en unos minutos. Si ya lo has pedido hace poco, espera antes de volver a intentarlo.',\n magicLinkCooldownMessage: 'Podrás pedir otro enlace en {seconds} s.',\n magicLinkResendCta: 'Pedir otro enlace',\n mfaTitle: 'Verificación en dos pasos',\n mfaMessage: 'Introduce el código de 6 dígitos de tu aplicación autenticadora.',\n mfaCodeLabel: 'Código de verificación',\n mfaCodePlaceholder: '123456',\n mfaSubmitCta: 'Verificar código',\n invalidEmailError: 'Introduce un correo electrónico válido.',\n networkError: 'No se pudo enviar el enlace. Inténtalo de nuevo.',\n invalidCredentialsError: 'Email o contraseña incorrectos',\n mfaInvalidCodeError: 'Código incorrecto. Por favor, inténtalo de nuevo.',\n}\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isValidEmail(value: string): boolean {\n return EMAIL_REGEX.test(value)\n}\n\ntype FormState = 'idle' | 'submitting' | 'magic-link-success' | 'mfa-required' | 'error'\n\ninterface MfaState {\n factorId: string\n accessToken: string\n refreshToken: string\n}\n\ninterface PasswordLoginResponse {\n data?: {\n mfaRequired: boolean\n sessionExchangeUrl?: string\n factorId?: string\n accessToken?: string\n refreshToken?: string\n }\n error?: string\n}\n\ninterface MfaVerifyResponse {\n data?: { sessionExchangeUrl: string }\n error?: string\n}\n\nexport function LoginForm(props: LoginFormProps) {\n const {\n passwordLoginUrl,\n mfaVerifyUrl,\n magicLinkRequestUrl,\n resetPasswordUrl,\n copy,\n className,\n defaultMethod = 'password',\n cooldownSeconds = MAGIC_LINK_COOLDOWN_SECONDS,\n onPasswordSubmitted,\n onMagicLinkSubmitted,\n onMfaSubmitted,\n } = props\n\n const c = { ...DEFAULT_COPY, ...copy }\n\n const [method, setMethod] = React.useState<'magic-link' | 'password'>(defaultMethod)\n const [state, setState] = React.useState<FormState>('idle')\n const [error, setError] = React.useState<string | null>(null)\n\n // Password & magic-link inputs\n const [email, setEmail] = React.useState('')\n const [password, setPassword] = React.useState('')\n\n // MFA state\n const [mfa, setMfa] = React.useState<MfaState | null>(null)\n const [mfaCode, setMfaCode] = React.useState('')\n\n // Seconds left before another magic link may be requested (REB-762).\n const [cooldownLeft, setCooldownLeft] = React.useState(0)\n\n // The success panel REPLACES the form, so the submit button the user just\n // pressed is unmounted and focus falls to <body> — a keyboard or screen-reader\n // user is dropped at the top of the document with no idea what happened.\n // Move focus to the panel's heading instead (WCAG 2.4.3 / 4.1.3).\n const successHeadingRef = React.useRef<HTMLHeadingElement>(null)\n\n // …and the return trip has the same problem: \"Pedir otro enlace\" unmounts\n // the button it was pressed on. Focus the email field the user is being sent\n // back to fill, rather than letting focus fall to <body> — the keyboard user\n // who waited out the full cooldown is exactly the person this path is for.\n const magicEmailRef = React.useRef<HTMLInputElement>(null)\n const returningFromSuccessRef = React.useRef(false)\n\n // One interval for the whole countdown, keyed on whether it is running rather\n // than on the seconds themselves: re-subscribing every tick would restart the\n // timer each second, and a chain of one-shot timeouts cannot be driven by a\n // test's fake clock in a single step (each tick would need its own flush).\n const isCoolingDown = cooldownLeft > 0\n React.useEffect(() => {\n if (!isCoolingDown) return\n const id = setInterval(() => {\n setCooldownLeft((s) => Math.max(0, s - 1))\n }, 1000)\n return () => {\n clearInterval(id)\n }\n }, [isCoolingDown])\n\n React.useEffect(() => {\n if (state === 'magic-link-success') successHeadingRef.current?.focus()\n if (state === 'idle' && returningFromSuccessRef.current) {\n returningFromSuccessRef.current = false\n magicEmailRef.current?.focus()\n }\n }, [state])\n\n const resetTransientState = React.useCallback(() => {\n setState('idle')\n setError(null)\n setMfa(null)\n setMfaCode('')\n }, [])\n\n const handleResend = () => {\n returningFromSuccessRef.current = true\n resetTransientState()\n }\n\n const handleTabChange = (next: string) => {\n if (next !== 'magic-link' && next !== 'password') return\n setMethod(next)\n resetTransientState()\n }\n\n const handleMagicLinkSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n\n const trimmed = email.trim().toLowerCase()\n if (!isValidEmail(trimmed)) {\n setError(c.invalidEmailError)\n setState('error')\n return\n }\n\n setState('submitting')\n try {\n const res = await fetch(magicLinkRequestUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: trimmed }),\n })\n\n if (res.ok) {\n setState('magic-link-success')\n setCooldownLeft(cooldownSeconds)\n // Tell any AuthCallback on this page that its verdict about the old\n // link is stale (REB-763). Fired before the SITE's own hook so a SITE\n // that navigates away in `onMagicLinkSubmitted` cannot swallow it.\n emitMagicLinkRequested()\n onMagicLinkSubmitted?.(trimmed)\n return\n }\n\n setState('error')\n setError(c.networkError)\n } catch {\n setState('error')\n setError(c.networkError)\n }\n }\n\n const handlePasswordSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n\n const trimmed = email.trim().toLowerCase()\n if (!isValidEmail(trimmed)) {\n setError(c.invalidEmailError)\n setState('error')\n return\n }\n if (!password) {\n setError(c.invalidCredentialsError)\n setState('error')\n return\n }\n\n setState('submitting')\n try {\n const res = await fetch(passwordLoginUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: trimmed, password }),\n })\n\n const json = (await res.json().catch(() => ({}))) as PasswordLoginResponse\n\n if (!res.ok || !json.data) {\n setState('error')\n setError(json.error ?? c.invalidCredentialsError)\n return\n }\n\n onPasswordSubmitted?.(trimmed)\n\n if (json.data.mfaRequired) {\n setMfa({\n factorId: json.data.factorId ?? '',\n accessToken: json.data.accessToken ?? '',\n refreshToken: json.data.refreshToken ?? '',\n })\n setState('mfa-required')\n return\n }\n\n const exchangeUrl = json.data.sessionExchangeUrl\n if (!exchangeUrl || !isSafeRedirectUrl(exchangeUrl)) {\n setState('error')\n setError(c.networkError)\n return\n }\n window.location.assign(exchangeUrl)\n } catch {\n setState('error')\n setError(c.networkError)\n }\n }\n\n const handleMfaSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n if (!mfa) return\n setError(null)\n setState('submitting')\n\n try {\n const res = await fetch(mfaVerifyUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n factorId: mfa.factorId,\n code: mfaCode,\n accessToken: mfa.accessToken,\n refreshToken: mfa.refreshToken,\n }),\n })\n\n const json = (await res.json().catch(() => ({}))) as MfaVerifyResponse\n\n if (\n !res.ok ||\n !json.data?.sessionExchangeUrl ||\n !isSafeRedirectUrl(json.data.sessionExchangeUrl)\n ) {\n // Stay in mfa-required state so user can retry the code\n setState('mfa-required')\n setError(json.error ?? c.mfaInvalidCodeError)\n setMfaCode('')\n return\n }\n\n onMfaSubmitted?.()\n window.location.assign(json.data.sessionExchangeUrl)\n } catch {\n setState('mfa-required')\n setError(c.networkError)\n }\n }\n\n // MFA prompt replaces the form entirely\n if (state === 'mfa-required' || (mfa && state === 'submitting')) {\n return (\n <div className={cn('space-y-6', className)}>\n <div className='space-y-1'>\n <h2 className='text-xl font-semibold'>{c.mfaTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.mfaMessage}</p>\n </div>\n <form onSubmit={(e) => void handleMfaSubmit(e)} className='space-y-4' noValidate>\n <div className='space-y-2'>\n <Label htmlFor='login-mfa-code'>{c.mfaCodeLabel}</Label>\n <Input\n id='login-mfa-code'\n type='text'\n inputMode='numeric'\n pattern='\\d{6}'\n maxLength={6}\n autoComplete='one-time-code'\n placeholder={c.mfaCodePlaceholder}\n value={mfaCode}\n onChange={(ev) => setMfaCode(ev.target.value.replace(/\\D/g, '').slice(0, 6))}\n disabled={state === 'submitting'}\n aria-describedby={error ? 'login-mfa-error' : undefined}\n autoFocus\n />\n {error && (\n <p id='login-mfa-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button\n type='submit'\n className='w-full'\n disabled={state === 'submitting' || mfaCode.length !== 6}\n >\n {c.mfaSubmitCta}\n </Button>\n <button\n type='button'\n onClick={resetTransientState}\n className='text-muted-foreground hover:text-foreground w-full text-sm transition-colors'\n disabled={state === 'submitting'}\n >\n ← Volver\n </button>\n </form>\n </div>\n )\n }\n\n // Magic-link success replaces the form. It stays in place for the whole\n // cooldown, which is what makes the throttle legible: there is no submit\n // button to press again while Supabase would silently drop the request\n // (REB-762). The countdown is shown to every address alike — existing or\n // not — so it is not an account-existence oracle.\n if (state === 'magic-link-success') {\n return (\n <div className={cn('space-y-3 text-center', className)}>\n <h2 className='text-xl font-semibold' ref={successHeadingRef} tabIndex={-1}>\n {c.magicLinkSuccessTitle}\n </h2>\n <p className='text-muted-foreground'>{c.magicLinkSuccessMessage}</p>\n {/*\n The live region is mounted for the WHOLE panel and stays empty during\n the wait — a region inserted with its content already in place is\n routinely not announced at all (screen readers watch mounted regions\n for changes), so a region that appears together with its text proves\n only that the role is in the DOM. Same shape AuthCallback already\n uses: one always-present `aria-live` wrapper, contents swapped inside.\n */}\n <div role='status' aria-live='polite' className='sr-only'>\n {cooldownLeft > 0 ? '' : c.magicLinkResendCta}\n </div>\n {cooldownLeft > 0 ? (\n // A ticking number is VISUAL. Every text mutation inside a live\n // region is a separate announcement, so marking this one would queue\n // ~60 polite interruptions; the heading above already took focus and\n // spoke the new state once. The only transition worth announcing is\n // the end of the wait, which the region above carries.\n <p className='text-muted-foreground text-sm'>\n {c.magicLinkCooldownMessage.replaceAll('{seconds}', String(cooldownLeft))}\n </p>\n ) : (\n <Button type='button' variant='outline' onClick={handleResend}>\n {c.magicLinkResendCta}\n </Button>\n )}\n </div>\n )\n }\n\n return (\n <div className={cn('space-y-6', className)}>\n <Tabs value={method} onValueChange={handleTabChange}>\n <TabsList className='grid w-full grid-cols-2'>\n <TabsTrigger value='password'>{c.passwordTabLabel}</TabsTrigger>\n <TabsTrigger value='magic-link'>{c.magicLinkTabLabel}</TabsTrigger>\n </TabsList>\n\n <TabsContent value='password' className='pt-4'>\n <form onSubmit={(e) => void handlePasswordSubmit(e)} className='space-y-4' noValidate>\n <div className='space-y-2'>\n <Label htmlFor='login-email-pwd'>{c.emailLabel}</Label>\n <Input\n id='login-email-pwd'\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(ev) => setEmail(ev.target.value)}\n disabled={state === 'submitting'}\n />\n </div>\n <div className='space-y-2'>\n <div className='flex items-center justify-between'>\n <Label htmlFor='login-password'>{c.passwordLabel}</Label>\n <a\n href={resetPasswordUrl}\n className='text-muted-foreground hover:text-foreground text-xs transition-colors'\n >\n {c.forgotPasswordCta}\n </a>\n </div>\n <Input\n id='login-password'\n type='password'\n autoComplete='current-password'\n placeholder={c.passwordPlaceholder}\n value={password}\n onChange={(ev) => setPassword(ev.target.value)}\n disabled={state === 'submitting'}\n aria-describedby={error ? 'login-pwd-error' : undefined}\n />\n {error && (\n <p id='login-pwd-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button type='submit' className='w-full' disabled={state === 'submitting'}>\n {c.passwordSubmitCta}\n </Button>\n </form>\n </TabsContent>\n\n <TabsContent value='magic-link' className='pt-4'>\n <form onSubmit={(e) => void handleMagicLinkSubmit(e)} className='space-y-4' noValidate>\n <div className='space-y-2'>\n <Label htmlFor='login-email-magic'>{c.emailLabel}</Label>\n <Input\n id='login-email-magic'\n ref={magicEmailRef}\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(ev) => setEmail(ev.target.value)}\n disabled={state === 'submitting'}\n aria-describedby={error ? 'login-magic-error' : undefined}\n />\n {error && (\n <p id='login-magic-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button type='submit' className='w-full' disabled={state === 'submitting'}>\n {c.magicLinkSubmitCta}\n </Button>\n </form>\n </TabsContent>\n </Tabs>\n </div>\n )\n}\n","/**\n * Scheme + URL-trick allowlist for a server-returned redirect target\n * (`sessionExchangeUrl`, `redirectTo`, etc.). Shared by `AuthCallback`,\n * `AuthConfirm`, and `SetPassword` — see `CLAUDE.md` Regla A: a predicate\n * applied more than 3 times gets one home, not N inline copies that drift.\n *\n * IMPORTANT — this is NOT full open-redirect confinement: it does NOT\n * restrict absolute `http`/`https` URLs to a specific origin.\n * `isSafeRedirectUrl('https://evil.com')` returns `true`. What it DOES\n * reject:\n * - dangerous schemes (`javascript:`, `data:`, `vbscript:`, ...) — the\n * classic XSS-via-redirect vector\n * - protocol-relative URLs (`//evil.com`) — these start with `/` but\n * browsers resolve them as an absolute, CROSS-ORIGIN URL (the scheme is\n * inherited from the current page), so a naive \"starts with /\" check\n * would wrongly treat them as same-origin\n * - backslash-authority URLs (`/\\evil.com`, `\\\\evil.com`) — browsers\n * normalize `\\` to `/` for special schemes, so these resolve the same way\n * - the same tricks with an ASCII tab/LF/CR spliced between the slashes\n * (`/\\t/evil.com`) — the WHATWG URL parser strips ALL tab/LF/CR from the\n * input BEFORE parsing (anywhere in the string, not just leading/trailing),\n * so `/\\t/evil.com` is indistinguishable from `//evil.com` to the browser\n * even though a naive regex over the raw string wouldn't see two adjacent\n * slashes\n *\n * Full origin-pinning (rejecting any absolute URL whose origin isn't the\n * actual expected one) is a SEPARATE, not-yet-implemented layer. It can't\n * simply pin to `REBLU_API_BASE_URL`: that's a server-only env var\n * unavailable in these `'use client'` components, and per REB-218's\n * subdomain redirect architecture it would be the WRONG origin anyway\n * (production session-exchange lives at `nexus.<tenantDomain>`, not the\n * SITE's own `REBLU_API_BASE_URL`). It would need a dedicated prop\n * conveying the actual expected origin per deploy/routing mode.\n */\nexport function isSafeRedirectUrl(url: string): boolean {\n if (!url) return false\n\n // Match the WHATWG URL parser's own first step: strip ALL ASCII\n // tab/LF/CR from the input, anywhere in the string — not just\n // leading/trailing — before evaluating what the browser will actually\n // resolve. Without this, '/\\t/evil.com' looks like a single-'/' relative\n // path to a naive check but the browser sees '//evil.com'.\n const clean = url.replace(/[\\t\\n\\r]/g, '')\n if (!clean) return false\n\n // A relative path must start with a SINGLE '/' — '//' or '/\\' resolve as\n // an absolute, cross-origin URL in the browser despite \"starting with /\".\n if (/^[/\\\\]{2}/.test(clean)) return false\n\n const scheme = clean.split(':')[0].toLowerCase()\n return ['http', 'https'].includes(scheme) || clean.startsWith('/')\n}\n","/**\n * How long the request form refuses to ask for another magic link (REB-762).\n *\n * ## This number is not ours\n *\n * The throttle that actually fires is Supabase Auth's per-project **\"Minimum\n * interval per user\"** (GoTrue `SMTP_MAX_FREQUENCY`; dashboard →\n * Authentication → Emails → SMTP settings). A second magic link requested\n * inside that window is **dropped before any email is sent**, silently — our\n * route still answers 200, so nothing on the wire says a wait is needed.\n *\n * Measured on the Dermamedicin project on 25-Sep-2026: **60 seconds**. The\n * LOCAL tenant stack sets `max_frequency = \"1s\"`\n * (`packages/tenant-db/supabase/config.toml`, `[auth.email]`), so the\n * authoritative value already differs per environment.\n *\n * ## Why it is mirrored rather than read\n *\n * It cannot be read at runtime from where it is needed. It lives in the\n * tenant's Supabase project config, reachable only through the Management API\n * with a PAT that must never reach a SITE, and our provisioning never writes it\n * (`patchAuthConfig` sets `site_url`, the redirect allow-list, SMTP relay and\n * templates — not the interval), so we do not own a copy to read either.\n *\n * So this is a **mirror of somebody else's setting, declared exactly once**.\n * The two CAN diverge: an operator changing the interval on a Supabase project\n * does not change this constant. When that happens, override it for that SITE\n * through `LoginForm`'s `cooldownSeconds` prop — do not add a second literal.\n *\n * Divergence is not symmetric: a mirror that is too SHORT re-opens the bug (the\n * user retries into a silent drop), while one that is too LONG only makes an\n * honest wait a little longer than it had to be.\n */\nexport const MAGIC_LINK_COOLDOWN_SECONDS = 60\n","/**\n * \"A magic link was just requested\" — the single fact `LoginForm` and\n * `AuthCallback` must share (REB-763).\n *\n * ## Why an event and not a prop\n *\n * A SITE can land on `/auth/callback` with an expired link and re-request from\n * the form rendered on that same page. `AuthCallback` then holds a verdict\n * (\"Enlace caducado\") about a link the user has already replaced, and the\n * expired notice is the louder of the two messages on screen.\n *\n * Both components are published SDK surface and a SITE may render **only one**\n * of them, so neither may require the other. Lifting the state into the page\n * would make every consuming SITE write the wiring (and the existing ones would\n * keep the bug until they did). A DOM event on `window` couples them at zero\n * API cost: rendering one alone is a dispatch nobody listens to, or a listener\n * nobody fires. No prop is added to `AuthCallback`, so an already-published\n * SITE page picks the fix up by upgrading the package alone.\n *\n * `window` rather than a module-scoped emitter on purpose: the `./auth` entry\n * ships both ESM and CJS builds, and a consumer that reaches the two components\n * through different formats would get two module instances — and a listener\n * registered on the wrong one. There is exactly one `window`.\n */\n\n/** Public event name — stable surface; a SITE may dispatch or observe it. */\nexport const MAGIC_LINK_REQUESTED_EVENT = 'reblu:magic-link-requested'\n\n/** Announce that a magic-link request just succeeded. No-op during SSR. */\nexport function emitMagicLinkRequested(): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent(MAGIC_LINK_REQUESTED_EVENT))\n}\n\n/**\n * Observe magic-link requests. Returns an unsubscribe function, so it plugs\n * straight into a `useEffect` cleanup. No-op during SSR.\n */\nexport function onMagicLinkRequested(listener: () => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const handler = () => {\n listener()\n }\n window.addEventListener(MAGIC_LINK_REQUESTED_EVENT, handler)\n return () => {\n window.removeEventListener(MAGIC_LINK_REQUESTED_EVENT, handler)\n }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { Button, cn } from '@reblu/site-ui'\nimport { isSafeRedirectUrl } from './safe-redirect'\nimport { onMagicLinkRequested } from './magic-link-events'\n\nexport interface AuthCallbackCopy {\n loadingTitle: string\n loadingMessage: string\n successMessage: string\n expiredTitle: string\n expiredMessage: string\n expiredCta: string\n networkErrorTitle: string\n networkErrorMessage: string\n networkErrorRetryCta: string\n networkErrorBackCta: string\n malformedTitle: string\n malformedMessage: string\n malformedCta: string\n}\n\nexport interface AuthCallbackProps {\n exchangeUrl: string\n loginUrl: string\n copy?: Partial<AuthCallbackCopy>\n onSuccess?: (redirectTo: string) => void\n onError?: (state: 'expired' | 'network' | 'malformed', error?: Error) => void\n className?: string\n}\n\ntype State =\n | 'mounting'\n | 'parsing'\n | 'exchanging'\n | 'success'\n | 'expired'\n | 'network'\n | 'malformed'\n // The verdict below was about a link the user has since replaced — render\n // nothing, so the request form's own message is the only one on screen\n // (REB-763). Deliberately not a greyed-out or struck-through notice: a stale\n // alarm that is merely dimmed is still read as current.\n | 'stale'\n\n/**\n * Terminal verdicts about the link this page was opened with. All of them are\n * made obsolete by a fresh request, not just the expired one the bug was\n * reported against: an invalid or already-used link lands in 'expired' or\n * 'malformed' depending on what Supabase put in the hash, and a failed exchange\n * lands in 'network'. Clearing the class rather than the reported case is the\n * point — the loading states and 'success' are excluded because a request in\n * flight or a redirect already issued is not a verdict to retract.\n */\nconst TERMINAL_FAILURE_STATES: readonly State[] = ['expired', 'network', 'malformed']\n\nconst DEFAULT_COPY: AuthCallbackCopy = {\n loadingTitle: 'Autenticando...',\n loadingMessage: 'Estamos verificando tu identidad.',\n successMessage: 'Iniciando sesión...',\n expiredTitle: 'Enlace caducado',\n expiredMessage: 'El enlace ha expirado o ya fue utilizado. Solicita uno nuevo.',\n expiredCta: 'Volver al login',\n networkErrorTitle: 'Error de conexión',\n networkErrorMessage: 'No pudimos completar el inicio de sesión. Inténtalo de nuevo.',\n networkErrorRetryCta: 'Reintentar',\n networkErrorBackCta: 'Volver al login',\n malformedTitle: 'Enlace inválido',\n malformedMessage: 'Enlace inválido. Solicita uno nuevo.',\n malformedCta: 'Volver al login',\n}\n\nfunction parseHash(hash: string): { access_token: string; refresh_token: string } | null {\n const raw = hash.startsWith('#') ? hash.slice(1) : hash\n if (!raw) return null\n const params = new URLSearchParams(raw)\n const access_token = params.get('access_token') ?? ''\n const refresh_token = params.get('refresh_token') ?? ''\n const type = params.get('type') ?? ''\n if (!access_token || !refresh_token || type !== 'magiclink') return null\n return { access_token, refresh_token }\n}\n\n/**\n * On failure Supabase redirects to the callback with the reason in the hash\n * fragment (e.g. `#error=access_denied&error_code=otp_expired`), never in the\n * query — the server cannot see it (REB-177). Detect it client-side and tell an\n * expired/used link (recoverable → 'expired') apart from any other failure\n * (→ 'malformed').\n */\nfunction parseHashError(hash: string): 'expired' | 'malformed' | null {\n const raw = hash.startsWith('#') ? hash.slice(1) : hash\n if (!raw) return null\n const params = new URLSearchParams(raw)\n const error = params.get('error')\n const errorCode = params.get('error_code')\n if (!error && !errorCode) return null\n // Expired or already-used magic links come back as access_denied / otp_expired.\n return errorCode === 'otp_expired' || error === 'access_denied' ? 'expired' : 'malformed'\n}\n\nexport function AuthCallback({\n exchangeUrl,\n loginUrl,\n copy,\n onSuccess,\n onError,\n className,\n}: AuthCallbackProps) {\n const [state, setState] = React.useState<State>('mounting')\n const [tokens, setTokens] = React.useState<{\n access_token: string\n refresh_token: string\n } | null>(null)\n const retryRef = React.useRef<HTMLButtonElement>(null)\n const linkRef = React.useRef<HTMLAnchorElement>(null)\n const c = { ...DEFAULT_COPY, ...copy }\n\n const exchange = React.useCallback(\n async (t: { access_token: string; refresh_token: string }) => {\n setState('exchanging')\n try {\n const res = await fetch(exchangeUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(t),\n })\n\n if (res.ok) {\n const raw = (await res.json()) as unknown\n const redirectTo =\n raw !== null &&\n typeof raw === 'object' &&\n 'redirectTo' in raw &&\n typeof (raw as { redirectTo: unknown }).redirectTo === 'string'\n ? (raw as { redirectTo: string }).redirectTo\n : null\n\n if (!redirectTo) {\n setState('network')\n onError?.('network', undefined)\n return\n }\n\n // Guard against dangerous schemes (javascript:, data:, etc.) AND\n // protocol-relative/backslash-authority URLs (//evil.com, /\\evil.com)\n // — see safe-redirect.ts.\n if (!isSafeRedirectUrl(redirectTo)) {\n setState('network')\n onError?.('network', undefined)\n return\n }\n\n history.replaceState({}, '', window.location.pathname)\n window.location.replace(redirectTo)\n setState('success')\n onSuccess?.(redirectTo)\n return\n }\n\n if (res.status === 401) {\n setState('expired')\n onError?.('expired')\n return\n }\n\n setState('network')\n onError?.('network', undefined)\n } catch (err) {\n setState('network')\n onError?.('network', err instanceof Error ? err : undefined)\n }\n },\n [exchangeUrl, onError, onSuccess],\n )\n\n React.useEffect(() => {\n setState('parsing')\n // A failure reported in the hash (expired/used/invalid link) is not a token\n // to exchange — surface it directly without hitting the network (REB-177).\n const hashError = parseHashError(window.location.hash)\n if (hashError) {\n setState(hashError)\n onError?.(hashError)\n return\n }\n const parsed = parseHash(window.location.hash)\n if (!parsed) {\n setState('malformed')\n onError?.('malformed')\n return\n }\n setTokens(parsed)\n void exchange(parsed)\n // `exchange`/`onError` are intentionally excluded: this effect must run\n // EXACTLY ONCE on mount to parse `window.location.hash` — re-running it\n // on every `exchange` identity change (which itself changes whenever\n // `onSuccess`/`onError` props change identity) would re-parse an\n // already-cleared hash and misfire.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n React.useEffect(() => {\n if (state === 'network') retryRef.current?.focus()\n if (state === 'expired' || state === 'malformed') linkRef.current?.focus()\n }, [state])\n\n // A new link was just requested — from the form a SITE may render on this very\n // page — so any verdict about the OLD link is stale and must go (REB-763).\n // Subscribing costs nothing when the SITE renders this component alone.\n React.useEffect(\n () =>\n onMagicLinkRequested(() => {\n setState((current) => (TERMINAL_FAILURE_STATES.includes(current) ? 'stale' : current))\n }),\n [],\n )\n\n const isLoading = state === 'mounting' || state === 'parsing' || state === 'exchanging'\n\n // Render NOTHING, not an empty screen: the wrapper is `min-h-screen`, so\n // keeping it would leave a full blank viewport where the retracted notice\n // used to be and push the sibling request form's confirmation below the fold\n // — the user would watch the thing they were reading disappear and see no\n // replacement (REB-763).\n if (state === 'stale') return null\n\n return (\n <main className={cn('flex min-h-screen items-center justify-center', className)}>\n <div aria-live='polite' className='w-full max-w-md space-y-4 p-6 text-center'>\n {isLoading && (\n <section>\n <h2 className='text-xl font-semibold'>{c.loadingTitle}</h2>\n <p className='text-muted-foreground mt-2'>{c.loadingMessage}</p>\n </section>\n )}\n\n {state === 'success' && (\n <section>\n <p>{c.successMessage}</p>\n </section>\n )}\n\n {state === 'expired' && (\n <section className='space-y-4'>\n <h2 className='text-xl font-semibold'>{c.expiredTitle}</h2>\n <p className='text-muted-foreground'>{c.expiredMessage}</p>\n <Button asChild variant='outline'>\n <a href={loginUrl} ref={linkRef}>\n {c.expiredCta}\n </a>\n </Button>\n </section>\n )}\n\n {state === 'network' && (\n <section className='space-y-4'>\n <h2 className='text-xl font-semibold'>{c.networkErrorTitle}</h2>\n <p className='text-muted-foreground'>{c.networkErrorMessage}</p>\n <div className='flex flex-col gap-2'>\n <Button type='button' ref={retryRef} onClick={() => tokens && void exchange(tokens)}>\n {c.networkErrorRetryCta}\n </Button>\n <Button asChild variant='outline'>\n <a href={loginUrl}>{c.networkErrorBackCta}</a>\n </Button>\n </div>\n </section>\n )}\n\n {state === 'malformed' && (\n <section className='space-y-4'>\n <h2 className='text-xl font-semibold'>{c.malformedTitle}</h2>\n <p className='text-muted-foreground'>{c.malformedMessage}</p>\n <Button asChild variant='outline'>\n <a href={loginUrl} ref={linkRef}>\n {c.malformedCta}\n </a>\n </Button>\n </section>\n )}\n </div>\n </main>\n )\n}\n","/**\n * Maps the raw `?error=` query param that Supabase appends when a magic link is\n * already consumed or expired to a user-facing Spanish message.\n *\n * Supabase redirects to /login?error=... with a raw English message (e.g.\n * \"Email link is invalid or has expired\"). Showing that verbatim is confusing,\n * so we translate the common cases and signal whether the \"Solicitar otro\n * enlace\" CTA should be offered.\n *\n * Matching is case-insensitive and whitespace-tolerant so it survives small\n * variations in how the error text arrives.\n *\n * Relocated from apps/demo-site/src/lib/utils/login-errors.ts (REB-510 slice\n * 2a) as a copy-injectable helper — Spanish defaults are unchanged, and a SITE\n * MAY override individual messages via `Partial<LoginErrorCopy>`.\n */\n\nexport interface LoginErrorInfo {\n /** Message shown in the error card. */\n message: string\n /** Whether to show the \"Solicitar otro enlace\" CTA. */\n showRequestLink: boolean\n}\n\nexport interface LoginErrorCopy {\n expiredMessage: string\n missingTokenMessage: string\n genericMessage: string\n}\n\nconst DEFAULT_COPY: LoginErrorCopy = {\n expiredMessage: 'El enlace ha expirado o ya fue utilizado.',\n missingTokenMessage: 'El enlace no es válido. Solicita uno nuevo.',\n genericMessage: 'No se pudo completar el inicio de sesión. Intenta de nuevo.',\n}\n\nexport function mapLoginError(\n error: string | null | undefined,\n copy?: Partial<LoginErrorCopy>,\n): LoginErrorInfo | null {\n if (!error) return null\n\n const normalized = error.trim().toLowerCase()\n if (normalized === '') return null\n\n const c = { ...DEFAULT_COPY, ...copy }\n\n if (\n normalized.includes('email link is invalid or has expired') ||\n normalized.includes('token has expired or is invalid')\n ) {\n return { message: c.expiredMessage, showRequestLink: true }\n }\n\n if (normalized.includes('missing_token')) {\n return { message: c.missingTokenMessage, showRequestLink: true }\n }\n\n return { message: c.genericMessage, showRequestLink: true }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useTheme } from 'next-themes'\nimport { cn } from '@reblu/site-ui'\n\nexport interface AuthLogoCopy {\n /**\n * Overrides the rendered `<img>` alt text. Pass `''` explicitly to mark the\n * logo as decorative (e.g. when an adjacent heading already announces\n * `siteName`, so screen readers shouldn't read it twice) — `undefined`\n * (the key simply omitted from `copy`) is what falls back to `siteName`.\n */\n altText?: string\n /** Shown when neither a logo URL nor `siteName` is available. */\n fallbackLabel: string\n}\n\nexport interface AuthLogoProps {\n logoLightExpandedUrl: string | null\n logoDarkExpandedUrl: string | null\n logoLightSmallUrl: string | null\n logoDarkSmallUrl: string | null\n siteName: string\n /** @default 'expanded' */\n variant?: 'expanded' | 'compact'\n className?: string\n copy?: Partial<AuthLogoCopy>\n /**\n * Heading level for the `siteName`/fallback-label text rendered when no logo\n * resolves. AuthLogo owns the auth page's heading, so this defaults to the\n * top-level `1`; pass a deeper level only if the page already has an `<h1>`\n * elsewhere that AuthLogo must sit beneath.\n * @default 1\n */\n headingLevel?: 1 | 2 | 3 | 4 | 5 | 6\n /**\n * When `true`, renders `null` instead of the `siteName`/fallback-label\n * heading whenever no logo URL resolves for the active variant+theme, rather\n * than showing the text. Use this ONLY when the consumer intentionally\n * renders `siteName` elsewhere itself — by default AuthLogo IS the heading\n * (\"logo XOR name\"), so the consumer should NOT add its own\n * `<h1>{siteName}</h1>`.\n * @default false\n */\n hideTextFallback?: boolean\n}\n\nconst DEFAULT_COPY: Required<Pick<AuthLogoCopy, 'fallbackLabel'>> = {\n fallbackLabel: 'Inicio de sesión',\n}\n\nconst VARIANT_CLASS: Record<'expanded' | 'compact', string> = {\n expanded: 'h-8 w-auto',\n compact: 'h-8 w-8',\n}\n\n// Intrinsic `width`/`height` attributes so the browser reserves layout space\n// via its built-in aspect-ratio-from-attributes heuristic before the real\n// image loads (avoids vertical CLS at minimum; horizontal width still\n// depends on the tenant's actual logo aspect ratio, which `/api/site/config`\n// doesn't expose — an arbitrary-but-reasonable box, not a promise of zero\n// shift). `VARIANT_CLASS`'s `h-8` always wins for height regardless.\nconst VARIANT_INTRINSIC_SIZE: Record<'expanded' | 'compact', { width: number; height: number }> = {\n expanded: { width: 160, height: 32 },\n compact: { width: 32, height: 32 },\n}\n\n/**\n * Dynamic tenant logo for auth pages (REB-510 R1). Resolves expanded/compact ×\n * light/dark from the 4 `/api/site/config` logo URLs, switching light/dark\n * CLIENT-SIDE via `next-themes`. Falls back to `siteName` text, then a generic\n * `copy.fallbackLabel`, whenever the resolved URL for the active\n * variant+theme combination is `null` — it never silently swaps in the OTHER\n * theme's logo.\n *\n * Hydration-safe: `next-themes`' `resolvedTheme` is `undefined` until it\n * mounts client-side, so pre-mount `isDark` is always `false` — the same\n * light-first markup the server rendered, avoiding a hydration mismatch.\n *\n * Renders a plain `<img>`, not `next/image`: this is a distributable SDK\n * consumed by external SITE repos, so it cannot assume the consuming app's\n * `next.config` `images.remotePatterns` allowlists the tenant's logo CDN.\n */\nexport function AuthLogo(props: AuthLogoProps) {\n const {\n logoLightExpandedUrl,\n logoDarkExpandedUrl,\n logoLightSmallUrl,\n logoDarkSmallUrl,\n siteName,\n variant = 'expanded',\n className,\n copy,\n headingLevel = 1,\n hideTextFallback = false,\n } = props\n const fallbackLabel = copy?.fallbackLabel ?? DEFAULT_COPY.fallbackLabel\n const { resolvedTheme } = useTheme()\n const isDark = resolvedTheme === 'dark'\n\n const lightUrl = variant === 'compact' ? logoLightSmallUrl : logoLightExpandedUrl\n const darkUrl = variant === 'compact' ? logoDarkSmallUrl : logoDarkExpandedUrl\n const logoUrl = isDark ? darkUrl : lightUrl\n\n // Tracks a broken/stale logo URL (404, CDN outage, etc.) — degrades to the\n // text fallback instead of a broken-image glyph. Reset whenever `logoUrl`\n // itself changes (e.g. a theme toggle resolves a DIFFERENT URL) so a past\n // failure doesn't permanently block a future, valid attempt.\n const [imgError, setImgError] = React.useState(false)\n React.useEffect(() => {\n setImgError(false)\n }, [logoUrl])\n\n const fallbackText = siteName || fallbackLabel\n // `copy.altText` — a VALUE-defined key (including `''`) wins outright,\n // that's how a consumer marks the logo decorative. A value-undefined key\n // (whether omitted from `copy` entirely, or present with an explicit\n // `undefined` value) falls back to siteName/fallbackLabel — checking value\n // definedness, not `'altText' in copy` key presence, is what makes\n // `copy={{ altText: undefined }}` behave the same as omitting the key.\n const altText = copy?.altText !== undefined ? copy.altText : siteName || fallbackLabel\n\n // AuthLogo is \"logo XOR name\": exactly ONE of the brand mark (logo image) or\n // the `siteName` text is shown, never both — and by default AuthLogo IS the\n // auth page's heading, so the consuming page must NOT add its own separate\n // `<h1>{siteName}</h1>` (which is what double-rendered the name before).\n const Heading = `h${headingLevel}` as const\n\n if (logoUrl && !imgError) {\n const { width, height } = VARIANT_INTRINSIC_SIZE[variant]\n // Decorative escape hatch: when the consumer hides the text fallback OR\n // marks the logo decorative (`alt=''`), it owns the page heading itself, so\n // render a bare image — wrapping a nameless image in a heading would emit\n // an empty, accessible-name-less heading.\n const bare = hideTextFallback || altText === ''\n const image = (\n <img\n src={logoUrl}\n alt={altText}\n width={width}\n height={height}\n className={cn(VARIANT_CLASS[variant], 'max-w-full', bare && className)}\n onError={() => setImgError(true)}\n />\n )\n if (bare) return image\n // Default: wrap the logo in the heading so the auth page ALWAYS has a\n // heading — even when only the brand image is visible — whose accessible\n // name is the logo's `alt` (= `siteName`). No visible, duplicated name text.\n return <Heading className={cn('inline-flex items-center max-w-full', className)}>{image}</Heading>\n }\n\n if (hideTextFallback) return null\n\n // No logo resolved → render `siteName` (or the generic fallback label) as the\n // page's heading text.\n return (\n <Heading className={cn('text-2xl font-bold tracking-tight text-foreground', className)}>\n {fallbackText}\n </Heading>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\n\nexport interface ResetPasswordRequestCopy {\n emailLabel: string\n emailPlaceholder: string\n submitCta: string\n submittingCta: string\n successTitle: string\n successMessage: string\n backToLoginCta: string\n invalidEmailError: string\n networkError: string\n}\n\nexport interface ResetPasswordRequestProps {\n requestUrl: string\n loginUrl: string\n copy?: Partial<ResetPasswordRequestCopy>\n className?: string\n onSubmitted?: (email: string) => void\n}\n\nconst DEFAULT_COPY: ResetPasswordRequestCopy = {\n emailLabel: 'Correo electrónico',\n emailPlaceholder: 'tu@email.com',\n submitCta: 'Enviar enlace de recuperación',\n submittingCta: 'Enviando...',\n successTitle: 'Revisa tu email',\n successMessage:\n 'Si el email existe en nuestra plataforma, recibirás un enlace para restablecer tu contraseña en los próximos minutos.',\n backToLoginCta: '← Volver al inicio de sesión',\n invalidEmailError: 'Introduce un correo electrónico válido.',\n networkError: 'Error de conexión. Por favor, inténtalo de nuevo.',\n}\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Step 1 of the password-reset flow — anti-enumeration: always shows the same\n * generic success message on a 2xx response, regardless of whether the email\n * exists (the backend never leaks that).\n */\nexport function ResetPasswordRequest(props: ResetPasswordRequestProps) {\n const { requestUrl, loginUrl, copy, className, onSubmitted } = props\n const c = { ...DEFAULT_COPY, ...copy }\n\n const [email, setEmail] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [submitted, setSubmitted] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n\n const trimmed = email.trim().toLowerCase()\n if (!EMAIL_REGEX.test(trimmed)) {\n setError(c.invalidEmailError)\n return\n }\n\n setLoading(true)\n // `succeeded` isolates the consumer's `onSubmitted` callback from this\n // try/catch: if we called it INSIDE the try and it threw, that throw\n // would land in the network `catch` below and misreport a successful\n // request as a connection error.\n let succeeded = false\n try {\n const res = await fetch(requestUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: trimmed }),\n })\n\n if (!res.ok) {\n const json = (await res.json().catch(() => ({}))) as { error?: string }\n setError(json.error ?? c.networkError)\n setLoading(false)\n return\n }\n\n succeeded = true\n setSubmitted(true)\n setLoading(false)\n } catch {\n setError(c.networkError)\n setLoading(false)\n return\n }\n\n if (succeeded) {\n try {\n onSubmitted?.(trimmed)\n } catch (err) {\n console.error('[ResetPasswordRequest] onSubmitted callback threw:', err)\n }\n }\n }\n\n if (submitted) {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.successTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.successMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.backToLoginCta}\n </a>\n </div>\n )\n }\n\n return (\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className={cn('space-y-5', className)}\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='reset-request-email'>{c.emailLabel}</Label>\n <Input\n id='reset-request-email'\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n disabled={loading}\n aria-describedby={error ? 'reset-request-error' : undefined}\n autoFocus\n />\n {error && (\n <p id='reset-request-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n </div>\n <Button type='submit' className='w-full' disabled={loading}>\n {loading ? c.submittingCta : c.submitCta}\n </Button>\n <div className='text-center'>\n <a\n href={loginUrl}\n className='text-muted-foreground hover:text-foreground text-sm transition-colors'\n >\n {c.backToLoginCta}\n </a>\n </div>\n </form>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\n\n/**\n * Appends `key=value` to `url` without producing a malformed double `?` when\n * `url` (consumer-supplied `loginUrl`, e.g. `/login?tenant=acme`) already\n * carries a query string. Deliberately NOT `new URL(url)` — `loginUrl` is\n * typically a relative path and the WHATWG `URL` constructor requires a base\n * for those.\n */\nfunction withQueryParam(url: string, key: string, value: string): string {\n const separator = url.includes('?') ? '&' : '?'\n return `${url}${separator}${key}=${encodeURIComponent(value)}`\n}\n\nexport interface ResetPasswordConfirmCopy {\n passwordLabel: string\n passwordPlaceholder: string\n confirmPasswordLabel: string\n confirmPasswordPlaceholder: string\n mfaCodeLabel: string\n mfaCodePlaceholder: string\n mfaHelpMessage: string\n submitCta: string\n submittingCta: string\n passwordTooShortError: string\n passwordMismatchError: string\n mfaInvalidCodeError: string\n genericError: string\n missingTokensTitle: string\n missingTokensMessage: string\n requestNewLinkCta: string\n}\n\nexport interface ResetPasswordConfirmProps {\n confirmUrl: string\n loginUrl: string\n resetRequestUrl: string\n copy?: Partial<ResetPasswordConfirmCopy>\n className?: string\n onSuccess?: () => void\n}\n\nconst DEFAULT_COPY: ResetPasswordConfirmCopy = {\n passwordLabel: 'Nueva contraseña',\n passwordPlaceholder: 'Mínimo 8 caracteres',\n confirmPasswordLabel: 'Confirmar contraseña',\n confirmPasswordPlaceholder: 'Repite la contraseña',\n mfaCodeLabel: 'Código de verificación (2FA)',\n mfaCodePlaceholder: '6 dígitos',\n mfaHelpMessage:\n 'Tu cuenta tiene verificación en dos pasos. Introduce el código de tu app de autenticación para completar el cambio.',\n submitCta: 'Establecer nueva contraseña',\n submittingCta: 'Actualizando contraseña...',\n passwordTooShortError: 'La contraseña debe tener al menos 8 caracteres',\n passwordMismatchError: 'Las contraseñas no coinciden',\n mfaInvalidCodeError: 'Introduce el código de 6 dígitos de tu app de autenticación',\n genericError: 'Error al restablecer la contraseña',\n missingTokensTitle: 'Enlace inválido',\n missingTokensMessage: 'El enlace ha expirado o no es válido. Solicita uno nuevo.',\n requestNewLinkCta: 'Solicitar nuevo enlace',\n}\n\n/**\n * Step 2 of the password-reset flow. Reads `access_token`/`refresh_token`\n * straight off `useSearchParams` — no `window.location.search` + effect-timing\n * workaround needed here because, unlike a page-level default export, this is\n * a component the SITE mounts inside its own `<Suspense>` boundary.\n *\n * REB-378: when the account has a verified MFA factor, the recovery session\n * (AAL1) cannot change the password on its own. The backend responds\n * `{ data: { mfaRequired: true } }` and this reveals a 6-digit TOTP field so\n * the user can resubmit with the code (the recovery tokens stay valid).\n */\nexport function ResetPasswordConfirm(props: ResetPasswordConfirmProps) {\n const { confirmUrl, loginUrl, resetRequestUrl, copy, className, onSuccess } = props\n const c = { ...DEFAULT_COPY, ...copy }\n const router = useRouter()\n const searchParams = useSearchParams()\n\n const accessToken = searchParams.get('access_token')\n const refreshToken = searchParams.get('refresh_token')\n const hasTokens = Boolean(accessToken && refreshToken)\n\n const [password, setPassword] = React.useState('')\n const [confirmPassword, setConfirmPassword] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [mfaRequired, setMfaRequired] = React.useState(false)\n const [mfaCode, setMfaCode] = React.useState('')\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n if (!hasTokens) return\n\n if (password.length < 8) {\n setError(c.passwordTooShortError)\n return\n }\n if (password !== confirmPassword) {\n setError(c.passwordMismatchError)\n return\n }\n if (mfaRequired && !/^\\d{6}$/.test(mfaCode)) {\n setError(c.mfaInvalidCodeError)\n return\n }\n\n setLoading(true)\n // `succeeded` isolates the consumer's `onSuccess` callback (and the\n // navigation) from this try/catch: calling either INSIDE the try means a\n // throwing callback lands in the network `catch` below and misreports an\n // already-successful password change as a failure — the user retries\n // with recovery tokens the backend has already consumed.\n let succeeded = false\n try {\n const res = await fetch(confirmUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n access_token: accessToken,\n refresh_token: refreshToken,\n new_password: password,\n ...(mfaRequired ? { mfa_code: mfaCode } : {}),\n }),\n })\n\n const json = (await res.json().catch(() => ({}))) as {\n error?: string\n data?: { success?: boolean; mfaRequired?: boolean }\n }\n\n if (!res.ok) {\n setError(json.error ?? c.genericError)\n setLoading(false)\n return\n }\n\n if (json.data?.mfaRequired) {\n setMfaRequired(true)\n setLoading(false)\n return\n }\n\n succeeded = true\n setLoading(false)\n } catch {\n setError(c.genericError)\n setLoading(false)\n return\n }\n\n if (succeeded) {\n // Navigate FIRST, consumer callback SECOND (isolated in its own\n // try/catch) — same reasoning as set-password.tsx: a throwing\n // `onSuccess` can no longer block the redirect or escape as an\n // unhandled rejection out of this fire-and-forget async handler.\n router.replace(withQueryParam(loginUrl, 'reset', 'success'))\n try {\n onSuccess?.()\n } catch (err) {\n console.error('[ResetPasswordConfirm] onSuccess callback threw:', err)\n }\n }\n }\n\n if (!hasTokens) {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.missingTokensTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.missingTokensMessage}</p>\n <a href={resetRequestUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.requestNewLinkCta}\n </a>\n </div>\n )\n }\n\n return (\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className={cn('space-y-5', className)}\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='reset-confirm-password'>{c.passwordLabel}</Label>\n <Input\n id='reset-confirm-password'\n type='password'\n autoComplete='new-password'\n placeholder={c.passwordPlaceholder}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'reset-confirm-error' : undefined}\n autoFocus\n />\n </div>\n <div className='space-y-1.5'>\n <Label htmlFor='reset-confirm-password-2'>{c.confirmPasswordLabel}</Label>\n <Input\n id='reset-confirm-password-2'\n type='password'\n autoComplete='new-password'\n placeholder={c.confirmPasswordPlaceholder}\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'reset-confirm-error' : undefined}\n />\n </div>\n {mfaRequired && (\n <div className='space-y-1.5'>\n <Label htmlFor='reset-confirm-mfa'>{c.mfaCodeLabel}</Label>\n <Input\n id='reset-confirm-mfa'\n type='text'\n inputMode='numeric'\n autoComplete='one-time-code'\n placeholder={c.mfaCodePlaceholder}\n value={mfaCode}\n onChange={(e) => setMfaCode(e.target.value.replace(/\\D/g, '').slice(0, 6))}\n disabled={loading}\n maxLength={6}\n aria-describedby={error ? 'reset-confirm-error' : undefined}\n autoFocus\n />\n <p className='text-muted-foreground text-xs'>{c.mfaHelpMessage}</p>\n </div>\n )}\n {error && (\n <p id='reset-confirm-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n <Button\n type='submit'\n className='w-full'\n disabled={\n loading || !password || !confirmPassword || (mfaRequired && mfaCode.length !== 6)\n }\n >\n {loading ? c.submittingCta : c.submitCta}\n </Button>\n </form>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\nimport { isSafeRedirectUrl } from './safe-redirect'\n\nexport interface SetPasswordCopy {\n passwordLabel: string\n passwordPlaceholder: string\n confirmPasswordLabel: string\n confirmPasswordPlaceholder: string\n submitCta: string\n submittingCta: string\n passwordTooShortError: string\n passwordMismatchError: string\n genericError: string\n missingTokenTitle: string\n missingTokenMessage: string\n backToLoginCta: string\n}\n\nexport interface SetPasswordProps {\n submitUrl: string\n loginUrl: string\n copy?: Partial<SetPasswordCopy>\n className?: string\n onSuccess?: (sessionExchangeUrl: string) => void\n}\n\nconst DEFAULT_COPY: SetPasswordCopy = {\n passwordLabel: 'Contraseña',\n passwordPlaceholder: 'Mínimo 8 caracteres',\n confirmPasswordLabel: 'Confirmar contraseña',\n confirmPasswordPlaceholder: 'Repite la contraseña',\n submitCta: 'Activar cuenta',\n submittingCta: 'Activando cuenta...',\n passwordTooShortError: 'La contraseña debe tener al menos 8 caracteres',\n passwordMismatchError: 'Las contraseñas no coinciden',\n genericError: 'Error al establecer la contraseña',\n missingTokenTitle: 'Enlace inválido',\n missingTokenMessage: 'El enlace de invitación ha expirado o no es válido.',\n backToLoginCta: '← Volver al inicio de sesión',\n}\n\n/**\n * Invite flow — for a user setting their initial password. Reads\n * `session_exchange_token` off `useSearchParams`; on success, redirects via\n * `window.location.assign` (NOT the Next router) because the returned\n * `sessionExchangeUrl` points at base-api's own domain to set cookies, not a\n * same-origin SITE route.\n */\nexport function SetPassword(props: SetPasswordProps) {\n const { submitUrl, loginUrl, copy, className, onSuccess } = props\n const c = { ...DEFAULT_COPY, ...copy }\n const searchParams = useSearchParams()\n const sessionExchangeToken = searchParams.get('session_exchange_token')\n\n const [password, setPassword] = React.useState('')\n const [confirmPassword, setConfirmPassword] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n setError(null)\n if (!sessionExchangeToken) return\n\n if (password.length < 8) {\n setError(c.passwordTooShortError)\n return\n }\n if (password !== confirmPassword) {\n setError(c.passwordMismatchError)\n return\n }\n\n setLoading(true)\n // `sessionExchangeUrl` isolates the consumer's `onSuccess` callback (and\n // the actual navigation) from this try/catch: calling either INSIDE the\n // try means a throwing callback lands in the network `catch` below and\n // misreports an already-successful password-set as a connection error —\n // stranding an activated user on a form that tells them it failed.\n let sessionExchangeUrl: string | null = null\n try {\n const res = await fetch(submitUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n session_exchange_token: sessionExchangeToken,\n new_password: password,\n }),\n })\n\n const json = (await res.json().catch(() => ({}))) as {\n data?: { sessionExchangeUrl?: string }\n error?: string\n }\n\n if (!res.ok || !json.data?.sessionExchangeUrl) {\n setError(json.error ?? c.genericError)\n setLoading(false)\n return\n }\n\n if (!isSafeRedirectUrl(json.data.sessionExchangeUrl)) {\n setError(c.genericError)\n setLoading(false)\n return\n }\n\n sessionExchangeUrl = json.data.sessionExchangeUrl\n } catch {\n setError(c.genericError)\n setLoading(false)\n return\n }\n\n // Navigate FIRST, consumer callback SECOND (isolated in its own\n // try/catch): if `onSuccess` throws (ad blocker on an analytics call,\n // etc.) the navigation has already been triggered and is unaffected, and\n // the throw doesn't escape as an unhandled rejection out of this\n // fire-and-forget async handler — the user is never stranded on this\n // form by code that isn't even this component's own.\n window.location.assign(sessionExchangeUrl)\n try {\n onSuccess?.(sessionExchangeUrl)\n } catch (err) {\n console.error('[SetPassword] onSuccess callback threw:', err)\n }\n }\n\n if (!sessionExchangeToken) {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.missingTokenTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.missingTokenMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.backToLoginCta}\n </a>\n </div>\n )\n }\n\n return (\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className={cn('space-y-5', className)}\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='set-password-password'>{c.passwordLabel}</Label>\n <Input\n id='set-password-password'\n type='password'\n autoComplete='new-password'\n placeholder={c.passwordPlaceholder}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'set-password-error' : undefined}\n // Single-purpose activation page (this IS the whole page's task,\n // there's no earlier content competing for attention) — same\n // pattern as reset-password-confirm.tsx/reset-password-request.tsx.\n autoFocus\n />\n </div>\n <div className='space-y-1.5'>\n <Label htmlFor='set-password-confirm'>{c.confirmPasswordLabel}</Label>\n <Input\n id='set-password-confirm'\n type='password'\n autoComplete='new-password'\n placeholder={c.confirmPasswordPlaceholder}\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n disabled={loading}\n minLength={8}\n aria-describedby={error ? 'set-password-error' : undefined}\n />\n </div>\n {error && (\n <p id='set-password-error' role='alert' className='text-destructive text-sm'>\n {error}\n </p>\n )}\n <Button type='submit' className='w-full' disabled={loading || !password || !confirmPassword}>\n {loading ? c.submittingCta : c.submitCta}\n </Button>\n </form>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport { cn } from '@reblu/site-ui'\nimport { isSafeRedirectUrl } from './safe-redirect'\n\nexport interface AuthConfirmCopy {\n loadingMessage: string\n successTitle: string\n successMessage: string\n successCta: string\n errorTitle: string\n errorMessage: string\n errorCta: string\n}\n\nexport interface AuthConfirmProps {\n confirmUrl: string\n loginUrl: string\n copy?: Partial<AuthConfirmCopy>\n className?: string\n}\n\nconst DEFAULT_COPY: AuthConfirmCopy = {\n loadingMessage: 'Confirmando tu email...',\n successTitle: 'Email confirmado',\n successMessage: 'Tu dirección de email ha sido verificada correctamente.',\n successCta: 'Iniciar sesión',\n errorTitle: 'Error al confirmar',\n errorMessage: 'El enlace de confirmación no es válido o ha expirado.',\n errorCta: 'Volver al inicio de sesión',\n}\n\ntype Status = 'loading' | 'success' | 'error'\n\n/**\n * Dual-mode email confirmation (signup / email-change):\n *\n * 1. `?success=true` — base-api already ran `verifyOtp` server-side and\n * redirected here. Shows the confirmed message with no API call.\n * 2. `?token_hash=&type=` — client-initiated verify: POSTs to `confirmUrl`\n * and follows the returned `sessionExchangeUrl`, or shows success/error\n * copy based on the response.\n *\n * Neither param present → shown as an error state with a link back to login,\n * rather than a silent client-side redirect.\n */\nexport function AuthConfirm(props: AuthConfirmProps) {\n const { confirmUrl, loginUrl, copy, className } = props\n const c = { ...DEFAULT_COPY, ...copy }\n const searchParams = useSearchParams()\n\n const success = searchParams.get('success')\n const tokenHash = searchParams.get('token_hash')\n const type = searchParams.get('type')\n\n const [status, setStatus] = React.useState<Status>(success === 'true' ? 'success' : 'loading')\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (success === 'true') {\n setStatus('success')\n return\n }\n\n if (tokenHash && type) {\n // `token_hash` is single-use server-side — verifying it twice makes\n // the SECOND call fail even though the first already succeeded. React\n // Strict Mode's dev-only mount→cleanup→mount double-invoke would\n // otherwise fire this fetch twice for the same token. Aborting the\n // in-flight request on cleanup stops the CLIENT from processing that\n // first response and swallows its `AbortError` below instead of\n // surfacing a false \"confirmation failed\" — but `abort()` cannot\n // retroactively un-send a request that already reached base-api\n // before cleanup ran, so this reduces rather than deterministically\n // eliminates the double-POST race (in practice React's dev\n // double-invoke runs both phases synchronously in the same commit, so\n // the abort typically wins, but that is not a spec guarantee). A\n // fully deterministic fix needs server-side idempotency on\n // `verify-otp`, which is out of this package's control. The same\n // abort also guards a genuine unmount mid-request.\n const controller = new AbortController()\n setStatus('loading')\n fetch(confirmUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token_hash: tokenHash, type }),\n signal: controller.signal,\n })\n .then(async (res) => {\n const json = (await res.json().catch(() => ({}))) as {\n data?: { sessionExchangeUrl?: string; success?: boolean }\n error?: string\n }\n\n if (!res.ok) {\n setStatus('error')\n setErrorMessage(json.error ?? c.errorMessage)\n return\n }\n\n if (json.data?.sessionExchangeUrl) {\n if (!isSafeRedirectUrl(json.data.sessionExchangeUrl)) {\n setStatus('error')\n setErrorMessage(c.errorMessage)\n return\n }\n window.location.assign(json.data.sessionExchangeUrl)\n return\n }\n\n setStatus('success')\n })\n .catch((err: unknown) => {\n if (err instanceof Error && err.name === 'AbortError') return\n setStatus('error')\n setErrorMessage(c.errorMessage)\n })\n\n return () => controller.abort()\n }\n\n // Neither success=true nor token_hash+type — nothing to confirm.\n setStatus('error')\n // `c.errorMessage` (derived from the `copy` prop) is intentionally\n // excluded: re-running this effect on every `copy` object identity\n // change would re-fire the fetch. The URL params are the only real\n // trigger for this effect.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [success, tokenHash, type, confirmUrl])\n\n if (status === 'loading') {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <p className='text-muted-foreground text-sm'>{c.loadingMessage}</p>\n </div>\n )\n }\n\n if (status === 'error') {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.errorTitle}</h2>\n <p className='text-muted-foreground text-sm'>{errorMessage ?? c.errorMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.errorCta}\n </a>\n </div>\n )\n }\n\n return (\n <div className={cn('space-y-4 text-center', className)}>\n <h2 className='text-lg font-semibold'>{c.successTitle}</h2>\n <p className='text-muted-foreground text-sm'>{c.successMessage}</p>\n <a href={loginUrl} className='text-primary inline-block text-sm hover:underline'>\n {c.successCta}\n </a>\n </div>\n )\n}\n"],"mappings":"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","names":["React","Button","cn","jsx","jsxs","DEFAULT_COPY","cn","Button","DEFAULT_COPY","React","cn","jsx","DEFAULT_COPY","React","Button","Input","cn","Label","jsx","jsxs","DEFAULT_COPY","EMAIL_REGEX","React","Button","Input","cn","Label","jsx","jsxs","DEFAULT_COPY","React","useSearchParams","Button","Input","cn","Label","jsx","jsxs","DEFAULT_COPY","useSearchParams","cn","Label","Input","Button","React","useSearchParams","cn","jsx","jsxs","DEFAULT_COPY","useSearchParams","cn"]}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/recaptcha.ts
21
+ var recaptcha_exports = {};
22
+ __export(recaptcha_exports, {
23
+ CONTACT_FORM_OPERATIONAL_MODES: () => CONTACT_FORM_OPERATIONAL_MODES,
24
+ RECAPTCHA_ACTION: () => RECAPTCHA_ACTION,
25
+ RECAPTCHA_SITE_KEY_PATTERN: () => RECAPTCHA_SITE_KEY_PATTERN,
26
+ deferredRecaptchaKey: () => deferredRecaptchaKey,
27
+ isContactFormAvailable: () => isContactFormAvailable,
28
+ isSafeRecaptchaSiteKey: () => isSafeRecaptchaSiteKey,
29
+ resolveContactFormAvailability: () => resolveContactFormAvailability
30
+ });
31
+ module.exports = __toCommonJS(recaptcha_exports);
32
+ var RECAPTCHA_ACTION = "contact_form";
33
+ var RECAPTCHA_SITE_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
34
+ function isSafeRecaptchaSiteKey(value) {
35
+ return typeof value === "string" && RECAPTCHA_SITE_KEY_PATTERN.test(value);
36
+ }
37
+ var CONTACT_FORM_OPERATIONAL_MODES = ["DB_ONLY", "WEBHOOK", "RESEND"];
38
+ function trimmed(value) {
39
+ if (typeof value !== "string") return null;
40
+ const text = value.trim();
41
+ return text.length > 0 ? text : null;
42
+ }
43
+ function isOperationalMode(mode) {
44
+ return CONTACT_FORM_OPERATIONAL_MODES.includes(mode);
45
+ }
46
+ function resolveContactFormAvailability(analytics) {
47
+ if (!analytics) return { state: "unavailable", reason: "unreadable" };
48
+ const mode = analytics.contactFormMode;
49
+ if (!isOperationalMode(mode)) return { state: "unavailable", reason: "mode-not-operational" };
50
+ const recaptchaSiteKey = trimmed(analytics.recaptchaSiteKey);
51
+ if (!recaptchaSiteKey) return { state: "unavailable", reason: "missing-recaptcha-key" };
52
+ if (!isSafeRecaptchaSiteKey(recaptchaSiteKey)) {
53
+ return { state: "unavailable", reason: "malformed-recaptcha-key" };
54
+ }
55
+ return { state: "available", mode, recaptchaSiteKey };
56
+ }
57
+ function isContactFormAvailable(availability) {
58
+ return availability.state === "available";
59
+ }
60
+ function deferredRecaptchaKey(siteKey, armed) {
61
+ if (!armed) return "";
62
+ const key = trimmed(siteKey);
63
+ return key && isSafeRecaptchaSiteKey(key) ? key : "";
64
+ }
65
+ // Annotate the CommonJS export names for ESM import in node:
66
+ 0 && (module.exports = {
67
+ CONTACT_FORM_OPERATIONAL_MODES,
68
+ RECAPTCHA_ACTION,
69
+ RECAPTCHA_SITE_KEY_PATTERN,
70
+ deferredRecaptchaKey,
71
+ isContactFormAvailable,
72
+ isSafeRecaptchaSiteKey,
73
+ resolveContactFormAvailability
74
+ });
75
+ //# sourceMappingURL=recaptcha.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recaptcha.ts"],"sourcesContent":["/**\n * reCAPTCHA v3 — the SITE's half of the contract (REB-588).\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * WHICH HALF LIVES WHERE, AND WHY\n * ─────────────────────────────────────────────────────────────────────────────\n *\n * reCAPTCHA is two secrets and two steps, and they are deliberately split across\n * a trust boundary:\n *\n * SITE KEY — public by design. It ships to the browser inside the script URL,\n * so the tenant serves it from `/api/site/analytics`. This module\n * handles it.\n * SECRET — never leaves base-api. It is stored per tenant in\n * `site_integration_config.recaptchaSecretKey` and read only by\n * `serverCaptchaVerify` when `POST /api/site/contact-form` runs.\n *\n * **Nothing in this module, in this package, or anywhere in the `@reblu/site-*`\n * SDK ever sees the secret, and nothing here calls Google's `siteverify`.** A\n * SITE holding the secret would be a SITE that could be tricked into approving\n * its own tokens, and a distributable SDK is the worst possible place to put a\n * credential. Verification is base-api's job and already works: the SITE mints a\n * token, posts it as `ContactFormInput.captcha`, and base-api decides.\n *\n * That is also why this module is NOT \"reCAPTCHA support\" in the round. It is\n * the three decisions every SITE was otherwise making alone and differently:\n * which action to name, whether a form may be shown at all, and when Google's\n * script is allowed to load.\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * WHY IT IS IN THE SDK AT ALL\n * ─────────────────────────────────────────────────────────────────────────────\n *\n * Because it was already being written four times. `apps/base-api`,\n * `apps/demo-site` and the external `reblu-site-dermamedicin` each grew their\n * own copy of this flow, and they had already drifted — different action\n * strings, different mount timing, a retry loop in one of them. A score that\n * reCAPTCHA reports per action is not comparable across SITEs that name the\n * action differently, and a defect fixed in one copy stays live in the other\n * two. The SDK is where a decision gets made once.\n *\n * This module has ZERO runtime dependencies and imports nothing — not React, not\n * `next`, not `react-google-recaptcha-v3`, not `@reblu/site-ui`. It is pure\n * functions over plain data, callable from a Server Component deciding whether\n * to render a form and from the Client Component that renders it. The subpath is\n * kept dependency-free by `tests/recaptcha-subpath-isolation.test.ts`.\n */\n\nimport type { Analytics, ContactFormMode } from '@reblu/site-contracts'\n\n/**\n * The action name every Reblu SITE mints its contact-form token under.\n *\n * reCAPTCHA v3 scores PER ACTION, and a score is only interpretable against a\n * stable one: Google's own admin console charts each action separately, and a\n * threshold tuned on one action says nothing about another. Two SITEs calling\n * `executeRecaptcha('submit_form')` and `executeRecaptcha('contact_form')` are\n * building two unrelated score histories for the same feature — which is exactly\n * the state the repo was in before this constant existed.\n *\n * `contact_form` over `submit_form` because it names the form, not the verb;\n * a second protected form would need its own action, and `submit_form` would\n * have nothing left to call it.\n *\n * ── WHAT THIS CONSTANT DOES **NOT** BUY, STATED PLAINLY ──\n *\n * It buys comparable scores. It does NOT bind a token to this form, because\n * `serverCaptchaVerify` in base-api checks only `success` and `score`, and never\n * reads the `action` field Google returns. A token minted for any other action\n * on the same site verifies here exactly as well. Making the action load-bearing\n * requires base-api to assert `action === RECAPTCHA_ACTION` on verify; until it\n * does, do not read this constant as a security control.\n */\nexport const RECAPTCHA_ACTION = 'contact_form'\n\n/**\n * The shape a site key must have before this SDK will hand it to Google.\n *\n * ── WHAT THIS IS, AND EMPHATICALLY WHAT IT IS NOT ──\n *\n * It is a SINK-SAFETY guard, not a guess at Google's key format. The key is\n * interpolated into the loader's script URL as a query parameter, so what\n * matters here is that it cannot carry a space, a quote, a newline, an `&`, or a\n * `#` — characters that make a URL mean something other than what was intended.\n * The base64url alphabet Google actually issues from (`A-Z a-z 0-9 - _`) is a\n * strict subset of \"safe\", so a real key passes by construction.\n *\n * The length bounds are deliberately far wider than any key Google issues\n * (theirs are 40 characters). A bound tight enough to be a real format check\n * would be a bound tight enough to take a WORKING tenant's contact form offline\n * the day Google changes its key length — which is strictly worse than the\n * malformed key it was meant to catch. If you are tempted to tighten this,\n * the thing to tighten is the nexus write path, where an owner can still see\n * and fix the error, not this read path where they cannot.\n *\n * ── WHY THE SDK RE-VALIDATES AT ALL ──\n *\n * Same reason `@reblu/site-next/analytics` re-checks `gaId` and `clarityId`\n * (REB-617): base-api is not the only thing a SITE ever talks to, and the layer\n * that owns the value is not the layer that suffers from it being wrong. As of\n * REB-588 this column has NO validation anywhere — not in the nexus form, not on\n * the way out of `/api/site/analytics`, not in the response contract, which is\n * the one asymmetry `gaId`/`clarityId` do not share. Until that is fixed\n * upstream, this is the only guard between a mistyped key and a contact form\n * that renders, looks live, and silently eats every enquiry.\n */\nexport const RECAPTCHA_SITE_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/\n\n/**\n * Whether `value` is a site key safe to put in the loader's script URL.\n *\n * Takes `unknown` on purpose: the value arrives over HTTP, so the contract's\n * `string | null` is a promise about the shape, not a guarantee about the bytes.\n */\nexport function isSafeRecaptchaSiteKey(value: unknown): value is string {\n return typeof value === 'string' && RECAPTCHA_SITE_KEY_PATTERN.test(value)\n}\n\n/**\n * The modes in which base-api will actually accept a submission.\n *\n * An ALLOW-LIST, never `mode !== 'DISABLED'`. The three differ only in what\n * base-api DOES with the payload — a row, a webhook, an email — and the SITE\n * must not care and must never learn the destination: a webhook URL or a Resend\n * key is a tenant secret with no business in a public site.\n *\n * The deny-list version of this is a real incident, not a hypothetical: a page\n * that hid the form on `DISABLED` and rendered it otherwise rendered it for\n * `NOT_CONFIGURED` too — which is what EVERY freshly provisioned tenant carries\n * until someone finishes the wizard (REB-586). A visitor typed out a medical\n * enquiry, pressed send, and got `503 Contact form is disabled`. They learned\n * the clinic could not be reached only after writing to it.\n */\nexport const CONTACT_FORM_OPERATIONAL_MODES = ['DB_ONLY', 'WEBHOOK', 'RESEND'] as const\n\n/** A mode in which the form may be shown. */\nexport type ContactFormOperationalMode = (typeof CONTACT_FORM_OPERATIONAL_MODES)[number]\n\n/**\n * The modes deliberately declined. A TYPE, never a value: the runtime must never\n * read it, because an allow-list must not be expressible as \"everything except\n * this list\", and this subpath's defining property is that it ships nothing it\n * does not need. It exists only to feed {@link ContactFormModeExhaustiveCheck}.\n */\ntype DeclinedMode = 'NOT_CONFIGURED' | 'DISABLED'\n\n/**\n * Compile-time exhaustiveness guard over `ContactFormMode`.\n *\n * `Missing` is constrained to `never` and DEFAULTS to the contract modes that\n * are neither accepted nor explicitly declined, so a default violating its own\n * constraint breaks the build the moment `@reblu/site-contracts` gains a mode.\n *\n * At runtime a new mode already fails closed, which is correct but SILENT — and\n * silence is how a tenant switches their form on in nexus and watches nothing\n * appear. This forces a human to make the call in a diff instead.\n *\n * Exported so it is never reported as an unused symbol.\n */\nexport type ContactFormModeExhaustiveCheck<\n Missing extends never = Exclude<ContactFormMode, ContactFormOperationalMode | DeclinedMode>,\n> = Missing\n\n/** Why the form is not being shown. Drives diagnostics, never visitor-facing copy. */\nexport type ContactFormUnavailableReason =\n /** The analytics read failed or was never made — no answer at all. */\n | 'unreadable'\n /** The tenant's mode is not one base-api accepts submissions in. */\n | 'mode-not-operational'\n /** Accepting mode, but no site key: the browser cannot mint a token. */\n | 'missing-recaptcha-key'\n /** A site key is present but cannot be a key — see {@link RECAPTCHA_SITE_KEY_PATTERN}. */\n | 'malformed-recaptcha-key'\n\nexport type ContactFormAvailability =\n /**\n * Confirmed: base-api accepts submissions AND a usable site key exists. The\n * key travels inside the `available` case on purpose — a component that has\n * proven the form may render should not have to re-read (and re-null-check)\n * the payload to find out what to render it WITH.\n */\n | { state: 'available'; mode: ContactFormOperationalMode; recaptchaSiteKey: string }\n | { state: 'unavailable'; reason: ContactFormUnavailableReason }\n\n/** Whitespace-only is absent — a key someone saved blank is not a key. */\nfunction trimmed(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const text = value.trim()\n return text.length > 0 ? text : null\n}\n\nfunction isOperationalMode(mode: unknown): mode is ContactFormOperationalMode {\n // Widened to `unknown` deliberately: see isSafeRecaptchaSiteKey. A base-api\n // newer than this SDK sends a string TypeScript has never heard of, and it\n // must fail closed rather than be assumed accepting.\n return (CONTACT_FORM_OPERATIONAL_MODES as readonly string[]).includes(mode as string)\n}\n\n/**\n * Resolve whether a contact form may be rendered for this tenant.\n *\n * We offer what we can CONFIRM, never what we have merely failed to disprove: an\n * unknown mode, a mode from a runtime newer than this build, and a config read\n * that threw all resolve to the same answer — no form. The page still shows the\n * phone, the email and the address, so a hidden form costs a visitor one extra\n * glance; a form that eats their message costs them the message.\n *\n * Order matters. The mode is checked BEFORE the key, because \"the tenant has not\n * switched the form on\" is the actionable half of a double failure — a site key\n * for a form nobody enabled is not the problem to fix first.\n */\nexport function resolveContactFormAvailability(\n analytics: Analytics | null | undefined,\n): ContactFormAvailability {\n if (!analytics) return { state: 'unavailable', reason: 'unreadable' }\n\n const mode = analytics.contactFormMode\n if (!isOperationalMode(mode)) return { state: 'unavailable', reason: 'mode-not-operational' }\n\n // `ContactFormInput.captcha` is REQUIRED and base-api verifies it, so an\n // accepting mode with no usable key is still a form whose every submission is\n // dead on arrival. \"Missing\" and \"malformed\" are separated because they call\n // for different fixes by different people: one owner never filled the field,\n // the other filled it with the wrong thing.\n const recaptchaSiteKey = trimmed(analytics.recaptchaSiteKey)\n if (!recaptchaSiteKey) return { state: 'unavailable', reason: 'missing-recaptcha-key' }\n if (!isSafeRecaptchaSiteKey(recaptchaSiteKey)) {\n return { state: 'unavailable', reason: 'malformed-recaptcha-key' }\n }\n\n return { state: 'available', mode, recaptchaSiteKey }\n}\n\n/** Narrowing helper — the single question a renderer should ask. */\nexport function isContactFormAvailable(\n availability: ContactFormAvailability,\n): availability is Extract<ContactFormAvailability, { state: 'available' }> {\n return availability.state === 'available'\n}\n\n/**\n * The key to hand `GoogleReCaptchaProvider` on this render — `''` until `armed`.\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * WHAT THIS BUYS\n * ─────────────────────────────────────────────────────────────────────────────\n *\n * `GoogleReCaptchaProvider` loads Google's `api.js` the moment it MOUNTS. Mount\n * it with the form and every visitor who merely lands on the page to read the\n * phone number has contacted Google — on a clinic's contact page, a third-party\n * request nobody asked for, on a route whose whole subject is personal data.\n *\n * Arming on first interaction with the form (focus, first keystroke) means\n * Google is contacted only for a visitor who is actually about to submit, which\n * is the only visitor whose token is worth anything. It is a smaller page, a\n * faster one, and a materially smaller consent surface.\n *\n * **This is NOT a cookie-consent implementation.** Deferring the script narrows\n * the exposure; it does not gate it on a lawful basis. Full consent gating for\n * third parties is REB-642 and has its own owner.\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * HOW, WITHOUT A HACK — MEASURED, NOT ASSUMED\n * ─────────────────────────────────────────────────────────────────────────────\n *\n * `react-google-recaptcha-v3` injects the script from an effect shaped\n *\n * useEffect(() => { if (reCaptchaKey) { …inject…; return cleanup } … },\n * [ …, reCaptchaKey, … ])\n *\n * Two facts fall out, and together they are the whole mechanism:\n *\n * 1. An EMPTY key mounts the provider and injects NOTHING.\n * 2. `reCaptchaKey` is a dependency, so swapping the real key in later re-runs\n * the effect and injects the script then.\n *\n * Crucially the provider element never leaves the tree, so its children are\n * never remounted: the visitor does not lose focus, a half-typed word, or the\n * caret position at the instant the script arrives. This is the library's own\n * documented prop doing its documented job — no imperative script injection, no\n * reaching into the DOM, no fork.\n *\n * Both facts are pinned by `tests/recaptcha-deferred-oracle.test.tsx`, which\n * drives the REAL library and counts script tags in the DOM. They are claims\n * about someone else's code, so prose asserting them is not enough — if a future\n * version of the library injects eagerly, that test goes red and this whole\n * optimisation is known to be void.\n *\n * ── THE ONE COST, MEASURED ──\n *\n * On the empty-key render the library calls its own warn helper, which is\n * written inverted, so the warning fires in PRODUCTION and stays silent in\n * development. A visitor who opens devtools before touching a field sees one\n * `\"<GoogleReCaptchaProvider /> recaptcha key not provided\"`. Cosmetic console\n * noise, traded against a third-party request on every view of the page. Worth\n * it, and recorded here so the next reader knows it is expected.\n *\n * ── AND NO FALLBACK KEY, EVER ──\n *\n * `''` for a blank OR malformed key however armed. There is no hardcoded\n * fallback anywhere in this module, because a captcha key that is not the\n * tenant's fails Google's verification while LOOKING configured — converting a\n * visible gap an owner can fix in nexus into a silent one nobody can.\n * {@link resolveContactFormAvailability} hides the form in that case; this is\n * the second lock on the same door.\n */\nexport function deferredRecaptchaKey(siteKey: string, armed: boolean): string {\n if (!armed) return ''\n const key = trimmed(siteKey)\n return key && isSafeRecaptchaSiteKey(key) ? key : ''\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyEO,IAAM,mBAAmB;AAiCzB,IAAM,6BAA6B;AAQnC,SAAS,uBAAuB,OAAiC;AACtE,SAAO,OAAO,UAAU,YAAY,2BAA2B,KAAK,KAAK;AAC3E;AAiBO,IAAM,iCAAiC,CAAC,WAAW,WAAW,QAAQ;AAoD7E,SAAS,QAAQ,OAA+B;AAC9C,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,OAAO,MAAM,KAAK;AACxB,SAAO,KAAK,SAAS,IAAI,OAAO;AAClC;AAEA,SAAS,kBAAkB,MAAmD;AAI5E,SAAQ,+BAAqD,SAAS,IAAc;AACtF;AAeO,SAAS,+BACd,WACyB;AACzB,MAAI,CAAC,UAAW,QAAO,EAAE,OAAO,eAAe,QAAQ,aAAa;AAEpE,QAAM,OAAO,UAAU;AACvB,MAAI,CAAC,kBAAkB,IAAI,EAAG,QAAO,EAAE,OAAO,eAAe,QAAQ,uBAAuB;AAO5F,QAAM,mBAAmB,QAAQ,UAAU,gBAAgB;AAC3D,MAAI,CAAC,iBAAkB,QAAO,EAAE,OAAO,eAAe,QAAQ,wBAAwB;AACtF,MAAI,CAAC,uBAAuB,gBAAgB,GAAG;AAC7C,WAAO,EAAE,OAAO,eAAe,QAAQ,0BAA0B;AAAA,EACnE;AAEA,SAAO,EAAE,OAAO,aAAa,MAAM,iBAAiB;AACtD;AAGO,SAAS,uBACd,cAC0E;AAC1E,SAAO,aAAa,UAAU;AAChC;AAoEO,SAAS,qBAAqB,SAAiB,OAAwB;AAC5E,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,MAAM,QAAQ,OAAO;AAC3B,SAAO,OAAO,uBAAuB,GAAG,IAAI,MAAM;AACpD;","names":[]}