@burdenoff/microfe-auth 2026.1102.1 → 2026.1103.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"RegisterPage.js","names":["_Fragment","RegisterPage","tr","useTr","useAuth","navigate","useNavigate","useAuthOperations","useOAuthOperations","showPassword","setShowPassword","useState","formData","setFormData","validationErrors","setValidationErrors","signupSuccess","setSignupSuccess","error","setError","oauthError","setOauthError","captchaToken","setCaptchaToken","captchaResetKey","setCaptchaResetKey","captchaEnabled","captchaSiteKey","useSignupCaptcha","isResendingVerification","setIsResendingVerification","resendError","setResendError","isAccountDeactivated","errorCode","isAccountRemoved","isAccountSuspended","passwordChecks","password","passwordStrength","metCount","c","handleSubmit","errors","validateEmail","userFieldsValidation","validateUserFields","passwordValidation","validatePassword","userFields","signUp","err","message","k","handleGoogleSignup","handleMicrosoftSignup","isGoogleConfigured","isProviderConfigured","isMicrosoftConfigured","oauthProviders","GoogleIcon","GitHubIcon","MicrosoftIcon","verificationEmailPrefix","verificationEmailSuffix","AuthLayout","Button","resendVerification","productName","PagePurpose","AlertIcon","UserIcon","EmailIcon","LockIcon","jsx","EyeOffIcon","EyeIcon","check","CheckIcon","XIcon","resolveLegalUrl","CaptchaV2","isLoading","Fragment","SpinnerIcon","provider"],"sources":["../../../src/auth/pages/RegisterPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useNavigate } from \"react-router\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport {\n validateEmail,\n validatePassword,\n validateUserFields,\n} from \"../utils/validation\";\nimport { resolveLegalUrl } from \"../utils/legalUrls\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useOAuthOperations } from \"../hooks/useOAuthOperations\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport {\n EmailIcon,\n LockIcon,\n UserIcon,\n EyeIcon,\n EyeOffIcon,\n AlertIcon,\n CheckIcon,\n XIcon,\n SpinnerIcon,\n GoogleIcon,\n GitHubIcon,\n MicrosoftIcon,\n} from \"../components/AuthIcons\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { CaptchaV2 } from \"../components/CaptchaV2\";\nimport { useSignupCaptcha } from \"../hooks/useSignupCaptcha\";\nimport type { SignUpInput } from \"../types\";\n\n/**\n * Registration page with professional design\n */\nexport function RegisterPage() {\n const tr = useTr();\n const { productName } = useAuth();\n const navigate = useNavigate();\n\n const { signUp, resendVerification, isLoading, errorCode } =\n useAuthOperations();\n const { initiateGoogleOAuth, initiateMicrosoftOAuth, isProviderConfigured } =\n useOAuthOperations();\n const [showPassword, setShowPassword] = useState(false);\n const [formData, setFormData] = useState<SignUpInput>({\n email: \"\",\n username: \"\",\n password: \"\",\n firstName: \"\",\n lastName: \"\",\n });\n const [validationErrors, setValidationErrors] = useState<string[]>([]);\n const [signupSuccess, setSignupSuccess] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [oauthError, setOauthError] = useState<string | null>(null);\n const [captchaToken, setCaptchaToken] = useState<string | null>(null);\n // reCAPTCHA v2 tokens are single-use. Bumping this key remounts the widget so\n // a fresh token is issued after a failed submit (the prior token is consumed\n // server-side; reusing it returns timeout-or-duplicate → CAPTCHA_FAILED loop).\n const [captchaResetKey, setCaptchaResetKey] = useState(0);\n const { enabled: captchaEnabled, siteKey: captchaSiteKey } =\n useSignupCaptcha();\n const [isResendingVerification, setIsResendingVerification] = useState(false);\n const [resendError, setResendError] = useState<string | null>(null);\n\n // Signing up with an email that already belongs to a non-active account:\n // route the user to the right recovery path instead of dead-ending.\n const isAccountDeactivated =\n Boolean(error) && errorCode === \"ACCOUNT_DEACTIVATED\";\n const isAccountRemoved = Boolean(error) && errorCode === \"ACCOUNT_REMOVED\";\n const isAccountSuspended =\n Boolean(error) && errorCode === \"ACCOUNT_SUSPENDED\";\n\n // Password strength indicators\n const passwordChecks = (() => {\n const password = formData.password;\n return [\n {\n label: tr(\"auth.register.pwMin8\", \"At least 12 characters\"),\n met: password.length >= 12,\n },\n {\n label: tr(\"auth.register.pwUppercase\", \"One uppercase letter\"),\n met: /[A-Z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwLowercase\", \"One lowercase letter\"),\n met: /[a-z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwNumber\", \"One number\"),\n met: /\\d/.test(password),\n },\n {\n label: tr(\"auth.register.pwSpecial\", \"One special character\"),\n met: /[!@#$%^&*(),.?\":{}|<>]/.test(password),\n },\n ];\n })();\n\n const passwordStrength = (() => {\n const metCount = passwordChecks.filter((c) => c.met).length;\n if (metCount <= 2)\n return {\n level: tr(\"auth.register.passwordStrengthWeak\", \"weak\"),\n color: \"bg-status-error-bg-subtle0\",\n width: \"w-1/4\",\n };\n if (metCount <= 3)\n return {\n level: tr(\"auth.register.passwordStrengthFair\", \"fair\"),\n color: \"bg-status-warning-text\",\n width: \"w-2/4\",\n };\n if (metCount <= 4)\n return {\n level: tr(\"auth.register.passwordStrengthGood\", \"good\"),\n color: \"bg-status-warning-bg-subtle0\",\n width: \"w-3/4\",\n };\n return {\n level: tr(\"auth.register.passwordStrengthStrong\", \"strong\"),\n color: \"bg-status-success-text\",\n width: \"w-full\",\n };\n })();\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n const errors: string[] = [];\n if (!validateEmail(formData.email)) {\n errors.push(\n tr(\"auth.register.invalidEmail\", \"Please enter a valid email address\"),\n );\n }\n const userFieldsValidation = validateUserFields(formData);\n if (!userFieldsValidation.valid) {\n errors.push(\n ...userFieldsValidation.errors.map((e) =>\n tr(`auth.register.${e.field}_${e.code}`, e.message),\n ),\n );\n }\n const passwordValidation = validatePassword(formData.password);\n if (!passwordValidation.valid) {\n errors.push(...passwordValidation.errors);\n }\n\n if (captchaEnabled && !captchaToken) {\n errors.push(\n tr(\n \"auth.register.completeCaptcha\",\n \"Please complete the reCAPTCHA verification\",\n ),\n );\n }\n\n if (errors.length > 0) {\n setValidationErrors(errors);\n return;\n }\n\n setValidationErrors([]);\n setError(null);\n try {\n // Submit the canonicalised values the validators approved, not the\n // raw formData — so leading/trailing whitespace and typographic\n // apostrophes/hyphens don't reach the backend / DB. The cast is\n // sound because the `errors.length > 0` early return above would\n // have caught any failed validation.\n const userFields = (\n userFieldsValidation as {\n valid: true;\n values: { firstName: string; lastName: string; username: string };\n }\n ).values;\n await signUp({\n ...formData,\n firstName: userFields.firstName,\n lastName: userFields.lastName,\n username: userFields.username,\n ...(captchaToken ? { recaptchaToken: captchaToken } : {}),\n });\n // Sign up successful - check if email verification is required\n // (Backend returns empty tokens when email verification is needed)\n setSignupSuccess(true);\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\"auth.register.signUpFailed\", \"Sign up failed\");\n setError(message);\n console.error(\"Sign up failed:\", err);\n // The captcha token (if any) was consumed during this attempt — discard it\n // and remount the widget so the user gets a fresh, solvable challenge on\n // retry instead of being trapped in a CAPTCHA_FAILED loop.\n if (captchaEnabled) {\n setCaptchaToken(null);\n setCaptchaResetKey((k) => k + 1);\n }\n }\n };\n\n /**\n * Handle Google OAuth signup\n */\n const handleGoogleSignup = () => {\n setOauthError(null);\n try {\n initiateGoogleOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.googleSignUpFailed\",\n \"Failed to initiate Google sign up\",\n ),\n );\n }\n };\n\n /**\n * Handle Microsoft OAuth signup\n */\n const handleMicrosoftSignup = () => {\n setOauthError(null);\n try {\n initiateMicrosoftOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.microsoftSignUpFailed\",\n \"Failed to initiate Microsoft sign up\",\n ),\n );\n }\n };\n\n // Check if OAuth providers are configured\n const isGoogleConfigured = isProviderConfigured(\"google\");\n const isMicrosoftConfigured = isProviderConfigured(\"microsoft\");\n\n const oauthProviders = [\n {\n name: \"Google\",\n icon: GoogleIcon,\n onClick: handleGoogleSignup,\n disabled: !isGoogleConfigured,\n tooltip: isGoogleConfigured\n ? undefined\n : tr(\n \"auth.register.googleNotConfigured\",\n \"Google sign up is not configured\",\n ),\n },\n {\n name: \"GitHub\",\n icon: GitHubIcon,\n onClick: () => {},\n disabled: true,\n tooltip: tr(\n \"auth.register.githubNotConfigured\",\n \"GitHub sign up is not configured\",\n ),\n },\n {\n name: \"Microsoft\",\n icon: MicrosoftIcon,\n onClick: handleMicrosoftSignup,\n disabled: !isMicrosoftConfigured,\n tooltip: isMicrosoftConfigured\n ? undefined\n : tr(\n \"auth.register.microsoftNotConfigured\",\n \"Microsoft sign up is not configured\",\n ),\n },\n ];\n\n const verificationEmailMessage = tr(\n \"auth.register.verificationEmailSentTo\",\n \"We've sent a verification email to {{email}}. Please check your inbox and click the verification link to activate your account.\",\n );\n const [verificationEmailPrefix, verificationEmailSuffix = \"\"] =\n verificationEmailMessage.split(\"{{email}}\");\n\n // Show success state after signup\n if (signupSuccess) {\n return (\n <AuthLayout\n title={tr(\"auth.register.checkEmail\", \"Check your email\")}\n subtitle={tr(\n \"auth.register.verificationLinkSent\",\n \"We've sent you a verification link\",\n )}\n >\n <div className=\"flex flex-col items-center justify-center py-6\">\n {/* Success Icon */}\n <div className=\"size-20 rounded-full bg-status-success-bg-subtle flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <h3 className=\"text-lg font-semibold text-[var(--color-text-primary)] mb-2\">\n {tr(\n \"auth.register.accountCreated\",\n \"Account created successfully!\",\n )}\n </h3>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-8 max-w-sm\">\n {verificationEmailPrefix}\n <span className=\"font-medium text-[var(--color-text-primary)]\">\n {formData.email}\n </span>\n {verificationEmailSuffix}\n </p>\n\n <div className=\"space-y-3 w-full\">\n <Button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\")}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {tr(\"auth.register.continueToSignIn\", \"Continue to sign in\")}\n </Button>\n\n {resendError && (\n <p className=\"text-center text-sm text-status-error-text\">\n {resendError}\n </p>\n )}\n <p className=\"text-center text-sm text-[var(--color-text-tertiary)]\">\n {tr(\n \"auth.register.didntReceiveEmail\",\n \"Didn't receive the email? Check your spam folder or\",\n )}{\" \"}\n <button\n type=\"button\"\n disabled={isResendingVerification}\n onClick={async () => {\n setResendError(null);\n setIsResendingVerification(true);\n try {\n await resendVerification(formData.email);\n } catch (err) {\n setResendError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.resendFailed\",\n \"Failed to resend verification email\",\n ),\n );\n } finally {\n setIsResendingVerification(false);\n }\n }}\n className=\"text-text-link hover:text-text-linkHover font-medium disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isResendingVerification\n ? tr(\"auth.register.resending\", \"resending...\")\n : tr(\"auth.register.tryAgain\", \"try again\")}\n </button>\n </p>\n </div>\n </div>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout\n title={tr(\"auth.register.createAccount\", \"Create your account\")}\n subtitle={tr(\n \"auth.register.startJourney\",\n \"Start your journey with {{productName}}\",\n ).replace(\"{{productName}}\", productName || \"Burdenoff Workspaces\")}\n productName={productName}\n >\n <PagePurpose className=\"mb-5\">\n {tr(\n \"auth.register.purpose\",\n \"Create your account to get a personal workspace and start collaborating. You'll verify your email, then you can sign in and invite your team.\",\n )}\n </PagePurpose>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Deactivated account → offer self-service reactivation */}\n {isAccountDeactivated && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n \"auth.register.accountDeactivated\",\n \"This email belongs to a deactivated account.\",\n )}\n </p>\n <p className=\"text-xs text-status-warning-text/80\">\n {tr(\n \"auth.register.accountDeactivatedHelp\",\n \"If this is your account, you can reactivate it instead of creating a new one.\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() =>\n navigate(\"/auth/reactivate\", {\n state: { email: formData.email },\n })\n }\n className=\"text-sm font-semibold text-status-warning-text underline underline-offset-2\"\n >\n {tr(\"auth.register.reactivateCta\", \"Reactivate my account\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Admin-removed / suspended → point to support (no self-service path) */}\n {(isAccountRemoved || isAccountSuspended) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <p className=\"text-sm text-status-warning-text\">{error}</p>\n </div>\n )}\n\n {/* Generic error / validation alert */}\n {((error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended) ||\n validationErrors.length > 0) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\">\n <AlertIcon className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-1\">\n {error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended && (\n <p className=\"text-sm text-status-error-text\">{error}</p>\n )}\n {validationErrors.map((err) => (\n <p key={err} className=\"text-sm text-status-error-text\">\n {err}\n </p>\n ))}\n </div>\n </div>\n )}\n\n {/* Name Fields */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <label\n htmlFor=\"firstName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.firstName\", \"First Name\")}\n </label>\n <input\n id=\"firstName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.firstName\", \"First Name\")}\n value={formData.firstName}\n onChange={(e) =>\n setFormData({ ...formData, firstName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.firstNamePlaceholder\",\n \"Enter first name\",\n )}\n />\n </div>\n <div className=\"space-y-2\">\n <label\n htmlFor=\"lastName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.lastName\", \"Last Name\")}\n </label>\n <input\n id=\"lastName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.lastName\", \"Last Name\")}\n value={formData.lastName}\n onChange={(e) =>\n setFormData({ ...formData, lastName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.lastNamePlaceholder\",\n \"Enter last name\",\n )}\n />\n </div>\n </div>\n\n {/* Username Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"username\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.username\", \"Username\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <UserIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"username\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.username\", \"Username\")}\n autoComplete=\"username\"\n value={formData.username}\n onChange={(e) =>\n setFormData({ ...formData, username: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.usernamePlaceholder\",\n \"Choose a username\",\n )}\n />\n </div>\n </div>\n\n {/* Email Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.email\", \"Email\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <EmailIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.email\", \"Email\")}\n autoComplete=\"email\"\n value={formData.email}\n onChange={(e) =>\n setFormData({ ...formData, email: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.emailPlaceholder\",\n \"you@example.com\",\n )}\n />\n </div>\n </div>\n\n {/* Password Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.password\", \"Password\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <LockIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.password\", \"Password\")}\n autoComplete=\"new-password\"\n value={formData.password}\n onChange={(e) =>\n setFormData({ ...formData, password: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-12 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.passwordPlaceholder\",\n \"Create a strong password\",\n )}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n aria-label={\n showPassword\n ? tr(\"auth.register.hidePassword\", \"Hide password\")\n : tr(\"auth.register.showPassword\", \"Show password\")\n }\n className=\"absolute inset-y-0 right-0 pr-3.5 flex items-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors\"\n >\n {showPassword ? (\n <EyeOffIcon className=\"size-5\" />\n ) : (\n <EyeIcon className=\"size-5\" />\n )}\n </button>\n </div>\n\n {/* Password Strength Indicator */}\n {formData.password && (\n <div className=\"space-y-3 pt-2\">\n {/* Strength Bar */}\n <div className=\"space-y-1\">\n <div className=\"h-1.5 bg-[var(--color-bg-sunken)] rounded-full overflow-hidden\">\n <div\n className={`h-full ${passwordStrength.color} ${passwordStrength.width} transition-all duration-300`}\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-tertiary)] capitalize\">\n {tr(\"auth.register.passwordStrength\", \"Password strength:\")}{\" \"}\n <span className=\"font-medium\">{passwordStrength.level}</span>\n </p>\n </div>\n\n {/* Requirements Checklist */}\n <div className=\"grid grid-cols-2 gap-2\">\n {passwordChecks.map((check) => (\n <div key={check.label} className=\"flex items-center gap-2\">\n {check.met ? (\n <CheckIcon className=\"size-4 text-status-success-text\" />\n ) : (\n <XIcon className=\"size-4 text-[var(--color-text-tertiary)]\" />\n )}\n <span\n className={`text-xs ${\n check.met\n ? \"text-status-success-text\"\n : \"text-[var(--color-text-tertiary)]\"\n }`}\n >\n {check.label}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n\n {/* Terms Agreement */}\n <p className=\"text-xs text-[var(--color-text-tertiary)] leading-relaxed\">\n {tr(\n \"auth.register.agreeToTerms\",\n \"By creating an account, you agree to our\",\n )}{\" \"}\n {/* These link to the PUBLIC marketing site, not an app route: the\n reader is signed out, and the app's /legal/:slug route sits behind\n the shell's auth guard. Opened in a new tab so a half-completed\n signup form is not discarded to read the terms. */}\n <a\n href={resolveLegalUrl(\"/terms\")}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.termsOfService\", \"Terms of Service\")}\n </a>{\" \"}\n {tr(\"auth.register.and\", \"and\")}{\" \"}\n <a\n href={resolveLegalUrl(\"/privacy\")}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.privacyPolicy\", \"Privacy Policy\")}\n </a>\n .\n </p>\n\n {/* reCAPTCHA v2 Checkbox — enabled per-product via the platform catalog.\n `key` remounts it after a failed submit so a fresh token is issued. */}\n <CaptchaV2\n key={captchaResetKey}\n enabled={captchaEnabled}\n siteKey={captchaSiteKey}\n onVerify={setCaptchaToken}\n onExpired={() => setCaptchaToken(null)}\n />\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n disabled={isLoading}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>\n {tr(\"auth.register.creatingAccount\", \"Creating account...\")}\n </span>\n </>\n ) : (\n tr(\"auth.register.createAccountBtn\", \"Create account\")\n )}\n </Button>\n </form>\n\n {/* Divider */}\n <div className=\"relative my-8\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-[var(--color-border-default)]\" />\n </div>\n <div className=\"relative flex justify-center\">\n <span className=\"px-4 bg-[var(--color-bg-canvas)] text-sm text-[var(--color-text-tertiary)]\">\n {tr(\"auth.register.orSignUpWith\", \"or sign up with\")}\n </span>\n </div>\n </div>\n\n {/* OAuth Error */}\n {oauthError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-error-bg-subtle border-status-error-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{oauthError}</p>\n </div>\n )}\n\n {/* OAuth Buttons */}\n <div className=\"grid grid-cols-3 gap-3\">\n {oauthProviders.map((provider) => (\n <button\n key={provider.name}\n type=\"button\"\n onClick={provider.onClick}\n disabled={provider.disabled}\n title={provider.tooltip}\n className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all duration-200 group ${\n provider.disabled\n ? \"border-[var(--color-border-subtle)] bg-[var(--color-bg-disabled)] cursor-not-allowed opacity-50\"\n : \"border-[var(--color-border-default)] bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-sunken)] hover:border-[var(--color-border-strong)]\"\n }`}\n >\n <provider.icon\n className={`size-5 ${provider.disabled ? \"opacity-50\" : \"\"}`}\n />\n <span\n className={`sr-only sm:not-sr-only text-sm font-medium transition-colors ${\n provider.disabled\n ? \"text-[var(--color-text-disabled)]\"\n : \"text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]\"\n }`}\n >\n {provider.name}\n </span>\n </button>\n ))}\n </div>\n\n {/* Sign In Link */}\n <p className=\"mt-8 text-center text-sm text-[var(--color-text-secondary)]\">\n {tr(\"auth.register.alreadyHaveAccount\", \"Already have an account?\")}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\")}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.register.signIn\", \"Sign in\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoCA,SAAgBC,IAAe;CAC7B,IAAMC,IAAKC,EAAM,GACX,EAAE,mBAAgBC,GAAQ,GAC1BC,IAAWC,GAAY,GAEvB,EAAE,WAAQ,uBAAoB,cAAW,iBAC7CC,EAAkB,GACd,EAAE,yBAAqB,4BAAwB,4BACnDC,EAAmB,GACf,CAACC,GAAcC,MAAmBC,EAAS,EAAK,GAChD,CAACC,GAAUC,KAAeF,EAAsB;EACpD,OAAO;EACP,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;CACZ,CAAC,GACK,CAACG,GAAkBC,KAAuBJ,EAAmB,CAAC,CAAC,GAC/D,CAACK,GAAeC,KAAoBN,EAAS,EAAK,GAClD,CAACO,GAAOC,KAAYR,EAAwB,IAAI,GAChD,CAACS,GAAYC,KAAiBV,EAAwB,IAAI,GAC1D,CAACW,GAAcC,KAAmBZ,EAAwB,IAAI,GAI9D,CAACa,GAAiBC,MAAsBd,EAAS,CAAC,GAClD,EAAE,SAASe,GAAgB,SAASC,OACxCC,GAAiB,GACb,CAACC,GAAyBC,KAA8BnB,EAAS,EAAK,GACtE,CAACoB,GAAaC,KAAkBrB,EAAwB,IAAI,GAI5DsB,IACJ,EAAQf,KAAUgB,MAAc,uBAC5BC,IAAmB,EAAQjB,KAAUgB,MAAc,mBACnDE,IACJ,EAAQlB,KAAUgB,MAAc,qBAG5BG,WAAwB;EAC5B,IAAMC,IAAW1B,EAAS;EAC1B,OAAO;GACL;IACE,OAAOV,EAAG,wBAAwB,wBAAwB;IAC1D,KAAKoC,EAAS,UAAU;GAC1B;GACA;IACE,OAAOpC,EAAG,6BAA6B,sBAAsB;IAC7D,KAAK,QAAQ,KAAKoC,CAAQ;GAC5B;GACA;IACE,OAAOpC,EAAG,6BAA6B,sBAAsB;IAC7D,KAAK,QAAQ,KAAKoC,CAAQ;GAC5B;GACA;IACE,OAAOpC,EAAG,0BAA0B,YAAY;IAChD,KAAK,KAAK,KAAKoC,CAAQ;GACzB;GACA;IACE,OAAOpC,EAAG,2BAA2B,uBAAuB;IAC5D,KAAK,yBAAyB,KAAKoC,CAAQ;GAC7C;EACF;CACF,EAAA,CAAG,GAEGC,WAA0B;EAC9B,IAAMC,IAAWH,EAAe,QAAQ,MAAMI,EAAE,GAAG,CAAC,CAAC;EAmBrD,OAlBID,KAAY,IACP;GACL,OAAOtC,EAAG,sCAAsC,MAAM;GACtD,OAAO;GACP,OAAO;EACT,IACEsC,KAAY,IACP;GACL,OAAOtC,EAAG,sCAAsC,MAAM;GACtD,OAAO;GACP,OAAO;EACT,IACEsC,KAAY,IACP;GACL,OAAOtC,EAAG,sCAAsC,MAAM;GACtD,OAAO;GACP,OAAO;EACT,IACK;GACL,OAAOA,EAAG,wCAAwC,QAAQ;GAC1D,OAAO;GACP,OAAO;EACT;CACF,EAAA,CAAG,GAEGwC,KAAe,OAAO,MAAuB;EACjD,EAAE,eAAe;EAEjB,IAAMC,IAAmB,CAAC;EAC1B,AAAKC,EAAchC,EAAS,KAAK,KAC/B+B,EAAO,KACLzC,EAAG,8BAA8B,oCAAoC,CACvE;EAEF,IAAM2C,IAAuBC,GAAmBlC,CAAQ;EACxD,AAAKiC,EAAqB,SACxBF,EAAO,KACL,GAAGE,EAAqB,OAAO,KAAK,MAClC3C,EAAG,iBAAiB,EAAE,MAAM,GAAG,EAAE,QAAQ,EAAE,OAAO,CACpD,CACF;EAEF,IAAM6C,IAAqBC,GAAiBpC,EAAS,QAAQ;EAc7D,IAbKmC,EAAmB,SACtBJ,EAAO,KAAK,GAAGI,EAAmB,MAAM,GAGtCrB,KAAkB,CAACJ,KACrBqB,EAAO,KACLzC,EACE,iCACA,4CACF,CACF,GAGEyC,EAAO,SAAS,GAClB,EAAoBA,CAAM;OAI5B;GACA,AADA5B,EAAoB,CAAC,CAAC,GACtBI,EAAS,IAAI;GACb,IAAI;IAMF,IAAM8B,IACJJ,EAIA;IAUF,AATA,MAAMK,EAAO;KACX,GAAGtC;KACH,WAAWqC,EAAW;KACtB,UAAUA,EAAW;KACrB,UAAUA,EAAW;KACrB,GAAI3B,IAAe,EAAE,gBAAgBA,EAAa,IAAI,CAAC;IACzD,CAAC,GAGDL,EAAiB,EAAI;GACvB,SAASkC,GAAK;IACZ,IAAMC,IACJD,aAAe,QACXA,EAAI,UACJjD,EAAG,8BAA8B,gBAAgB;IAMvD,AALAiB,EAASiC,CAAO,GAChB,QAAQ,MAAM,mBAAmBD,CAAG,GAIhCzB,MACFH,EAAgB,IAAI,GACpBE,IAAoB,MAAM4B,IAAI,CAAC;GAEnC;EAtCsB;CAuCxB,GAKMC,WAA2B;EAC/B,EAAc,IAAI;EAClB,IAAI;GACF,GAAoB;EACtB,SAASH,GAAK;GACZ,EACEA,aAAe,QACXA,EAAI,UACJjD,EACE,oCACA,mCACF,CACN;EACF;CACF,GAKMqD,WAA8B;EAClC,EAAc,IAAI;EAClB,IAAI;GACF,GAAuB;EACzB,SAASJ,GAAK;GACZ,EACEA,aAAe,QACXA,EAAI,UACJjD,EACE,uCACA,sCACF,CACN;EACF;CACF,GAGMsD,IAAqBC,EAAqB,QAAQ,GAClDC,IAAwBD,EAAqB,WAAW,GAExDE,KAAiB;EACrB;GACE,MAAM;GACN,MAAMC;GACN,SAASN;GACT,UAAU,CAACE;GACX,SAASA,IACL,KAAA,IACAtD,EACE,qCACA,kCACF;EACN;EACA;GACE,MAAM;GACN,MAAM2D;GACN,eAAe,CAAC;GAChB,UAAU;GACV,SAAS3D,EACP,qCACA,kCACF;EACF;EACA;GACE,MAAM;GACN,MAAM4D;GACN,SAASP;GACT,UAAU,CAACG;GACX,SAASA,IACL,KAAA,IACAxD,EACE,wCACA,qCACF;EACN;CACF,GAMM,CAAC6D,IAAyBC,KAA0B,MAJzB9D,EAC/B,yCACA,iIAGA,CAAA,CAAyB,MAAM,WAAW;CAoG5C,OAjGIc,IAEA,kBAACiD,GAAD;EACE,OAAO/D,EAAG,4BAA4B,kBAAkB;EACxD,UAAUA,EACR,sCACA,oCACF;EAEA,UAAA,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IAEE,kBAAC,OAAD;KAAK,WAAU;KACb,UAAA,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;MAEP,UAAA,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;MACH,CAAA;KACE,CAAA;IACF,CAAA;IAEL,kBAAC,MAAD;KAAI,WAAU;KACX,UAAAA,EACC,gCACA,+BACF;IACE,CAAA;IAEJ,kBAAC,KAAD;KAAG,WAAU;KAAb,UAAA;MACG6D;MACD,kBAAC,QAAD;OAAM,WAAU;OACb,UAAAnD,EAAS;MACN,CAAA;MACLoD;KACA;;IAEH,kBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,kBAACE,GAAD;OACE,MAAK;OACL,eAAe7D,EAAS,aAAa;OACrC,SAAQ;OACR,MAAK;OACL,WAAU;OAET,UAAAH,EAAG,kCAAkC,qBAAqB;MACrD,CAAA;MAEP6B,KACC,kBAAC,KAAD;OAAG,WAAU;OACV,UAAAA;MACA,CAAA;MAEL,kBAAC,KAAD;OAAG,WAAU;OAAb,UAAA;QACG7B,EACC,mCACA,qDACF;QAAG;QACH,kBAAC,UAAD;SACE,MAAK;SACL,UAAU2B;SACV,SAAS,YAAY;UAEnB,AADAG,EAAe,IAAI,GACnBF,EAA2B,EAAI;UAC/B,IAAI;WACF,MAAMqC,EAAmBvD,EAAS,KAAK;UACzC,SAASuC,GAAK;WACZ,EACEA,aAAe,QACXA,EAAI,UACJjD,EACE,8BACA,qCACF,CACN;UACF,UAAU;WACR,EAA2B,EAAK;UAClC;SACF;SACA,WAAU;SAET,UAAA2B,IACG3B,EAAG,2BAA2B,cAAc,IAC5CA,EAAG,0BAA0B,WAAW;QACtC,CAAA;OACP;;KACA;;GACF;;CACK,CAAA,IAKd,kBAAC+D,GAAD;EACE,OAAO/D,EAAG,+BAA+B,qBAAqB;EAC9D,UAAUA,EACR,8BACA,yCACF,CAAC,CAAC,QAAQ,mBAAmBkE,KAAe,sBAAsB;EACrD;EANf,UAAA;GAQE,kBAACC,IAAD;IAAa,WAAU;IACpB,UAAAnE,EACC,yBACA,+IACF;GACW,CAAA;GACb,kBAAC,QAAD;IAAM,UAAUwC;IAAc,WAAU;IAAxC,UAAA;KAEGT,KACC,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAACqC,GAAD,EAAW,WAAU,uDAAwD,CAAA,GAC7E,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA;QACE,kBAAC,KAAD;SAAG,WAAU;SACV,UAAApE,EACC,oCACA,8CACF;QACC,CAAA;QACH,kBAAC,KAAD;SAAG,WAAU;SACV,UAAAA,EACC,wCACA,+EACF;QACC,CAAA;QACH,kBAAC,UAAD;SACE,MAAK;SACL,eACEG,EAAS,oBAAoB,EAC3B,OAAO,EAAE,OAAOO,EAAS,MAAM,EACjC,CAAC;SAEH,WAAU;SAET,UAAAV,EAAG,+BAA+B,uBAAuB;QACpD,CAAA;OACL;MACF,CAAA,CAAA;;MAILiC,KAAoBC,MACpB,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAACkC,GAAD,EAAW,WAAU,uDAAwD,CAAA,GAC7E,kBAAC,KAAD;OAAG,WAAU;OAAoC,UAAApD;MAAS,CAAA,CACvD;;MAIJA,KACD,CAACe,KACD,CAACE,KACD,CAACC,KACDtB,EAAiB,SAAS,MAC1B,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAACwD,GAAD,EAAW,WAAU,qDAAsD,CAAA,GAC3E,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACGpD,KACC,CAACe,KACD,CAACE,KACD,CAACC,KACC,kBAAC,KAAD;QAAG,WAAU;QAAkC,UAAAlB;OAAS,CAAA,GAE3DJ,EAAiB,KAAK,MACrB,kBAAC,KAAD;QAAa,WAAU;QACpB,UAAAqC;OACA,GAFKA,CAEL,CACJ,CACE;MACF,CAAA,CAAA;;KAIP,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,kBAAC,SAAD;QACE,SAAQ;QACR,WAAU;QAET,UAAAjD,EAAG,2BAA2B,YAAY;OACtC,CAAA,GACP,kBAAC,SAAD;QACE,IAAG;QACH,MAAK;QACL,UAAA;QACA,iBAAc;QACd,cAAYA,EAAG,2BAA2B,YAAY;QACtD,OAAOU,EAAS;QAChB,WAAW,MACTC,EAAY;SAAE,GAAGD;SAAU,WAAW,EAAE,OAAO;QAAM,CAAC;QAExD,WAAU;QACV,aAAaV,EACX,sCACA,kBACF;OACD,CAAA,CACE;MACL,CAAA,GAAA,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,kBAAC,SAAD;QACE,SAAQ;QACR,WAAU;QAET,UAAAA,EAAG,0BAA0B,WAAW;OACpC,CAAA,GACP,kBAAC,SAAD;QACE,IAAG;QACH,MAAK;QACL,UAAA;QACA,iBAAc;QACd,cAAYA,EAAG,0BAA0B,WAAW;QACpD,OAAOU,EAAS;QAChB,WAAW,MACTC,EAAY;SAAE,GAAGD;SAAU,UAAU,EAAE,OAAO;QAAM,CAAC;QAEvD,WAAU;QACV,aAAaV,EACX,qCACA,iBACF;OACD,CAAA,CACE;MACF,CAAA,CAAA;;KAGL,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAAC,SAAD;OACE,SAAQ;OACR,WAAU;OAET,UAAAA,EAAG,0BAA0B,UAAU;MACnC,CAAA,GACP,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,kBAAC,OAAD;QAAK,WAAU;QACb,UAAA,kBAACqE,GAAD,EAAU,WAAU,2CAA4C,CAAA;OAC7D,CAAA,GACL,kBAAC,SAAD;QACE,IAAG;QACH,MAAK;QACL,UAAA;QACA,iBAAc;QACd,cAAYrE,EAAG,0BAA0B,UAAU;QACnD,cAAa;QACb,OAAOU,EAAS;QAChB,WAAW,MACTC,EAAY;SAAE,GAAGD;SAAU,UAAU,EAAE,OAAO;QAAM,CAAC;QAEvD,WAAU;QACV,aAAaV,EACX,qCACA,mBACF;OACD,CAAA,CACE;MACF,CAAA,CAAA;;KAGL,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAAC,SAAD;OACE,SAAQ;OACR,WAAU;OAET,UAAAA,EAAG,uBAAuB,OAAO;MAC7B,CAAA,GACP,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,kBAAC,OAAD;QAAK,WAAU;QACb,UAAA,kBAACsE,GAAD,EAAW,WAAU,2CAA4C,CAAA;OAC9D,CAAA,GACL,kBAAC,SAAD;QACE,IAAG;QACH,MAAK;QACL,UAAA;QACA,iBAAc;QACd,cAAYtE,EAAG,uBAAuB,OAAO;QAC7C,cAAa;QACb,OAAOU,EAAS;QAChB,WAAW,MACTC,EAAY;SAAE,GAAGD;SAAU,OAAO,EAAE,OAAO;QAAM,CAAC;QAEpD,WAAU;QACV,aAAaV,EACX,kCACA,iBACF;OACD,CAAA,CACE;MACF,CAAA,CAAA;;KAGL,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA;OACE,kBAAC,SAAD;QACE,SAAQ;QACR,WAAU;QAET,UAAAA,EAAG,0BAA0B,UAAU;OACnC,CAAA;OACP,kBAAC,OAAD;QAAK,WAAU;QAAf,UAAA;SACE,kBAAC,OAAD;UAAK,WAAU;UACb,UAAA,kBAACuE,IAAD,EAAU,WAAU,2CAA4C,CAAA;SAC7D,CAAA;SACL,kBAAC,SAAD;UACE,IAAG;UACH,MAAMhE,IAAe,SAAS;UAC9B,UAAA;UACA,iBAAc;UACd,cAAYP,EAAG,0BAA0B,UAAU;UACnD,cAAa;UACb,OAAOU,EAAS;UAChB,WAAW,MACTC,EAAY;WAAE,GAAGD;WAAU,UAAU,EAAE,OAAO;UAAM,CAAC;UAEvD,WAAU;UACV,aAAaV,EACX,qCACA,0BACF;SACD,CAAA;SACD,kBAAC,UAAD;UACE,MAAK;UACL,eAAeQ,GAAgB,CAACD,CAAY;UAC5C,cACEA,IACIP,EAAG,8BAA8B,eAAe,IAChDA,EAAG,8BAA8B,eAAe;UAEtD,WAAU;UAET,UACCwE,EADDjE,IACEkE,IAEAC,GAFD,EAAY,WAAU,SAAU,CAEH;SAEzB,CAAA;QACL;;OAGJhE,EAAS,YACR,kBAAC,OAAD;QAAK,WAAU;QAAf,UAAA,CAEE,kBAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,kBAAC,OAAD;UAAK,WAAU;UACb,UAAA,kBAAC,OAAD,EACE,WAAW,UAAU2B,EAAiB,MAAM,GAAGA,EAAiB,MAAM,8BACvE,CAAA;SACE,CAAA,GACL,kBAAC,KAAD;UAAG,WAAU;UAAb,UAAA;WACGrC,EAAG,kCAAkC,oBAAoB;WAAG;WAC7D,kBAAC,QAAD;YAAM,WAAU;YAAe,UAAAqC,EAAiB;WAAY,CAAA;UAC3D;SACA,CAAA,CAAA;QAGL,CAAA,GAAA,kBAAC,OAAD;SAAK,WAAU;SACZ,UAAAF,EAAe,KAAK,MACnB,kBAAC,OAAD;UAAuB,WAAU;UAAjC,UAAA,CACGwC,EAAM,MACL,kBAACC,GAAD,EAAW,WAAU,kCAAmC,CAAA,IAExD,kBAACC,GAAD,EAAO,WAAU,2CAA4C,CAAA,GAE/D,kBAAC,QAAD;WACE,WAAW,WACTF,EAAM,MACF,6BACA;WAGL,UAAAA,EAAM;UACH,CAAA,CACH;SAfK,GAAAA,EAAM,KAeX,CACN;QACE,CAAA,CACF;;MAEJ;;KAGL,kBAAC,KAAD;MAAG,WAAU;MAAb,UAAA;OACG3E,EACC,8BACA,0CACF;OAAG;OAKH,kBAAC,KAAD;QACE,MAAM8E,EAAgB,QAAQ;QAC9B,QAAO;QACP,KAAI;QACJ,WAAU;QAET,UAAA9E,EAAG,gCAAgC,kBAAkB;OACrD,CAAA;OAAE;OACJA,EAAG,qBAAqB,KAAK;OAAG;OACjC,kBAAC,KAAD;QACE,MAAM8E,EAAgB,UAAU;QAChC,QAAO;QACP,KAAI;QACJ,WAAU;QAET,UAAA9E,EAAG,+BAA+B,gBAAgB;OAClD,CAAA;OAAC;MAEH;;KAIH,kBAAC+E,IAAD;MAEE,SAASvD;MACT,SAASC;MACT,UAAUJ;MACV,iBAAiBA,EAAgB,IAAI;KACtC,GALMC,CAKN;KAGD,kBAAC0C,GAAD;MACE,MAAK;MACL,UAAUgB;MACV,SAAQ;MACR,MAAK;MACL,WAAU;MAET,UAAAA,IACC,kBAAAlF,IAAA,EAAA,UAAA,CACE,kBAACoF,GAAD,EAAa,WAAU,sBAAuB,CAAA,GAC9C,kBAAC,QAAD,EAAA,UACGlF,EAAG,iCAAiC,qBAAqB,EACtD,CAAA,CACN,EAAA,CAAA,IAEFA,EAAG,kCAAkC,gBAAgB;KAEjD,CAAA;IACJ;;GAGN,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;KACb,UAAA,kBAAC,OAAD,EAAK,WAAU,uDAAwD,CAAA;IACpE,CAAA,GACL,kBAAC,OAAD;KAAK,WAAU;KACb,UAAA,kBAAC,QAAD;MAAM,WAAU;MACb,UAAAA,EAAG,8BAA8B,iBAAiB;KAC/C,CAAA;IACH,CAAA,CACF;;GAGJkB,KACC,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAACkD,GAAD,EAAW,WAAU,qDAAsD,CAAA,GAC3E,kBAAC,KAAD;KAAG,WAAU;KAAkC,UAAAlD;IAAc,CAAA,CAC1D;;GAIP,kBAAC,OAAD;IAAK,WAAU;IACZ,UAAAuC,GAAe,KAAK,MACnB,kBAAC,UAAD;KAEE,MAAK;KACL,SAAS0B,EAAS;KAClB,UAAUA,EAAS;KACnB,OAAOA,EAAS;KAChB,WAAW,wGACTA,EAAS,WACL,oGACA;KATR,UAAA,CAYE,kBAACA,EAAS,MAAV,EACE,WAAW,UAAUA,EAAS,WAAW,eAAe,KACzD,CAAA,GACD,kBAAC,QAAD;MACE,WAAW,gEACTA,EAAS,WACL,sCACA;MAGL,UAAAA,EAAS;KACN,CAAA,CACA;IAvBD,GAAAA,EAAS,IAuBR,CACT;GACE,CAAA;GAGL,kBAAC,KAAD;IAAG,WAAU;IAAb,UAAA;KACGnF,EAAG,oCAAoC,0BAA0B;KAAG;KACrE,kBAAC,UAAD;MACE,MAAK;MACL,eAAeG,EAAS,aAAa;MACrC,WAAU;MAET,UAAAH,EAAG,wBAAwB,SAAS;KAC/B,CAAA;IACP;;EACO;;AAEhB"}
1
+ {"version":3,"file":"RegisterPage.js","names":["_Fragment","RegisterPage","tr","useTr","useAuth","navigate","useNavigate","searchParams","useSearchParams","rawReturnTo","returnTo","useMemo","sanitizeReturnPath","loginPath","authPathWithReturnTo","useAuthOperations","useOAuthOperations","showPassword","setShowPassword","useState","formData","setFormData","validationErrors","setValidationErrors","signupSuccess","setSignupSuccess","error","setError","oauthError","setOauthError","captchaToken","setCaptchaToken","captchaResetKey","setCaptchaResetKey","captchaEnabled","captchaSiteKey","useSignupCaptcha","isResendingVerification","setIsResendingVerification","resendError","setResendError","isAccountDeactivated","errorCode","isAccountRemoved","isAccountSuspended","passwordChecks","password","passwordStrength","metCount","c","handleSubmit","errors","validateEmail","userFieldsValidation","validateUserFields","passwordValidation","validatePassword","userFields","signUp","err","message","k","handleGoogleSignup","handleMicrosoftSignup","isGoogleConfigured","isProviderConfigured","isMicrosoftConfigured","oauthProviders","GoogleIcon","GitHubIcon","MicrosoftIcon","verificationEmailPrefix","verificationEmailSuffix","AuthLayout","Button","resendVerification","productName","PagePurpose","AlertIcon","UserIcon","EmailIcon","LockIcon","jsx","EyeOffIcon","EyeIcon","check","CheckIcon","XIcon","resolveLegalUrl","CaptchaV2","isLoading","Fragment","SpinnerIcon","provider"],"sources":["../../../src/auth/pages/RegisterPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { useNavigate, useSearchParams } from \"react-router\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport {\n validateEmail,\n validatePassword,\n validateUserFields,\n} from \"../utils/validation\";\nimport { resolveLegalUrl } from \"../utils/legalUrls\";\nimport {\n authPathWithReturnTo,\n rememberReturnTo,\n sanitizeReturnPath,\n} from \"../utils/pendingReturnTo\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useOAuthOperations } from \"../hooks/useOAuthOperations\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport {\n EmailIcon,\n LockIcon,\n UserIcon,\n EyeIcon,\n EyeOffIcon,\n AlertIcon,\n CheckIcon,\n XIcon,\n SpinnerIcon,\n GoogleIcon,\n GitHubIcon,\n MicrosoftIcon,\n} from \"../components/AuthIcons\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { CaptchaV2 } from \"../components/CaptchaV2\";\nimport { useSignupCaptcha } from \"../hooks/useSignupCaptcha\";\nimport type { SignUpInput } from \"../types\";\n\n/**\n * Registration page with professional design\n */\nexport function RegisterPage() {\n const tr = useTr();\n const { productName } = useAuth();\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const rawReturnTo = searchParams.get(\"returnTo\");\n // Where to go after the visitor verifies their email and signs in. Kept\n // across the verification email round trip (see pendingReturnTo.ts).\n const returnTo = useMemo(\n () => sanitizeReturnPath(rawReturnTo),\n [rawReturnTo],\n );\n useEffect(() => {\n rememberReturnTo(rawReturnTo);\n }, [rawReturnTo]);\n const loginPath = authPathWithReturnTo(\"/auth/login\", returnTo);\n\n const { signUp, resendVerification, isLoading, errorCode } =\n useAuthOperations();\n const { initiateGoogleOAuth, initiateMicrosoftOAuth, isProviderConfigured } =\n useOAuthOperations();\n const [showPassword, setShowPassword] = useState(false);\n const [formData, setFormData] = useState<SignUpInput>({\n email: \"\",\n username: \"\",\n password: \"\",\n firstName: \"\",\n lastName: \"\",\n });\n const [validationErrors, setValidationErrors] = useState<string[]>([]);\n const [signupSuccess, setSignupSuccess] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [oauthError, setOauthError] = useState<string | null>(null);\n const [captchaToken, setCaptchaToken] = useState<string | null>(null);\n // reCAPTCHA v2 tokens are single-use. Bumping this key remounts the widget so\n // a fresh token is issued after a failed submit (the prior token is consumed\n // server-side; reusing it returns timeout-or-duplicate → CAPTCHA_FAILED loop).\n const [captchaResetKey, setCaptchaResetKey] = useState(0);\n const { enabled: captchaEnabled, siteKey: captchaSiteKey } =\n useSignupCaptcha();\n const [isResendingVerification, setIsResendingVerification] = useState(false);\n const [resendError, setResendError] = useState<string | null>(null);\n\n // Signing up with an email that already belongs to a non-active account:\n // route the user to the right recovery path instead of dead-ending.\n const isAccountDeactivated =\n Boolean(error) && errorCode === \"ACCOUNT_DEACTIVATED\";\n const isAccountRemoved = Boolean(error) && errorCode === \"ACCOUNT_REMOVED\";\n const isAccountSuspended =\n Boolean(error) && errorCode === \"ACCOUNT_SUSPENDED\";\n\n // Password strength indicators\n const passwordChecks = (() => {\n const password = formData.password;\n return [\n {\n label: tr(\"auth.register.pwMin8\", \"At least 12 characters\"),\n met: password.length >= 12,\n },\n {\n label: tr(\"auth.register.pwUppercase\", \"One uppercase letter\"),\n met: /[A-Z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwLowercase\", \"One lowercase letter\"),\n met: /[a-z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwNumber\", \"One number\"),\n met: /\\d/.test(password),\n },\n {\n label: tr(\"auth.register.pwSpecial\", \"One special character\"),\n met: /[!@#$%^&*(),.?\":{}|<>]/.test(password),\n },\n ];\n })();\n\n const passwordStrength = (() => {\n const metCount = passwordChecks.filter((c) => c.met).length;\n if (metCount <= 2)\n return {\n level: tr(\"auth.register.passwordStrengthWeak\", \"weak\"),\n color: \"bg-status-error-bg-subtle0\",\n width: \"w-1/4\",\n };\n if (metCount <= 3)\n return {\n level: tr(\"auth.register.passwordStrengthFair\", \"fair\"),\n color: \"bg-status-warning-text\",\n width: \"w-2/4\",\n };\n if (metCount <= 4)\n return {\n level: tr(\"auth.register.passwordStrengthGood\", \"good\"),\n color: \"bg-status-warning-bg-subtle0\",\n width: \"w-3/4\",\n };\n return {\n level: tr(\"auth.register.passwordStrengthStrong\", \"strong\"),\n color: \"bg-status-success-text\",\n width: \"w-full\",\n };\n })();\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n const errors: string[] = [];\n if (!validateEmail(formData.email)) {\n errors.push(\n tr(\"auth.register.invalidEmail\", \"Please enter a valid email address\"),\n );\n }\n const userFieldsValidation = validateUserFields(formData);\n if (!userFieldsValidation.valid) {\n errors.push(\n ...userFieldsValidation.errors.map((e) =>\n tr(`auth.register.${e.field}_${e.code}`, e.message),\n ),\n );\n }\n const passwordValidation = validatePassword(formData.password);\n if (!passwordValidation.valid) {\n errors.push(...passwordValidation.errors);\n }\n\n if (captchaEnabled && !captchaToken) {\n errors.push(\n tr(\n \"auth.register.completeCaptcha\",\n \"Please complete the reCAPTCHA verification\",\n ),\n );\n }\n\n if (errors.length > 0) {\n setValidationErrors(errors);\n return;\n }\n\n setValidationErrors([]);\n setError(null);\n try {\n // Submit the canonicalised values the validators approved, not the\n // raw formData — so leading/trailing whitespace and typographic\n // apostrophes/hyphens don't reach the backend / DB. The cast is\n // sound because the `errors.length > 0` early return above would\n // have caught any failed validation.\n const userFields = (\n userFieldsValidation as {\n valid: true;\n values: { firstName: string; lastName: string; username: string };\n }\n ).values;\n await signUp({\n ...formData,\n firstName: userFields.firstName,\n lastName: userFields.lastName,\n username: userFields.username,\n ...(captchaToken ? { recaptchaToken: captchaToken } : {}),\n });\n // Sign up successful - check if email verification is required\n // (Backend returns empty tokens when email verification is needed)\n setSignupSuccess(true);\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\"auth.register.signUpFailed\", \"Sign up failed\");\n setError(message);\n console.error(\"Sign up failed:\", err);\n // The captcha token (if any) was consumed during this attempt — discard it\n // and remount the widget so the user gets a fresh, solvable challenge on\n // retry instead of being trapped in a CAPTCHA_FAILED loop.\n if (captchaEnabled) {\n setCaptchaToken(null);\n setCaptchaResetKey((k) => k + 1);\n }\n }\n };\n\n /**\n * Handle Google OAuth signup\n */\n const handleGoogleSignup = () => {\n setOauthError(null);\n try {\n initiateGoogleOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.googleSignUpFailed\",\n \"Failed to initiate Google sign up\",\n ),\n );\n }\n };\n\n /**\n * Handle Microsoft OAuth signup\n */\n const handleMicrosoftSignup = () => {\n setOauthError(null);\n try {\n initiateMicrosoftOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.microsoftSignUpFailed\",\n \"Failed to initiate Microsoft sign up\",\n ),\n );\n }\n };\n\n // Check if OAuth providers are configured\n const isGoogleConfigured = isProviderConfigured(\"google\");\n const isMicrosoftConfigured = isProviderConfigured(\"microsoft\");\n\n const oauthProviders = [\n {\n name: \"Google\",\n icon: GoogleIcon,\n onClick: handleGoogleSignup,\n disabled: !isGoogleConfigured,\n tooltip: isGoogleConfigured\n ? undefined\n : tr(\n \"auth.register.googleNotConfigured\",\n \"Google sign up is not configured\",\n ),\n },\n {\n name: \"GitHub\",\n icon: GitHubIcon,\n onClick: () => {},\n disabled: true,\n tooltip: tr(\n \"auth.register.githubNotConfigured\",\n \"GitHub sign up is not configured\",\n ),\n },\n {\n name: \"Microsoft\",\n icon: MicrosoftIcon,\n onClick: handleMicrosoftSignup,\n disabled: !isMicrosoftConfigured,\n tooltip: isMicrosoftConfigured\n ? undefined\n : tr(\n \"auth.register.microsoftNotConfigured\",\n \"Microsoft sign up is not configured\",\n ),\n },\n ];\n\n const verificationEmailMessage = tr(\n \"auth.register.verificationEmailSentTo\",\n \"We've sent a verification email to {{email}}. Please check your inbox and click the verification link to activate your account.\",\n );\n const [verificationEmailPrefix, verificationEmailSuffix = \"\"] =\n verificationEmailMessage.split(\"{{email}}\");\n\n // Show success state after signup\n if (signupSuccess) {\n return (\n <AuthLayout\n title={tr(\"auth.register.checkEmail\", \"Check your email\")}\n subtitle={tr(\n \"auth.register.verificationLinkSent\",\n \"We've sent you a verification link\",\n )}\n >\n <div className=\"flex flex-col items-center justify-center py-6\">\n {/* Success Icon */}\n <div className=\"size-20 rounded-full bg-status-success-bg-subtle flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <h3 className=\"text-lg font-semibold text-[var(--color-text-primary)] mb-2\">\n {tr(\n \"auth.register.accountCreated\",\n \"Account created successfully!\",\n )}\n </h3>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-8 max-w-sm\">\n {verificationEmailPrefix}\n <span className=\"font-medium text-[var(--color-text-primary)]\">\n {formData.email}\n </span>\n {verificationEmailSuffix}\n </p>\n\n <div className=\"space-y-3 w-full\">\n <Button\n type=\"button\"\n onClick={() => navigate(loginPath)}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {tr(\"auth.register.continueToSignIn\", \"Continue to sign in\")}\n </Button>\n\n {resendError && (\n <p className=\"text-center text-sm text-status-error-text\">\n {resendError}\n </p>\n )}\n <p className=\"text-center text-sm text-[var(--color-text-tertiary)]\">\n {tr(\n \"auth.register.didntReceiveEmail\",\n \"Didn't receive the email? Check your spam folder or\",\n )}{\" \"}\n <button\n type=\"button\"\n disabled={isResendingVerification}\n onClick={async () => {\n setResendError(null);\n setIsResendingVerification(true);\n try {\n await resendVerification(formData.email);\n } catch (err) {\n setResendError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.resendFailed\",\n \"Failed to resend verification email\",\n ),\n );\n } finally {\n setIsResendingVerification(false);\n }\n }}\n className=\"text-text-link hover:text-text-linkHover font-medium disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isResendingVerification\n ? tr(\"auth.register.resending\", \"resending...\")\n : tr(\"auth.register.tryAgain\", \"try again\")}\n </button>\n </p>\n </div>\n </div>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout\n title={tr(\"auth.register.createAccount\", \"Create your account\")}\n subtitle={tr(\n \"auth.register.startJourney\",\n \"Start your journey with {{productName}}\",\n ).replace(\"{{productName}}\", productName || \"Burdenoff Workspaces\")}\n productName={productName}\n >\n <PagePurpose className=\"mb-5\">\n {tr(\n \"auth.register.purpose\",\n \"Create your account to get a personal workspace and start collaborating. You'll verify your email, then you can sign in and invite your team.\",\n )}\n </PagePurpose>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Deactivated account → offer self-service reactivation */}\n {isAccountDeactivated && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n \"auth.register.accountDeactivated\",\n \"This email belongs to a deactivated account.\",\n )}\n </p>\n <p className=\"text-xs text-status-warning-text/80\">\n {tr(\n \"auth.register.accountDeactivatedHelp\",\n \"If this is your account, you can reactivate it instead of creating a new one.\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() =>\n navigate(\"/auth/reactivate\", {\n state: { email: formData.email },\n })\n }\n className=\"text-sm font-semibold text-status-warning-text underline underline-offset-2\"\n >\n {tr(\"auth.register.reactivateCta\", \"Reactivate my account\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Admin-removed / suspended → point to support (no self-service path) */}\n {(isAccountRemoved || isAccountSuspended) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <p className=\"text-sm text-status-warning-text\">{error}</p>\n </div>\n )}\n\n {/* Generic error / validation alert */}\n {((error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended) ||\n validationErrors.length > 0) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\">\n <AlertIcon className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-1\">\n {error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended && (\n <p className=\"text-sm text-status-error-text\">{error}</p>\n )}\n {validationErrors.map((err) => (\n <p key={err} className=\"text-sm text-status-error-text\">\n {err}\n </p>\n ))}\n </div>\n </div>\n )}\n\n {/* Name Fields */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <label\n htmlFor=\"firstName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.firstName\", \"First Name\")}\n </label>\n <input\n id=\"firstName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.firstName\", \"First Name\")}\n value={formData.firstName}\n onChange={(e) =>\n setFormData({ ...formData, firstName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.firstNamePlaceholder\",\n \"Enter first name\",\n )}\n />\n </div>\n <div className=\"space-y-2\">\n <label\n htmlFor=\"lastName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.lastName\", \"Last Name\")}\n </label>\n <input\n id=\"lastName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.lastName\", \"Last Name\")}\n value={formData.lastName}\n onChange={(e) =>\n setFormData({ ...formData, lastName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.lastNamePlaceholder\",\n \"Enter last name\",\n )}\n />\n </div>\n </div>\n\n {/* Username Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"username\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.username\", \"Username\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <UserIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"username\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.username\", \"Username\")}\n autoComplete=\"username\"\n value={formData.username}\n onChange={(e) =>\n setFormData({ ...formData, username: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.usernamePlaceholder\",\n \"Choose a username\",\n )}\n />\n </div>\n </div>\n\n {/* Email Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.email\", \"Email\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <EmailIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.email\", \"Email\")}\n autoComplete=\"email\"\n value={formData.email}\n onChange={(e) =>\n setFormData({ ...formData, email: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.emailPlaceholder\",\n \"you@example.com\",\n )}\n />\n </div>\n </div>\n\n {/* Password Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.password\", \"Password\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <LockIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.password\", \"Password\")}\n autoComplete=\"new-password\"\n value={formData.password}\n onChange={(e) =>\n setFormData({ ...formData, password: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-12 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.passwordPlaceholder\",\n \"Create a strong password\",\n )}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n aria-label={\n showPassword\n ? tr(\"auth.register.hidePassword\", \"Hide password\")\n : tr(\"auth.register.showPassword\", \"Show password\")\n }\n className=\"absolute inset-y-0 right-0 pr-3.5 flex items-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors\"\n >\n {showPassword ? (\n <EyeOffIcon className=\"size-5\" />\n ) : (\n <EyeIcon className=\"size-5\" />\n )}\n </button>\n </div>\n\n {/* Password Strength Indicator */}\n {formData.password && (\n <div className=\"space-y-3 pt-2\">\n {/* Strength Bar */}\n <div className=\"space-y-1\">\n <div className=\"h-1.5 bg-[var(--color-bg-sunken)] rounded-full overflow-hidden\">\n <div\n className={`h-full ${passwordStrength.color} ${passwordStrength.width} transition-all duration-300`}\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-tertiary)] capitalize\">\n {tr(\"auth.register.passwordStrength\", \"Password strength:\")}{\" \"}\n <span className=\"font-medium\">{passwordStrength.level}</span>\n </p>\n </div>\n\n {/* Requirements Checklist */}\n <div className=\"grid grid-cols-2 gap-2\">\n {passwordChecks.map((check) => (\n <div key={check.label} className=\"flex items-center gap-2\">\n {check.met ? (\n <CheckIcon className=\"size-4 text-status-success-text\" />\n ) : (\n <XIcon className=\"size-4 text-[var(--color-text-tertiary)]\" />\n )}\n <span\n className={`text-xs ${\n check.met\n ? \"text-status-success-text\"\n : \"text-[var(--color-text-tertiary)]\"\n }`}\n >\n {check.label}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n\n {/* Terms Agreement */}\n <p className=\"text-xs text-[var(--color-text-tertiary)] leading-relaxed\">\n {tr(\n \"auth.register.agreeToTerms\",\n \"By creating an account, you agree to our\",\n )}{\" \"}\n {/* These link to the PUBLIC marketing site, not an app route: the\n reader is signed out, and the app's /legal/:slug route sits behind\n the shell's auth guard. Opened in a new tab so a half-completed\n signup form is not discarded to read the terms. */}\n <a\n href={resolveLegalUrl(\"/terms\")}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.termsOfService\", \"Terms of Service\")}\n </a>{\" \"}\n {tr(\"auth.register.and\", \"and\")}{\" \"}\n <a\n href={resolveLegalUrl(\"/privacy\")}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.privacyPolicy\", \"Privacy Policy\")}\n </a>\n .\n </p>\n\n {/* reCAPTCHA v2 Checkbox — enabled per-product via the platform catalog.\n `key` remounts it after a failed submit so a fresh token is issued. */}\n <CaptchaV2\n key={captchaResetKey}\n enabled={captchaEnabled}\n siteKey={captchaSiteKey}\n onVerify={setCaptchaToken}\n onExpired={() => setCaptchaToken(null)}\n />\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n disabled={isLoading}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>\n {tr(\"auth.register.creatingAccount\", \"Creating account...\")}\n </span>\n </>\n ) : (\n tr(\"auth.register.createAccountBtn\", \"Create account\")\n )}\n </Button>\n </form>\n\n {/* Divider */}\n <div className=\"relative my-8\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-[var(--color-border-default)]\" />\n </div>\n <div className=\"relative flex justify-center\">\n <span className=\"px-4 bg-[var(--color-bg-canvas)] text-sm text-[var(--color-text-tertiary)]\">\n {tr(\"auth.register.orSignUpWith\", \"or sign up with\")}\n </span>\n </div>\n </div>\n\n {/* OAuth Error */}\n {oauthError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-error-bg-subtle border-status-error-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{oauthError}</p>\n </div>\n )}\n\n {/* OAuth Buttons */}\n <div className=\"grid grid-cols-3 gap-3\">\n {oauthProviders.map((provider) => (\n <button\n key={provider.name}\n type=\"button\"\n onClick={provider.onClick}\n disabled={provider.disabled}\n title={provider.tooltip}\n className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all duration-200 group ${\n provider.disabled\n ? \"border-[var(--color-border-subtle)] bg-[var(--color-bg-disabled)] cursor-not-allowed opacity-50\"\n : \"border-[var(--color-border-default)] bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-sunken)] hover:border-[var(--color-border-strong)]\"\n }`}\n >\n <provider.icon\n className={`size-5 ${provider.disabled ? \"opacity-50\" : \"\"}`}\n />\n <span\n className={`sr-only sm:not-sr-only text-sm font-medium transition-colors ${\n provider.disabled\n ? \"text-[var(--color-text-disabled)]\"\n : \"text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]\"\n }`}\n >\n {provider.name}\n </span>\n </button>\n ))}\n </div>\n\n {/* Sign In Link */}\n <p className=\"mt-8 text-center text-sm text-[var(--color-text-secondary)]\">\n {tr(\"auth.register.alreadyHaveAccount\", \"Already have an account?\")}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate(loginPath)}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.register.signIn\", \"Sign in\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Carry a post-sign-in destination across the email-verification round trip.
3
+ *
4
+ * A public entry point such as "Build on Workspaces" sends a new visitor to
5
+ * `/auth/register?returnTo=/devportal/setup`. Signing up does not sign the
6
+ * visitor in: they confirm their email from a link, land on
7
+ * `/auth/verify-email`, and only then sign in. Without help the `returnTo`
8
+ * is lost on the way, so a freshly registered developer ends up on the
9
+ * default dashboard instead of the page they came for (BOFF-10636).
10
+ *
11
+ * The register page remembers the destination here; the verify-email page
12
+ * hands it to the login page as an ordinary `returnTo` query parameter,
13
+ * which the login page already validates and honours.
14
+ *
15
+ * Only same-origin, non-auth paths are accepted, so a crafted link cannot
16
+ * turn this into an open redirect. The value is a plain path (no token, no
17
+ * identity) and expires with the verification link.
18
+ */
19
+ export declare const PENDING_RETURN_TO_KEY = "auth:pendingReturnTo";
20
+ /** Matches the 24-hour lifetime of the email verification link. */
21
+ export declare const PENDING_RETURN_TO_TTL_MS: number;
22
+ /**
23
+ * Normalise a candidate destination to a same-origin path, or return null.
24
+ *
25
+ * Expects the value as `URLSearchParams.get()` returns it (already decoded
26
+ * once) and does not decode it again, so percent-escapes inside the
27
+ * destination's own query survive and sanitising is idempotent. Rejects
28
+ * absolute URLs on another origin, protocol-relative (`//host`) and
29
+ * backslash tricks, non-http(s) schemes, embedded credentials and any
30
+ * `/auth/*` path (which would loop the visitor back into the sign-in flow).
31
+ */
32
+ export declare function sanitizeReturnPath(raw: string | null | undefined, origin?: string | null): string | null;
33
+ /**
34
+ * Remember a destination for after verification. Returns the sanitised path
35
+ * that was stored, or null when the value was rejected (an invalid value
36
+ * also clears any older one, so a stale destination never wins).
37
+ */
38
+ export declare function rememberReturnTo(raw: string | null | undefined, now?: number): string | null;
39
+ /** Read and remove the remembered destination, if it is still valid. */
40
+ export declare function consumeReturnTo(now?: number): string | null;
41
+ /** Build an auth route URL that carries `returnTo` (and optional email). */
42
+ export declare function authPathWithReturnTo(base: "/auth/login" | "/auth/register", returnTo: string | null | undefined, email?: string | null): string;
@@ -0,0 +1,74 @@
1
+ // src/auth/utils/pendingReturnTo.ts
2
+ var e = "auth:pendingReturnTo", t = 864e5;
3
+ function n() {
4
+ return typeof window > "u" ? null : window.location.origin;
5
+ }
6
+ function r(e, t = n()) {
7
+ if (!e || !t) return null;
8
+ let r = e.trim(), a = i(r, t);
9
+ if (!a) return null;
10
+ let o;
11
+ try {
12
+ o = decodeURIComponent(r);
13
+ } catch {
14
+ return null;
15
+ }
16
+ return i(o, t) ? a : null;
17
+ }
18
+ function i(e, t) {
19
+ if (!e.startsWith("/") || /^\/[/\\]/.test(e) || e.includes("\\")) return null;
20
+ try {
21
+ let n = new URL(e, t);
22
+ if (n.origin !== t || !["http:", "https:"].includes(n.protocol) || n.username || n.password || n.pathname === "/auth" || n.pathname.startsWith("/auth/")) return null;
23
+ let r = `${n.pathname}${n.search}${n.hash}`;
24
+ return /^\/[/\\]/.test(r) || r.includes("\\") ? null : r;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function a() {
30
+ try {
31
+ return typeof window > "u" ? null : window.localStorage;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function o(n, i = Date.now()) {
37
+ let o = r(n), s = a();
38
+ if (!s) return o;
39
+ try {
40
+ if (!o) return s.removeItem(e), null;
41
+ let n = {
42
+ path: o,
43
+ expiresAt: i + t
44
+ };
45
+ s.setItem(e, JSON.stringify(n));
46
+ } catch {}
47
+ return o;
48
+ }
49
+ function s(t = Date.now()) {
50
+ let n = a();
51
+ if (!n) return null;
52
+ let i = null;
53
+ try {
54
+ i = n.getItem(e), n.removeItem(e);
55
+ } catch {
56
+ return null;
57
+ }
58
+ if (!i) return null;
59
+ try {
60
+ let e = JSON.parse(i);
61
+ return typeof e.path != "string" || typeof e.expiresAt != "number" || e.expiresAt <= t ? null : r(e.path);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function c(e, t, n) {
67
+ let r = new URLSearchParams();
68
+ n && r.set("email", n), t && r.set("returnTo", t);
69
+ let i = r.toString();
70
+ return i ? `${e}?${i}` : e;
71
+ }
72
+ export { e as PENDING_RETURN_TO_KEY, t as PENDING_RETURN_TO_TTL_MS, c as authPathWithReturnTo, s as consumeReturnTo, o as rememberReturnTo, r as sanitizeReturnPath };
73
+
74
+ //# sourceMappingURL=pendingReturnTo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pendingReturnTo.js","names":["PENDING_RETURN_TO_KEY","PENDING_RETURN_TO_TTL_MS","currentOrigin","sanitizeReturnPath","raw","origin","candidate","normalized","safeSameOriginPath","decoded","url","path","storage","rememberReturnTo","store","value","now","consumeReturnTo","parsed","authPathWithReturnTo","params","email","returnTo","query","base"],"sources":["../../../src/auth/utils/pendingReturnTo.ts"],"sourcesContent":["/**\n * Carry a post-sign-in destination across the email-verification round trip.\n *\n * A public entry point such as \"Build on Workspaces\" sends a new visitor to\n * `/auth/register?returnTo=/devportal/setup`. Signing up does not sign the\n * visitor in: they confirm their email from a link, land on\n * `/auth/verify-email`, and only then sign in. Without help the `returnTo`\n * is lost on the way, so a freshly registered developer ends up on the\n * default dashboard instead of the page they came for (BOFF-10636).\n *\n * The register page remembers the destination here; the verify-email page\n * hands it to the login page as an ordinary `returnTo` query parameter,\n * which the login page already validates and honours.\n *\n * Only same-origin, non-auth paths are accepted, so a crafted link cannot\n * turn this into an open redirect. The value is a plain path (no token, no\n * identity) and expires with the verification link.\n */\n\nexport const PENDING_RETURN_TO_KEY = \"auth:pendingReturnTo\";\n\n/** Matches the 24-hour lifetime of the email verification link. */\nexport const PENDING_RETURN_TO_TTL_MS = 24 * 60 * 60 * 1000;\n\ninterface StoredReturnTo {\n path: string;\n expiresAt: number;\n}\n\nfunction currentOrigin(): string | null {\n if (typeof window === \"undefined\") return null;\n return window.location.origin;\n}\n\n/**\n * Normalise a candidate destination to a same-origin path, or return null.\n *\n * Expects the value as `URLSearchParams.get()` returns it (already decoded\n * once) and does not decode it again, so percent-escapes inside the\n * destination's own query survive and sanitising is idempotent. Rejects\n * absolute URLs on another origin, protocol-relative (`//host`) and\n * backslash tricks, non-http(s) schemes, embedded credentials and any\n * `/auth/*` path (which would loop the visitor back into the sign-in flow).\n */\nexport function sanitizeReturnPath(\n raw: string | null | undefined,\n origin: string | null = currentOrigin(),\n): string | null {\n if (!raw || !origin) return null;\n const candidate = raw.trim();\n const normalized = safeSameOriginPath(candidate, origin);\n if (!normalized) return null;\n // The login page decodes `returnTo` once more before using it, so a value\n // such as `/%2Fevil.example.com` would become `//evil.example.com` there.\n // Require the once-decoded form to be a safe same-origin path too, while\n // still storing the original (escapes intact).\n let decoded: string;\n try {\n decoded = decodeURIComponent(candidate);\n } catch {\n return null;\n }\n if (!safeSameOriginPath(decoded, origin)) return null;\n return normalized;\n}\n\n/** One validation pass; see sanitizeReturnPath for the rules. */\nfunction safeSameOriginPath(candidate: string, origin: string): string | null {\n if (!candidate.startsWith(\"/\") || /^\\/[/\\\\]/.test(candidate)) return null;\n if (candidate.includes(\"\\\\\")) return null;\n try {\n const url = new URL(candidate, origin);\n if (\n url.origin !== origin ||\n ![\"http:\", \"https:\"].includes(url.protocol) ||\n url.username ||\n url.password ||\n url.pathname === \"/auth\" ||\n url.pathname.startsWith(\"/auth/\")\n ) {\n return null;\n }\n const path = `${url.pathname}${url.search}${url.hash}`;\n // Dot-segment normalisation can turn `/x/..//host` into `//host`; check\n // the normalised result, not only the input.\n if (/^\\/[/\\\\]/.test(path) || path.includes(\"\\\\\")) return null;\n return path;\n } catch {\n return null;\n }\n}\n\nfunction storage(): Storage | null {\n try {\n return typeof window === \"undefined\" ? null : window.localStorage;\n } catch {\n return null;\n }\n}\n\n/**\n * Remember a destination for after verification. Returns the sanitised path\n * that was stored, or null when the value was rejected (an invalid value\n * also clears any older one, so a stale destination never wins).\n */\nexport function rememberReturnTo(\n raw: string | null | undefined,\n now: number = Date.now(),\n): string | null {\n const path = sanitizeReturnPath(raw);\n const store = storage();\n if (!store) return path;\n try {\n if (!path) {\n store.removeItem(PENDING_RETURN_TO_KEY);\n return null;\n }\n const value: StoredReturnTo = {\n path,\n expiresAt: now + PENDING_RETURN_TO_TTL_MS,\n };\n store.setItem(PENDING_RETURN_TO_KEY, JSON.stringify(value));\n } catch {\n // Storage full or blocked: the in-page links still carry the value.\n }\n return path;\n}\n\n/** Read and remove the remembered destination, if it is still valid. */\nexport function consumeReturnTo(now: number = Date.now()): string | null {\n const store = storage();\n if (!store) return null;\n let raw: string | null = null;\n try {\n raw = store.getItem(PENDING_RETURN_TO_KEY);\n store.removeItem(PENDING_RETURN_TO_KEY);\n } catch {\n return null;\n }\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<StoredReturnTo>;\n if (\n typeof parsed.path !== \"string\" ||\n typeof parsed.expiresAt !== \"number\" ||\n parsed.expiresAt <= now\n ) {\n return null;\n }\n // Re-validate: the stored value is browser-controlled input.\n return sanitizeReturnPath(parsed.path);\n } catch {\n return null;\n }\n}\n\n/** Build an auth route URL that carries `returnTo` (and optional email). */\nexport function authPathWithReturnTo(\n base: \"/auth/login\" | \"/auth/register\",\n returnTo: string | null | undefined,\n email?: string | null,\n): string {\n const params = new URLSearchParams();\n if (email) params.set(\"email\", email);\n if (returnTo) params.set(\"returnTo\", returnTo);\n const query = params.toString();\n return query ? `${base}?${query}` : base;\n}\n"],"mappings":";AAmBA,IAAaA,IAAwB,wBAGxBC,IAA2B;AAOxC,SAASC,IAA+B;CAEtC,OADI,OAAO,SAAW,MAAoB,OACnC,OAAO,SAAS;AACzB;AAYA,SAAgBC,EACd,GACA,IAAwBD,EAAc,GACvB;CACf,IAAI,CAACE,KAAO,CAACC,GAAQ,OAAO;CAC5B,IAAMC,IAAYF,EAAI,KAAK,GACrBG,IAAaC,EAAmBF,GAAWD,CAAM;CACvD,IAAI,CAACE,GAAY,OAAO;CAKxB,IAAIE;CACJ,IAAI;EACF,IAAU,mBAAmBH,CAAS;CACxC,QAAQ;EACN,OAAO;CACT;CAEA,OADKE,EAAmBC,GAASJ,CAAM,IAChCE,IAD0C;AAEnD;AAGA,SAASC,EAAmB,GAAmB,GAA+B;CAE5E,IADI,CAACF,EAAU,WAAW,GAAG,KAAK,WAAW,KAAKA,CAAS,KACvDA,EAAU,SAAS,IAAI,GAAG,OAAO;CACrC,IAAI;EACF,IAAMI,IAAM,IAAI,IAAIJ,GAAWD,CAAM;EACrC,IACEK,EAAI,WAAWL,KACf,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,SAASK,EAAI,QAAQ,KAC1CA,EAAI,YACJA,EAAI,YACJA,EAAI,aAAa,WACjBA,EAAI,SAAS,WAAW,QAAQ,GAEhC,OAAO;EAET,IAAMC,IAAO,GAAGD,EAAI,WAAWA,EAAI,SAASA,EAAI;EAIhD,OADI,WAAW,KAAKC,CAAI,KAAKA,EAAK,SAAS,IAAI,IAAU,OAClDA;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAASC,IAA0B;CACjC,IAAI;EACF,OAAO,OAAO,SAAW,MAAc,OAAO,OAAO;CACvD,QAAQ;EACN,OAAO;CACT;AACF;AAOA,SAAgBC,EACd,GACA,IAAc,KAAK,IAAI,GACR;CACf,IAAMF,IAAOR,EAAmBC,CAAG,GAC7BU,IAAQF,EAAQ;CACtB,IAAI,CAACE,GAAO,OAAOH;CACnB,IAAI;EACF,IAAI,CAACA,GAEH,OADAG,EAAM,WAAWd,CAAqB,GAC/B;EAET,IAAMe,IAAwB;GAC5B;GACA,WAAWC,IAAMf;EACnB;EACA,EAAM,QAAQD,GAAuB,KAAK,UAAUe,CAAK,CAAC;CAC5D,QAAQ,CAER;CACA,OAAOJ;AACT;AAGA,SAAgBM,EAAgB,IAAc,KAAK,IAAI,GAAkB;CACvE,IAAMH,IAAQF,EAAQ;CACtB,IAAI,CAACE,GAAO,OAAO;CACnB,IAAIV,IAAqB;CACzB,IAAI;EAEF,AADA,IAAMU,EAAM,QAAQd,CAAqB,GACzCc,EAAM,WAAWd,CAAqB;CACxC,QAAQ;EACN,OAAO;CACT;CACA,IAAI,CAACI,GAAK,OAAO;CACjB,IAAI;EACF,IAAMc,IAAS,KAAK,MAAMd,CAAG;EAS7B,OAPE,OAAOc,EAAO,QAAS,YACvB,OAAOA,EAAO,aAAc,YAC5BA,EAAO,aAAaF,IAEb,OAGFb,EAAmBe,EAAO,IAAI;CACvC,QAAQ;EACN,OAAO;CACT;AACF;AAGA,SAAgBC,EACd,GACA,GACA,GACQ;CACR,IAAMC,IAAS,IAAI,gBAAgB;CAEnC,AADIC,KAAOD,EAAO,IAAI,SAASC,CAAK,GAChCC,KAAUF,EAAO,IAAI,YAAYE,CAAQ;CAC7C,IAAMC,IAAQH,EAAO,SAAS;CAC9B,OAAOG,IAAQ,GAAGC,EAAK,GAAGD,MAAUC;AACtC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-auth",
3
- "version": "2026.1102.1",
3
+ "version": "2026.1103.1",
4
4
  "description": "Auth microfrontend for Burdenoff products",
5
5
  "type": "module",
6
6
  "imports": {