@burdenoff/microfe-auth 2026.1031.1 → 2026.1101.2

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.
Files changed (135) hide show
  1. package/dist/auth/AuthRoot.js +14 -11
  2. package/dist/auth/AuthRoot.js.map +1 -1
  3. package/dist/auth/AuthRoutes.js +1 -2
  4. package/dist/auth/AuthRoutes.js.map +1 -1
  5. package/dist/auth/components/AuthIcons.js +1 -2
  6. package/dist/auth/components/AuthIcons.js.map +1 -1
  7. package/dist/auth/components/AuthLayout.js +1 -2
  8. package/dist/auth/components/AuthLayout.js.map +1 -1
  9. package/dist/auth/components/CaptchaV2.js +9 -10
  10. package/dist/auth/components/CaptchaV2.js.map +1 -1
  11. package/dist/auth/components/ConfirmActionDialog.js +1 -2
  12. package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
  13. package/dist/auth/components/profile/UserProfileMenu.js +7 -6
  14. package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
  15. package/dist/auth/components/workspace/WorkspaceSelector.js +1 -2
  16. package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
  17. package/dist/auth/constants/permissions.js +1 -2
  18. package/dist/auth/constants/permissions.js.map +1 -1
  19. package/dist/auth/constants/sessionKeys.js +1 -2
  20. package/dist/auth/constants/sessionKeys.js.map +1 -1
  21. package/dist/auth/hooks/contextDeepLink.js +1 -2
  22. package/dist/auth/hooks/contextDeepLink.js.map +1 -1
  23. package/dist/auth/hooks/index.js +12 -11
  24. package/dist/auth/hooks/useAccountOperations.js +1 -2
  25. package/dist/auth/hooks/useAccountOperations.js.map +1 -1
  26. package/dist/auth/hooks/useActiveContextBootstrapMirror.js +1 -2
  27. package/dist/auth/hooks/useActiveContextBootstrapMirror.js.map +1 -1
  28. package/dist/auth/hooks/useAuthOperations.js +37 -37
  29. package/dist/auth/hooks/useAuthOperations.js.map +1 -1
  30. package/dist/auth/hooks/useAuthPermissions.js +1 -2
  31. package/dist/auth/hooks/useAuthPermissions.js.map +1 -1
  32. package/dist/auth/hooks/useCaptchaV3.js +1 -2
  33. package/dist/auth/hooks/useCaptchaV3.js.map +1 -1
  34. package/dist/auth/hooks/useContextSwitcherData.js +86 -79
  35. package/dist/auth/hooks/useContextSwitcherData.js.map +1 -1
  36. package/dist/auth/hooks/useMfaOperations.js +4 -5
  37. package/dist/auth/hooks/useMfaOperations.js.map +1 -1
  38. package/dist/auth/hooks/useMyWorkspaces.js +4 -5
  39. package/dist/auth/hooks/useMyWorkspaces.js.map +1 -1
  40. package/dist/auth/hooks/useOAuthOperations.js +11 -12
  41. package/dist/auth/hooks/useOAuthOperations.js.map +1 -1
  42. package/dist/auth/hooks/usePasswordOperations.js +1 -2
  43. package/dist/auth/hooks/usePasswordOperations.js.map +1 -1
  44. package/dist/auth/hooks/useSessionOperations.js +3 -4
  45. package/dist/auth/hooks/useSessionOperations.js.map +1 -1
  46. package/dist/auth/hooks/useSignupCaptcha.js +1 -2
  47. package/dist/auth/hooks/useSignupCaptcha.js.map +1 -1
  48. package/dist/auth/hooks/useSsoOperations.js +80 -81
  49. package/dist/auth/hooks/useSsoOperations.js.map +1 -1
  50. package/dist/auth/hooks/useTokenRefresh.js +8 -12
  51. package/dist/auth/hooks/useTokenRefresh.js.map +1 -1
  52. package/dist/auth/hooks/useUserOperations.js +1 -2
  53. package/dist/auth/hooks/useUserOperations.js.map +1 -1
  54. package/dist/auth/hooks/useWorkspaceOperations.js +1 -2
  55. package/dist/auth/hooks/useWorkspaceOperations.js.map +1 -1
  56. package/dist/auth/hooks/useWorkspaceToken.js +1 -2
  57. package/dist/auth/hooks/useWorkspaceToken.js.map +1 -1
  58. package/dist/auth/index.js +19 -18
  59. package/dist/auth/pages/AuditLogPage.js +21 -22
  60. package/dist/auth/pages/AuditLogPage.js.map +1 -1
  61. package/dist/auth/pages/EmailVerifyPage.js +15 -17
  62. package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
  63. package/dist/auth/pages/LoginPage.js +89 -105
  64. package/dist/auth/pages/LoginPage.js.map +1 -1
  65. package/dist/auth/pages/MagicLinkPage.js +30 -31
  66. package/dist/auth/pages/MagicLinkPage.js.map +1 -1
  67. package/dist/auth/pages/MfaSetupPage.js +18 -20
  68. package/dist/auth/pages/MfaSetupPage.js.map +1 -1
  69. package/dist/auth/pages/MfaVerifyPage.js +31 -35
  70. package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
  71. package/dist/auth/pages/OAuthCallbackPage.js +4 -11
  72. package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
  73. package/dist/auth/pages/PasswordResetPage.js +20 -23
  74. package/dist/auth/pages/PasswordResetPage.js.map +1 -1
  75. package/dist/auth/pages/ReactivatePage.js +14 -18
  76. package/dist/auth/pages/ReactivatePage.js.map +1 -1
  77. package/dist/auth/pages/RegisterPage.js +180 -181
  78. package/dist/auth/pages/RegisterPage.js.map +1 -1
  79. package/dist/auth/pages/SSOClearPage.js +9 -10
  80. package/dist/auth/pages/SSOClearPage.js.map +1 -1
  81. package/dist/auth/pages/SSODepositPage.js +9 -13
  82. package/dist/auth/pages/SSODepositPage.js.map +1 -1
  83. package/dist/auth/pages/SSORelayCallbackPage.js +3 -7
  84. package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
  85. package/dist/auth/pages/SSORelayPage.js +11 -10
  86. package/dist/auth/pages/SSORelayPage.js.map +1 -1
  87. package/dist/auth/pages/SecurityPage.js +19 -21
  88. package/dist/auth/pages/SecurityPage.js.map +1 -1
  89. package/dist/auth/pages/SessionBridgePage.js +9 -16
  90. package/dist/auth/pages/SessionBridgePage.js.map +1 -1
  91. package/dist/auth/pages/SessionsPage.js +17 -18
  92. package/dist/auth/pages/SessionsPage.js.map +1 -1
  93. package/dist/auth/pages/SsoCallbackPage.js +17 -22
  94. package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
  95. package/dist/auth/pages/SsoConfigPage.js +69 -69
  96. package/dist/auth/pages/SsoConfigPage.js.map +1 -1
  97. package/dist/auth/pages/SsoLoginPage.js +9 -10
  98. package/dist/auth/pages/SsoLoginPage.js.map +1 -1
  99. package/dist/auth/pages/UserDetailsPage.js +16 -23
  100. package/dist/auth/pages/UserDetailsPage.js.map +1 -1
  101. package/dist/auth/pages/UserProfilePage.js +140 -141
  102. package/dist/auth/pages/UserProfilePage.js.map +1 -1
  103. package/dist/auth/pages/UsersPage.js +446 -453
  104. package/dist/auth/pages/UsersPage.js.map +1 -1
  105. package/dist/auth/providers/AuthProvider.js +1 -2
  106. package/dist/auth/providers/AuthProvider.js.map +1 -1
  107. package/dist/auth/providers/CaptchaProvider.js +1 -2
  108. package/dist/auth/providers/CaptchaProvider.js.map +1 -1
  109. package/dist/auth/store/authStore.js +5 -12
  110. package/dist/auth/store/authStore.js.map +1 -1
  111. package/dist/auth/utils/captchaConfig.js +5 -8
  112. package/dist/auth/utils/captchaConfig.js.map +1 -1
  113. package/dist/auth/utils/formatters.js +1 -2
  114. package/dist/auth/utils/formatters.js.map +1 -1
  115. package/dist/auth/utils/gatewayBaseUrls.js +1 -2
  116. package/dist/auth/utils/gatewayBaseUrls.js.map +1 -1
  117. package/dist/auth/utils/legalUrls.js +2 -3
  118. package/dist/auth/utils/legalUrls.js.map +1 -1
  119. package/dist/auth/utils/redirect.js +1 -2
  120. package/dist/auth/utils/redirect.js.map +1 -1
  121. package/dist/auth/utils/ssoOrigins.js +2 -3
  122. package/dist/auth/utils/ssoOrigins.js.map +1 -1
  123. package/dist/auth/utils/validation.js +34 -36
  124. package/dist/auth/utils/validation.js.map +1 -1
  125. package/dist/generated/global-operations.js +2 -3
  126. package/dist/generated/global-operations.js.map +1 -1
  127. package/dist/generated/wspace-operations.js +2 -3
  128. package/dist/generated/wspace-operations.js.map +1 -1
  129. package/dist/microfe-auth.css +1 -1
  130. package/dist/shared/hooks/useTr.js +1 -2
  131. package/dist/shared/hooks/useTr.js.map +1 -1
  132. package/dist/translations/en.js +1 -2
  133. package/dist/translations/en.js.map +1 -1
  134. package/dist/utils/nativeBridge.js +1 -2
  135. package/package.json +7 -3
@@ -1 +1 @@
1
- {"version":3,"file":"RegisterPage.js","names":[],"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":"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"}
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,5 +1,5 @@
1
- import { useTr as e } from "../../shared/hooks/useTr.js";
2
- import { isAllowedSsoOrigin as t } from "../utils/ssoOrigins.js";
1
+ import { isAllowedSsoOrigin as e } from "../utils/ssoOrigins.js";
2
+ import { useTr as t } from "../../shared/hooks/useTr.js";
3
3
  import { useEffect as n } from "react";
4
4
  import { clearLocalAuthState as r } from "@burdenoff/fe-libs/shared/components";
5
5
  import { useSearchParams as i } from "react-router";
@@ -7,26 +7,25 @@ import { jsx as a } from "react/jsx-runtime";
7
7
  import { clearShellSsoCookies as o } from "@burdenoff/fe-libs/shared/utils";
8
8
  import { BrandedSplash as s } from "@burdenoff/fe-libs/ui";
9
9
  import { getAuthBridge as c } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
10
- //#region src/auth/pages/SSOClearPage.tsx
11
- function l(e) {
12
- if (!e) return "/auth/login?reason=logout";
10
+ // src/auth/pages/SSOClearPage.tsx
11
+ function l(t) {
12
+ if (!t) return "/auth/login?reason=logout";
13
13
  try {
14
- let n = new URL(e, window.location.origin);
15
- if (n.origin === window.location.origin || t(n)) return n.href;
14
+ let n = new URL(t, window.location.origin);
15
+ if (n.origin === window.location.origin || e(n)) return n.href;
16
16
  } catch {}
17
17
  return "/auth/login?reason=logout";
18
18
  }
19
19
  function u() {
20
- let t = e(), [u] = i();
20
+ let e = t(), [u] = i();
21
21
  return n(() => {
22
22
  let e = l(u.get("returnTo"));
23
23
  try {
24
24
  r(), o(), c()?.notifyLogoutAll();
25
25
  } catch {}
26
26
  window.location.replace(e);
27
- }, [u]), /* @__PURE__ */ a(s, { label: t("auth.ssoCallback.signingOut", "Signing you out…") });
27
+ }, [u]), /* @__PURE__ */ a(s, { label: e("auth.ssoCallback.signingOut", "Signing you out…") });
28
28
  }
29
- //#endregion
30
29
  export { u as SSOClearPage };
31
30
 
32
31
  //# sourceMappingURL=SSOClearPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SSOClearPage.js","names":[],"sources":["../../../src/auth/pages/SSOClearPage.tsx"],"sourcesContent":["/**\n * SSOClearPage — Cross-Domain Logout at the Central Bridge Host\n *\n * Companion to SSODepositPage. When a user signs out on a product origin,\n * the product app cleared its own localStorage / cookies but the bridge\n * host (alphaapp/app.burdenoff.com) still held the deposited session in\n * first-party (unpartitioned) localStorage. A later visit to any product\n * would then bridge-host-relay that stale session back, effectively\n * undoing the logout.\n *\n * AuthProvider.logout() / logoutAll() on a non-bridge-host origin redirect\n * here via a top-level navigation:\n *\n * product.com (logout)\n * → app.burdenoff.com/auth/sso-clear?returnTo=<encoded-return-url>\n * — wipes bridge-host localStorage + bf-sso cookie\n * → <returnTo>\n *\n * URL: /auth/sso-clear\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { clearShellSsoCookies } from \"@burdenoff/fe-libs/shared/utils\";\nimport { clearLocalAuthState } from \"@burdenoff/fe-libs/shared/components\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/auth/login?reason=logout\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n\n try {\n clearLocalAuthState();\n clearShellSsoCookies();\n // Broadcast to any same-origin shells still listening so they drop the\n // session immediately too (best-effort — only fires on the bridge\n // host's origin, which is the only place where iframe storage isn't\n // partitioned cross-product).\n getAuthBridge()?.notifyLogoutAll();\n } catch {\n /* ignore — proceed with the redirect regardless */\n }\n\n window.location.replace(returnTo);\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingOut\", \"Signing you out…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;;AA8BA,SAAS,EAAkB,GAAiC;CAC1D,IAAI,CAAC,GAAU,OAAO;CACtB,IAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,MAAM;EACpD,IAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,CAAG,GACjE,OAAO,EAAI;CAEf,QAAQ,CAER;CACA,OAAO;AACT;AAEA,SAAgB,IAAe;CAC7B,IAAM,IAAK,EAAM,GACX,CAAC,KAAgB,EAAgB;CAoBvC,OAlBA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,UAAU,CAAC;EAE/D,IAAI;GAOF,AANA,EAAoB,GACpB,EAAqB,GAKrB,EAAc,GAAG,gBAAgB;EACnC,QAAQ,CAER;EAEA,OAAO,SAAS,QAAQ,CAAQ;CAClC,GAAG,CAAC,CAAY,CAAC,GAGf,kBAAC,GAAD,EACE,OAAO,EAAG,+BAA+B,kBAAkB,EAC5D,CAAA;AAEL"}
1
+ {"version":3,"file":"SSOClearPage.js","names":["resolveSafeReturn","returnTo","url","isAllowedSsoOrigin","SSOClearPage","tr","useTr","searchParams","useSearchParams","useEffect","clearLocalAuthState","clearShellSsoCookies","getAuthBridge","BrandedSplash"],"sources":["../../../src/auth/pages/SSOClearPage.tsx"],"sourcesContent":["/**\n * SSOClearPage — Cross-Domain Logout at the Central Bridge Host\n *\n * Companion to SSODepositPage. When a user signs out on a product origin,\n * the product app cleared its own localStorage / cookies but the bridge\n * host (alphaapp/app.burdenoff.com) still held the deposited session in\n * first-party (unpartitioned) localStorage. A later visit to any product\n * would then bridge-host-relay that stale session back, effectively\n * undoing the logout.\n *\n * AuthProvider.logout() / logoutAll() on a non-bridge-host origin redirect\n * here via a top-level navigation:\n *\n * product.com (logout)\n * → app.burdenoff.com/auth/sso-clear?returnTo=<encoded-return-url>\n * — wipes bridge-host localStorage + bf-sso cookie\n * → <returnTo>\n *\n * URL: /auth/sso-clear\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { clearShellSsoCookies } from \"@burdenoff/fe-libs/shared/utils\";\nimport { clearLocalAuthState } from \"@burdenoff/fe-libs/shared/components\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/auth/login?reason=logout\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n\n try {\n clearLocalAuthState();\n clearShellSsoCookies();\n // Broadcast to any same-origin shells still listening so they drop the\n // session immediately too (best-effort — only fires on the bridge\n // host's origin, which is the only place where iframe storage isn't\n // partitioned cross-product).\n getAuthBridge()?.notifyLogoutAll();\n } catch {\n /* ignore — proceed with the redirect regardless */\n }\n\n window.location.replace(returnTo);\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingOut\", \"Signing you out…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;;AA8BA,SAASA,EAAkB,GAAiC;CAC1D,IAAI,CAACC,GAAU,OAAO;CACtB,IAAI;EACF,IAAMC,IAAM,IAAI,IAAID,GAAU,OAAO,SAAS,MAAM;EACpD,IAAIC,EAAI,WAAW,OAAO,SAAS,UAAUC,EAAmBD,CAAG,GACjE,OAAOA,EAAI;CAEf,QAAQ,CAER;CACA,OAAO;AACT;AAEA,SAAgBE,IAAe;CAC7B,IAAMC,IAAKC,EAAM,GACX,CAACC,KAAgBC,EAAgB;CAoBvC,OAlBAC,QAAgB;EACd,IAAMR,IAAWD,EAAkBO,EAAa,IAAI,UAAU,CAAC;EAE/D,IAAI;GAOF,AANAG,EAAoB,GACpBC,EAAqB,GAKrBC,EAAc,CAAC,EAAE,gBAAgB;EACnC,QAAQ,CAER;EAEA,OAAO,SAAS,QAAQX,CAAQ;CAClC,GAAG,CAACM,CAAY,CAAC,GAGf,kBAACM,GAAD,EACE,OAAOR,EAAG,+BAA+B,kBAAkB,EAC5D,CAAA;AAEL"}
@@ -1,30 +1,27 @@
1
1
  import { useAuthStore as e } from "../store/authStore.js";
2
- import { useTr as t } from "../../shared/hooks/useTr.js";
3
- import { isAllowedSsoOrigin as n } from "../utils/ssoOrigins.js";
2
+ import { isAllowedSsoOrigin as t } from "../utils/ssoOrigins.js";
3
+ import { useTr as n } from "../../shared/hooks/useTr.js";
4
4
  import { useEffect as r } from "react";
5
5
  import { useSearchParams as i } from "react-router";
6
6
  import { jsx as a } from "react/jsx-runtime";
7
7
  import { setSSoCookie as o } from "@burdenoff/fe-libs/shared/utils";
8
8
  import { BrandedSplash as s } from "@burdenoff/fe-libs/ui";
9
9
  import { getAuthBridge as c } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
10
- //#region src/auth/pages/SSODepositPage.tsx
10
+ // src/auth/pages/SSODepositPage.tsx
11
11
  function l(e) {
12
12
  if (!e) return "/";
13
13
  try {
14
- let t = new URL(e, window.location.origin);
15
- if (t.origin === window.location.origin || n(t)) return t.href;
14
+ let n = new URL(e, window.location.origin);
15
+ if (n.origin === window.location.origin || t(n)) return n.href;
16
16
  } catch {}
17
17
  return "/";
18
18
  }
19
19
  function u() {
20
- let n = t(), [u] = i();
20
+ let t = n(), [u] = i();
21
21
  return r(() => {
22
22
  let t = l(u.get("returnTo")), n = window.location.hash;
23
- if (!n.startsWith("#sso=")) {
24
- window.location.replace(t);
25
- return;
26
- }
27
- try {
23
+ if (!n.startsWith("#sso=")) window.location.replace(t);
24
+ else try {
28
25
  let r = n.slice(5), { profileId: i, tokens: a, user: s, profiles: l } = JSON.parse(atob(r));
29
26
  if (!i || !a) {
30
27
  window.location.replace(t);
@@ -39,9 +36,8 @@ function u() {
39
36
  } catch {
40
37
  window.location.replace(t);
41
38
  }
42
- }, [u]), /* @__PURE__ */ a(s, { label: n("auth.ssoCallback.signingIn", "Signing you in…") });
39
+ }, [u]), /* @__PURE__ */ a(s, { label: t("auth.ssoCallback.signingIn", "Signing you in…") });
43
40
  }
44
- //#endregion
45
41
  export { u as SSODepositPage };
46
42
 
47
43
  //# sourceMappingURL=SSODepositPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SSODepositPage.js","names":[],"sources":["../../../src/auth/pages/SSODepositPage.tsx"],"sourcesContent":["/**\n * SSODepositPage — Cross-Domain Token Deposit at the Central Bridge Host\n *\n * Loaded as a top-level navigation (NOT an iframe) on the bridge host\n * (alphaapp/app.burdenoff.com) right after a user signs in on any other\n * product origin. It receives the freshly-issued tokens in the URL fragment\n * (`#sso=<base64>` — fragments are never sent to servers) and stores them\n * in this origin's first-party (unpartitioned) localStorage so that:\n *\n * 1. A later top-level visit to the bridge host renders authenticated.\n * 2. Other product origins can discover the session via\n * `/auth/sso-relay?returnTo=...` against this host.\n *\n * Flow:\n * product.com/auth/login → user signs in\n * product.com → POST-success redirect to:\n * app.burdenoff.com/auth/sso-deposit?returnTo=<encoded-return-url>#sso=<base64>\n * → writes tokens here, sets bf-sso cookie\n * <returnTo> → user lands back at intended dashboard\n *\n * URL: /auth/sso-deposit\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n window.location.replace(returnTo);\n return;\n }\n\n try {\n const encoded = hash.slice(5);\n const payload = JSON.parse(atob(encoded)) as {\n profileId?: string;\n tokens?: string;\n user?: string;\n profiles?: string;\n };\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(returnTo);\n return;\n }\n\n // First-party (unpartitioned) writes on the bridge host origin —\n // future top-level visits here see the session.\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the same-origin auth bridge so any other open shells on\n // this origin pick up the new session immediately.\n getAuthBridge()?.notifyLogin({\n user: user ? (JSON.parse(user) as Record<string, unknown>) : null,\n tokens: tokens ? (JSON.parse(tokens) as { accessToken: string }) : null,\n activeProfileId: profileId,\n profiles: profiles\n ? (JSON.parse(profiles) as Array<Record<string, unknown>>)\n : [],\n });\n\n window.location.replace(returnTo);\n } catch {\n window.location.replace(returnTo);\n }\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingIn\", \"Signing you in…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAS,EAAkB,GAAiC;CAC1D,IAAI,CAAC,GAAU,OAAO;CACtB,IAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,MAAM;EACpD,IAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,CAAG,GACjE,OAAO,EAAI;CAEf,QAAQ,CAER;CACA,OAAO;AACT;AAEA,SAAgB,IAAiB;CAC/B,IAAM,IAAK,EAAM,GACX,CAAC,KAAgB,EAAgB;CAyDvC,OAvDA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,UAAU,CAAC,GACzD,IAAO,OAAO,SAAS;EAE7B,IAAI,CAAC,EAAK,WAAW,OAAO,GAAG;GAC7B,OAAO,SAAS,QAAQ,CAAQ;GAChC;EACF;EAEA,IAAI;GACF,IAAM,IAAU,EAAK,MAAM,CAAC,GAOtB,EAAE,cAAW,WAAQ,SAAM,gBANjB,KAAK,MAAM,KAAK,CAAO,CAMO;GAE9C,IAAI,CAAC,KAAa,CAAC,GAAQ;IACzB,OAAO,SAAS,QAAQ,CAAQ;IAChC;GACF;GA2BA,AAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,CAAM,GACnD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,CAAI,GAEjD,KACF,aAAa,QAAQ,eAAe,CAAQ,GAE9C,eAAe,QAAQ,qBAAqB,CAAS,GACrD,EAAa,CAAS,GACtB,EAAa,SAAS,EAAE,gBAAgB,CAAS,GACjD,OAAO,cAAc,IAAI,MAAM,2BAA2B,CAAC,GAI3D,EAAc,GAAG,YAAY;IAC3B,MAAM,IAAQ,KAAK,MAAM,CAAI,IAAgC;IAC7D,QAAQ,IAAU,KAAK,MAAM,CAAM,IAAgC;IACnE,iBAAiB;IACjB,UAAU,IACL,KAAK,MAAM,CAAQ,IACpB,CAAC;GACP,CAAC,GAED,OAAO,SAAS,QAAQ,CAAQ;EAClC,QAAQ;GACN,OAAO,SAAS,QAAQ,CAAQ;EAClC;CACF,GAAG,CAAC,CAAY,CAAC,GAGf,kBAAC,GAAD,EACE,OAAO,EAAG,8BAA8B,iBAAiB,EAC1D,CAAA;AAEL"}
1
+ {"version":3,"file":"SSODepositPage.js","names":["resolveSafeReturn","returnTo","url","isAllowedSsoOrigin","SSODepositPage","tr","useTr","searchParams","useSearchParams","useEffect","hash","encoded","profileId","tokens","user","profiles","setSSoCookie","useAuthStore","getAuthBridge","BrandedSplash"],"sources":["../../../src/auth/pages/SSODepositPage.tsx"],"sourcesContent":["/**\n * SSODepositPage — Cross-Domain Token Deposit at the Central Bridge Host\n *\n * Loaded as a top-level navigation (NOT an iframe) on the bridge host\n * (alphaapp/app.burdenoff.com) right after a user signs in on any other\n * product origin. It receives the freshly-issued tokens in the URL fragment\n * (`#sso=<base64>` — fragments are never sent to servers) and stores them\n * in this origin's first-party (unpartitioned) localStorage so that:\n *\n * 1. A later top-level visit to the bridge host renders authenticated.\n * 2. Other product origins can discover the session via\n * `/auth/sso-relay?returnTo=...` against this host.\n *\n * Flow:\n * product.com/auth/login → user signs in\n * product.com → POST-success redirect to:\n * app.burdenoff.com/auth/sso-deposit?returnTo=<encoded-return-url>#sso=<base64>\n * → writes tokens here, sets bf-sso cookie\n * <returnTo> → user lands back at intended dashboard\n *\n * URL: /auth/sso-deposit\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n window.location.replace(returnTo);\n return;\n }\n\n try {\n const encoded = hash.slice(5);\n const payload = JSON.parse(atob(encoded)) as {\n profileId?: string;\n tokens?: string;\n user?: string;\n profiles?: string;\n };\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(returnTo);\n return;\n }\n\n // First-party (unpartitioned) writes on the bridge host origin —\n // future top-level visits here see the session.\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the same-origin auth bridge so any other open shells on\n // this origin pick up the new session immediately.\n getAuthBridge()?.notifyLogin({\n user: user ? (JSON.parse(user) as Record<string, unknown>) : null,\n tokens: tokens ? (JSON.parse(tokens) as { accessToken: string }) : null,\n activeProfileId: profileId,\n profiles: profiles\n ? (JSON.parse(profiles) as Array<Record<string, unknown>>)\n : [],\n });\n\n window.location.replace(returnTo);\n } catch {\n window.location.replace(returnTo);\n }\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingIn\", \"Signing you in…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAASA,EAAkB,GAAiC;CAC1D,IAAI,CAACC,GAAU,OAAO;CACtB,IAAI;EACF,IAAMC,IAAM,IAAI,IAAID,GAAU,OAAO,SAAS,MAAM;EACpD,IAAIC,EAAI,WAAW,OAAO,SAAS,UAAUC,EAAmBD,CAAG,GACjE,OAAOA,EAAI;CAEf,QAAQ,CAER;CACA,OAAO;AACT;AAEA,SAAgBE,IAAiB;CAC/B,IAAMC,IAAKC,EAAM,GACX,CAACC,KAAgBC,EAAgB;CAyDvC,OAvDAC,QAAgB;EACd,IAAMR,IAAWD,EAAkBO,EAAa,IAAI,UAAU,CAAC,GACzDG,IAAO,OAAO,SAAS;EAE7B,IAAI,CAACA,EAAK,WAAW,OAAO,GAC1B,OAAO,SAAS,QAAQT,CAAQ;OAIlC,IAAI;GACF,IAAMU,IAAUD,EAAK,MAAM,CAAC,GAOtB,EAAE,cAAW,WAAQ,SAAM,gBANjB,KAAK,MAAM,KAAKC,CAAO,CAMO;GAE9C,IAAI,CAACC,KAAa,CAACC,GAAQ;IACzB,OAAO,SAAS,QAAQZ,CAAQ;IAChC;GACF;GA2BA,AAvBA,aAAa,QAAQ,QAAQW,EAAU,UAAUC,CAAM,GACnDC,KACF,aAAa,QAAQ,QAAQF,EAAU,QAAQE,CAAI,GAEjDC,KACF,aAAa,QAAQ,eAAeA,CAAQ,GAE9C,eAAe,QAAQ,qBAAqBH,CAAS,GACrDI,EAAaJ,CAAS,GACtBK,EAAa,SAAS,CAAC,CAAC,gBAAgBL,CAAS,GACjD,OAAO,cAAc,IAAI,MAAM,2BAA2B,CAAC,GAI3DM,EAAc,CAAC,EAAE,YAAY;IAC3B,MAAMJ,IAAQ,KAAK,MAAMA,CAAI,IAAgC;IAC7D,QAAQD,IAAU,KAAK,MAAMA,CAAM,IAAgC;IACnE,iBAAiBD;IACjB,UAAUG,IACL,KAAK,MAAMA,CAAQ,IACpB,CAAC;GACP,CAAC,GAED,OAAO,SAAS,QAAQd,CAAQ;EAClC,QAAQ;GACN,OAAO,SAAS,QAAQA,CAAQ;EAClC;CACF,GAAG,CAACM,CAAY,CAAC,GAGf,kBAACY,GAAD,EACE,OAAOd,EAAG,8BAA8B,iBAAiB,EAC1D,CAAA;AAEL"}
@@ -6,7 +6,7 @@ import { jsx as i } from "react/jsx-runtime";
6
6
  import { setSSoCookie as a } from "@burdenoff/fe-libs/shared/utils";
7
7
  import { BrandedSplash as o } from "@burdenoff/fe-libs/ui";
8
8
  import { getAuthBridge as s } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
9
- //#region src/auth/pages/SSORelayCallbackPage.tsx
9
+ // src/auth/pages/SSORelayCallbackPage.tsx
10
10
  function c(e) {
11
11
  if (!e) return "/dashboard";
12
12
  try {
@@ -20,11 +20,8 @@ function l() {
20
20
  let l = t(), [u] = r();
21
21
  return n(() => {
22
22
  let t = c(u.get("returnTo")), n = window.location.hash;
23
- if (!n.startsWith("#sso=")) {
24
- window.location.replace("/auth/login");
25
- return;
26
- }
27
- try {
23
+ if (!n.startsWith("#sso=")) window.location.replace("/auth/login");
24
+ else try {
28
25
  let r = n.slice(5), { profileId: i, tokens: o, user: c, profiles: l } = JSON.parse(atob(r));
29
26
  if (!i || !o) {
30
27
  window.location.replace("/auth/login");
@@ -41,7 +38,6 @@ function l() {
41
38
  }
42
39
  }, [u]), /* @__PURE__ */ i(o, { label: l("auth.ssoCallback.signingIn", "Signing you in…") });
43
40
  }
44
- //#endregion
45
41
  export { l as SSORelayCallbackPage };
46
42
 
47
43
  //# sourceMappingURL=SSORelayCallbackPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SSORelayCallbackPage.js","names":[],"sources":["../../../src/auth/pages/SSORelayCallbackPage.tsx"],"sourcesContent":["/**\n * SSORelayCallbackPage — Receives tokens from the SSO relay and stores them locally.\n *\n * Tokens arrive in the URL fragment (#sso=<base64>) which is never sent to servers.\n * After storing in localStorage, redirects to the returnTo path.\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction getSafeReturnTo(returnTo: string | null): string {\n if (!returnTo) return \"/dashboard\";\n\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin !== window.location.origin) return \"/dashboard\";\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return \"/dashboard\";\n }\n}\n\nexport function SSORelayCallbackPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = getSafeReturnTo(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n // No SSO data — redirect to login\n window.location.replace(\"/auth/login\");\n return;\n }\n\n try {\n const encoded = hash.slice(5); // Remove \"#sso=\"\n const payload = JSON.parse(atob(encoded));\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(\"/auth/login\");\n return;\n }\n\n // Store tokens in localStorage\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n\n // Dispatch event so AuthProvider picks up the new state\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the auth bridge so other open shells can pick up the new session\n getAuthBridge()?.notifyLogin({\n user: user ? JSON.parse(user) : null,\n tokens: tokens ? JSON.parse(tokens) : null,\n activeProfileId: profileId,\n profiles: profiles ? JSON.parse(profiles) : [],\n });\n\n // Clear the hash and redirect\n window.location.replace(returnTo);\n } catch {\n window.location.replace(\"/auth/login\");\n }\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingIn\", \"Signing you in…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;AAeA,SAAS,EAAgB,GAAiC;CACxD,IAAI,CAAC,GAAU,OAAO;CAEtB,IAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,MAAM;EAEpD,OADI,EAAI,WAAW,OAAO,SAAS,SAC5B,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,SADQ;CAEpD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,IAAuB;CACrC,IAAM,IAAK,EAAM,GACX,CAAC,KAAgB,EAAgB;CAoDvC,OAlDA,QAAgB;EACd,IAAM,IAAW,EAAgB,EAAa,IAAI,UAAU,CAAC,GACvD,IAAO,OAAO,SAAS;EAE7B,IAAI,CAAC,EAAK,WAAW,OAAO,GAAG;GAE7B,OAAO,SAAS,QAAQ,aAAa;GACrC;EACF;EAEA,IAAI;GACF,IAAM,IAAU,EAAK,MAAM,CAAC,GAEtB,EAAE,cAAW,WAAQ,SAAM,gBADjB,KAAK,MAAM,KAAK,CAAO,CACO;GAE9C,IAAI,CAAC,KAAa,CAAC,GAAQ;IACzB,OAAO,SAAS,QAAQ,aAAa;IACrC;GACF;GA0BA,AAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,CAAM,GACnD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,CAAI,GAEjD,KACF,aAAa,QAAQ,eAAe,CAAQ,GAE9C,eAAe,QAAQ,qBAAqB,CAAS,GACrD,EAAa,CAAS,GACtB,EAAa,SAAS,EAAE,gBAAgB,CAAS,GAGjD,OAAO,cAAc,IAAI,MAAM,2BAA2B,CAAC,GAG3D,EAAc,GAAG,YAAY;IAC3B,MAAM,IAAO,KAAK,MAAM,CAAI,IAAI;IAChC,QAAQ,IAAS,KAAK,MAAM,CAAM,IAAI;IACtC,iBAAiB;IACjB,UAAU,IAAW,KAAK,MAAM,CAAQ,IAAI,CAAC;GAC/C,CAAC,GAGD,OAAO,SAAS,QAAQ,CAAQ;EAClC,QAAQ;GACN,OAAO,SAAS,QAAQ,aAAa;EACvC;CACF,GAAG,CAAC,CAAY,CAAC,GAGf,kBAAC,GAAD,EACE,OAAO,EAAG,8BAA8B,iBAAiB,EAC1D,CAAA;AAEL"}
1
+ {"version":3,"file":"SSORelayCallbackPage.js","names":["getSafeReturnTo","returnTo","url","SSORelayCallbackPage","tr","useTr","searchParams","useSearchParams","useEffect","hash","encoded","profileId","tokens","user","profiles","setSSoCookie","useAuthStore","getAuthBridge","BrandedSplash"],"sources":["../../../src/auth/pages/SSORelayCallbackPage.tsx"],"sourcesContent":["/**\n * SSORelayCallbackPage — Receives tokens from the SSO relay and stores them locally.\n *\n * Tokens arrive in the URL fragment (#sso=<base64>) which is never sent to servers.\n * After storing in localStorage, redirects to the returnTo path.\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction getSafeReturnTo(returnTo: string | null): string {\n if (!returnTo) return \"/dashboard\";\n\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin !== window.location.origin) return \"/dashboard\";\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return \"/dashboard\";\n }\n}\n\nexport function SSORelayCallbackPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = getSafeReturnTo(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n // No SSO data — redirect to login\n window.location.replace(\"/auth/login\");\n return;\n }\n\n try {\n const encoded = hash.slice(5); // Remove \"#sso=\"\n const payload = JSON.parse(atob(encoded));\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(\"/auth/login\");\n return;\n }\n\n // Store tokens in localStorage\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n\n // Dispatch event so AuthProvider picks up the new state\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the auth bridge so other open shells can pick up the new session\n getAuthBridge()?.notifyLogin({\n user: user ? JSON.parse(user) : null,\n tokens: tokens ? JSON.parse(tokens) : null,\n activeProfileId: profileId,\n profiles: profiles ? JSON.parse(profiles) : [],\n });\n\n // Clear the hash and redirect\n window.location.replace(returnTo);\n } catch {\n window.location.replace(\"/auth/login\");\n }\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingIn\", \"Signing you in…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;AAeA,SAASA,EAAgB,GAAiC;CACxD,IAAI,CAACC,GAAU,OAAO;CAEtB,IAAI;EACF,IAAMC,IAAM,IAAI,IAAID,GAAU,OAAO,SAAS,MAAM;EAEpD,OADIC,EAAI,WAAW,OAAO,SAAS,SAC5B,GAAGA,EAAI,WAAWA,EAAI,SAASA,EAAI,SADQ;CAEpD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgBC,IAAuB;CACrC,IAAMC,IAAKC,EAAM,GACX,CAACC,KAAgBC,EAAgB;CAoDvC,OAlDAC,QAAgB;EACd,IAAMP,IAAWD,EAAgBM,EAAa,IAAI,UAAU,CAAC,GACvDG,IAAO,OAAO,SAAS;EAE7B,IAAI,CAACA,EAAK,WAAW,OAAO,GAE1B,OAAO,SAAS,QAAQ,aAAa;OAIvC,IAAI;GACF,IAAMC,IAAUD,EAAK,MAAM,CAAC,GAEtB,EAAE,cAAW,WAAQ,SAAM,gBADjB,KAAK,MAAM,KAAKC,CAAO,CACO;GAE9C,IAAI,CAACC,KAAa,CAACC,GAAQ;IACzB,OAAO,SAAS,QAAQ,aAAa;IACrC;GACF;GA0BA,AAvBA,aAAa,QAAQ,QAAQD,EAAU,UAAUC,CAAM,GACnDC,KACF,aAAa,QAAQ,QAAQF,EAAU,QAAQE,CAAI,GAEjDC,KACF,aAAa,QAAQ,eAAeA,CAAQ,GAE9C,eAAe,QAAQ,qBAAqBH,CAAS,GACrDI,EAAaJ,CAAS,GACtBK,EAAa,SAAS,CAAC,CAAC,gBAAgBL,CAAS,GAGjD,OAAO,cAAc,IAAI,MAAM,2BAA2B,CAAC,GAG3DM,EAAc,CAAC,EAAE,YAAY;IAC3B,MAAMJ,IAAO,KAAK,MAAMA,CAAI,IAAI;IAChC,QAAQD,IAAS,KAAK,MAAMA,CAAM,IAAI;IACtC,iBAAiBD;IACjB,UAAUG,IAAW,KAAK,MAAMA,CAAQ,IAAI,CAAC;GAC/C,CAAC,GAGD,OAAO,SAAS,QAAQb,CAAQ;EAClC,QAAQ;GACN,OAAO,SAAS,QAAQ,aAAa;EACvC;CACF,GAAG,CAACK,CAAY,CAAC,GAGf,kBAACY,GAAD,EACE,OAAOd,EAAG,8BAA8B,iBAAiB,EAC1D,CAAA;AAEL"}
@@ -5,8 +5,8 @@ import { jsx as r } from "react/jsx-runtime";
5
5
  import { mintSsoSessionTokens as i } from "@burdenoff/fe-libs/shared/utils";
6
6
  import { resolveAuthBridgeConfig as a } from "@burdenoff/fe-libs/shared/config";
7
7
  import { resolveGatewayUrls as o } from "@burdenoff/fe-libs/shared/config/gatewayUrls";
8
- //#region src/auth/pages/SSORelayPage.tsx
9
- var s = "relayChain", c = "sso_failed", l = new Set(["localhost", "127.0.0.1"]), u = {
8
+ // src/auth/pages/SSORelayPage.tsx
9
+ var s = "relayChain", c = "sso_failed", l = /* @__PURE__ */ new Set(["localhost", "127.0.0.1"]), u = {
10
10
  "bf-sso": /(?:^|;\s*)bf-sso=([^;]*)/,
11
11
  "bf-sso-profile": /(?:^|;\s*)bf-sso-profile=([^;]*)/
12
12
  };
@@ -72,7 +72,7 @@ function _(t) {
72
72
  let a = f();
73
73
  if (a.includes(r.origin) || a.length >= 3) return !1;
74
74
  let o = new URL("/auth/sso-relay", r.origin);
75
- return o.searchParams.set("returnTo", t), o.searchParams.set(s, Array.from(new Set([...a, window.location.origin])).join("|")), window.location.replace(o.href), !0;
75
+ return o.searchParams.set("returnTo", t), o.searchParams.set(s, Array.from(/* @__PURE__ */ new Set([...a, window.location.origin])).join("|")), window.location.replace(o.href), !0;
76
76
  } catch {
77
77
  return !1;
78
78
  }
@@ -156,11 +156,13 @@ function C() {
156
156
  return;
157
157
  }
158
158
  }
159
- if (!m) if (f?.accessToken && typeof f.expiresAt == "number" && f.expiresAt - Date.now() > 6e4) m = s;
160
- else {
161
- if (x(r), _(n)) return;
162
- d(), window.location.replace(p(n));
163
- return;
159
+ if (!m) {
160
+ if (f?.accessToken && typeof f.expiresAt == "number" && f.expiresAt - Date.now() > 6e4) m = s;
161
+ else {
162
+ if (x(r), _(n)) return;
163
+ d(), window.location.replace(p(n));
164
+ return;
165
+ }
164
166
  }
165
167
  let h = btoa(JSON.stringify({
166
168
  profileId: r,
@@ -177,7 +179,6 @@ function C() {
177
179
  children: /* @__PURE__ */ r("div", { className: "animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent" })
178
180
  });
179
181
  }
180
- //#endregion
181
- export { C as SSORelayPage };
182
+ export { C as SSORelayPage, _ as redirectToCookieRelayOrigin };
182
183
 
183
184
  //# sourceMappingURL=SSORelayPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SSORelayPage.js","names":[],"sources":["../../../src/auth/pages/SSORelayPage.tsx"],"sourcesContent":["/**\n * SSORelayPage — Cross-domain SSO token relay.\n *\n * This page is loaded on the ORIGIN where the user is logged in.\n * It reads tokens from localStorage and redirects to the TARGET domain's\n * /auth/sso-callback with the tokens in a URL fragment (not query param\n * for security — fragments are not sent to servers).\n *\n * Flow:\n * 1. User visits productA.com → not logged in → detects SSO cookie\n * 2. Redirect to productB.com/auth/sso-relay?returnTo=productA.com/dashboard\n * (productB is where the user IS logged in — identified by cookie)\n * 3. This page reads tokens from productB's localStorage\n * 4. Redirects to productA.com/auth/sso-callback#token=<base64-encoded-tokens>\n * 5. SSORelayCallbackPage on productA decodes and stores tokens, redirects to /dashboard\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { resolveAuthBridgeConfig } from \"@burdenoff/fe-libs/shared/config\";\nimport { resolveGatewayUrls } from \"@burdenoff/fe-libs/shared/config/gatewayUrls\";\nimport { mintSsoSessionTokens } from \"@burdenoff/fe-libs/shared/utils\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\n\nconst RELAY_CHAIN_PARAM = \"relayChain\";\nconst RELAY_FAILED_PARAM = \"sso_failed\";\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\"]);\n\nconst SSO_COOKIE_PATTERNS = {\n \"bf-sso\": /(?:^|;\\s*)bf-sso=([^;]*)/,\n \"bf-sso-profile\": /(?:^|;\\s*)bf-sso-profile=([^;]*)/,\n} as const;\n\nfunction clearRelayCookies(): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n document.cookie = `${cookieName}=; path=/; max-age=0`;\n if (domain) {\n document.cookie = `${cookieName}=; path=/; domain=${domain}; max-age=0`;\n }\n if (hostname && !LOCAL_HOSTNAMES.has(hostname)) {\n document.cookie = `${cookieName}=; path=/; domain=${hostname}; max-age=0`;\n }\n }\n } catch {\n /* best effort */\n }\n}\n\nfunction getRelayChain(): string[] {\n try {\n return (\n new URLSearchParams(window.location.search).get(RELAY_CHAIN_PARAM) || \"\"\n )\n .split(\"|\")\n .filter((origin) => origin.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction getFailedReturnTo(returnTo: string): string {\n try {\n const targetUrl = new URL(returnTo, window.location.origin);\n if (\n targetUrl.origin !== window.location.origin &&\n !isAllowedSsoOrigin(targetUrl)\n ) {\n return \"/auth/login\";\n }\n targetUrl.searchParams.set(RELAY_FAILED_PARAM, \"1\");\n return targetUrl.href;\n } catch {\n return \"/auth/login?sso_failed=1\";\n }\n}\n\nfunction readSsoCookieProfileId(): string | null {\n return readSsoCookieData()?.id ?? null;\n}\n\nfunction readSsoCookieData(): {\n id: string | null;\n origin: string | null;\n} | null {\n try {\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n const match = document.cookie.match(SSO_COOKIE_PATTERNS[cookieName]);\n if (!match) continue;\n\n const parsed = JSON.parse(decodeURIComponent(match[1])) as\n | string\n | { id?: unknown; origin?: unknown };\n if (typeof parsed === \"string\") return { id: parsed, origin: null };\n if (typeof parsed.id === \"string\" && parsed.id.length > 0) {\n return {\n id: parsed.id,\n origin: typeof parsed.origin === \"string\" ? parsed.origin : null,\n };\n }\n }\n } catch {\n return null;\n }\n return null;\n}\n\nfunction resolveBridgeOrigin(): string | null {\n try {\n return new URL(resolveAuthBridgeConfig().url).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * Follow the SSO cookie's origin hint only when it is safe to do so.\n *\n * - On a product app shell, the only safe cross-origin redirect is to the\n * central bridge host. A stale cookie pointing at a different product must\n * not strand the user on another product's login page (BOFF-6701).\n * - On the central bridge host, the cookie may legitimately point at a product\n * origin that holds the session, so we allow the hop out to that product.\n */\nexport function redirectToCookieRelayOrigin(returnTo: string): boolean {\n try {\n const ssoCookie = readSsoCookieData();\n if (!ssoCookie?.origin) return false;\n\n const relayUrl = new URL(ssoCookie.origin);\n if (!isAllowedSsoOrigin(relayUrl)) return false;\n if (relayUrl.origin === window.location.origin) return false;\n\n const bridgeOrigin = resolveBridgeOrigin();\n const currentOrigin = window.location.origin;\n const isBridgeHost = bridgeOrigin === currentOrigin;\n if (!isBridgeHost && relayUrl.origin !== bridgeOrigin) {\n return false;\n }\n\n const relayChain = getRelayChain();\n if (relayChain.includes(relayUrl.origin) || relayChain.length >= 3) {\n return false;\n }\n\n const nextRelayUrl = new URL(\"/auth/sso-relay\", relayUrl.origin);\n nextRelayUrl.searchParams.set(\"returnTo\", returnTo);\n nextRelayUrl.searchParams.set(\n RELAY_CHAIN_PARAM,\n Array.from(new Set([...relayChain, window.location.origin])).join(\"|\"),\n );\n\n window.location.replace(nextRelayUrl.href);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction readProfilesRaw(): string | null {\n return localStorage.getItem(\"bf-profiles\");\n}\n\nfunction readProfileIds(): string[] {\n try {\n const profilesRaw = readProfilesRaw();\n const profiles = profilesRaw\n ? (JSON.parse(profilesRaw) as Array<{ id?: unknown }>)\n : [];\n return profiles\n .map((profile) => profile.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction resolveRelayProfileId(): string | null {\n const candidates = [\n readSsoCookieProfileId(),\n sessionStorage.getItem(\"bf-active-profile\"),\n ...readProfileIds(),\n ].filter((id): id is string => typeof id === \"string\" && id.length > 0);\n\n for (const profileId of Array.from(new Set(candidates))) {\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n if (tokensRaw && userRaw) {\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n return profileId;\n }\n }\n\n return null;\n}\n\n/** Wipe a dead/stale profile session from THIS origin's storage so future\n * relays stop offering it (BOFF-4463). */\nfunction clearStaleRelayProfile(profileId: string): void {\n try {\n localStorage.removeItem(`bf-p-${profileId}-tokens`);\n localStorage.removeItem(`bf-p-${profileId}-user`);\n localStorage.removeItem(`bf-p-${profileId}-workspace-token`);\n localStorage.removeItem(`bf-p-${profileId}-auth-storage`);\n sessionStorage.removeItem(\"bf-active-profile\");\n } catch {\n /* best effort */\n }\n}\n\nfunction parseStoredTokens(tokensRaw: string): {\n accessToken?: string;\n refreshToken?: string;\n expiresAt?: number;\n} | null {\n try {\n return JSON.parse(tokensRaw) as {\n accessToken?: string;\n refreshToken?: string;\n expiresAt?: number;\n };\n } catch {\n return null;\n }\n}\n\nexport function SSORelayPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n let cancelled = false;\n\n const run = async () => {\n const returnTo = searchParams.get(\"returnTo\");\n if (!returnTo) {\n // No relay target — there is nothing to relay to. Do NOT strand the user\n // on a perpetual spinner (HM-016): send them to the app root, which routes\n // to the dashboard when authenticated or the landing/login when not.\n window.location.replace(\"/\");\n return;\n }\n\n // Read tokens from localStorage (this origin has them). Prefer the shared\n // SSO cookie profile because a relay often runs in a new tab with empty\n // sessionStorage and stale historical profiles in localStorage.\n const profileId = resolveRelayProfileId();\n if (!profileId) {\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n const profilesRaw = readProfilesRaw();\n\n if (!tokensRaw || !userRaw) {\n // No tokens — redirect back without them\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n // Validate the target BEFORE any server round-trip.\n let targetUrl: URL;\n try {\n targetUrl = new URL(returnTo);\n } catch {\n // returnTo is a relative path — can't do cross-domain relay\n window.location.replace(returnTo);\n return;\n }\n if (!isAllowedSsoOrigin(targetUrl)) {\n window.location.replace(\"/\");\n return;\n }\n\n // BOFF-4463: relay a FRESH, dedicated token lineage to the target\n // instead of copying this origin's pair. Copied pairs share one\n // rotating refresh token — the first origin to refresh revoked it for\n // every other origin, and this page previously relayed even EXPIRED\n // pairs, dropping the target into a session that force-logged-out\n // minutes later.\n const stored = parseStoredTokens(tokensRaw);\n let relayTokensJson: string | null = null;\n\n if (stored?.refreshToken) {\n const { globalBaseUrl } = resolveGatewayUrls();\n const { tokens: minted, denied } = await mintSsoSessionTokens(\n `${globalBaseUrl}/global/graphql`,\n stored.refreshToken,\n );\n if (cancelled) return;\n if (minted) {\n relayTokensJson = JSON.stringify(minted);\n } else if (denied) {\n // The session behind this stored pair is dead (revoked/rotated\n // away). Never hand it to the target — clear it and fail over.\n clearStaleRelayProfile(profileId);\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n }\n\n if (!relayTokensJson) {\n // Mint unavailable (rollout gap / transient). Legacy fallback is only\n // safe while the stored access token is actually still valid.\n const accessStillValid =\n !!stored?.accessToken &&\n typeof stored.expiresAt === \"number\" &&\n stored.expiresAt - Date.now() > 60_000;\n if (accessStillValid) {\n relayTokensJson = tokensRaw;\n } else {\n clearStaleRelayProfile(profileId);\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n }\n\n // Encode the auth state as a base64 URL fragment\n const payload = {\n profileId,\n tokens: relayTokensJson,\n user: userRaw,\n profiles: profilesRaw,\n };\n const encoded = btoa(JSON.stringify(payload));\n\n const returnPath = targetUrl.pathname + targetUrl.search;\n const callbackUrl = `${targetUrl.origin}/auth/sso-callback?returnTo=${encodeURIComponent(returnPath)}#sso=${encoded}`;\n\n // Redirect to the callback\n window.location.replace(callbackUrl);\n };\n\n void run();\n return () => {\n cancelled = true;\n };\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SSORelayPage.js","names":["RELAY_CHAIN_PARAM","RELAY_FAILED_PARAM","LOCAL_HOSTNAMES","SSO_COOKIE_PATTERNS","clearRelayCookies","hostname","domain","cookieName","getRelayChain","origin","getFailedReturnTo","targetUrl","returnTo","isAllowedSsoOrigin","readSsoCookieProfileId","readSsoCookieData","match","parsed","resolveBridgeOrigin","resolveAuthBridgeConfig","redirectToCookieRelayOrigin","ssoCookie","relayUrl","bridgeOrigin","relayChain","nextRelayUrl","readProfilesRaw","readProfileIds","profilesRaw","profile","id","resolveRelayProfileId","candidates","profileId","tokensRaw","userRaw","clearStaleRelayProfile","parseStoredTokens","SSORelayPage","searchParams","useSearchParams","useEffect","cancelled","stored","relayTokensJson","resolveGatewayUrls","minted","mintSsoSessionTokens","globalBaseUrl","denied","encoded","returnPath","callbackUrl"],"sources":["../../../src/auth/pages/SSORelayPage.tsx"],"sourcesContent":["/**\n * SSORelayPage — Cross-domain SSO token relay.\n *\n * This page is loaded on the ORIGIN where the user is logged in.\n * It reads tokens from localStorage and redirects to the TARGET domain's\n * /auth/sso-callback with the tokens in a URL fragment (not query param\n * for security — fragments are not sent to servers).\n *\n * Flow:\n * 1. User visits productA.com → not logged in → detects SSO cookie\n * 2. Redirect to productB.com/auth/sso-relay?returnTo=productA.com/dashboard\n * (productB is where the user IS logged in — identified by cookie)\n * 3. This page reads tokens from productB's localStorage\n * 4. Redirects to productA.com/auth/sso-callback#token=<base64-encoded-tokens>\n * 5. SSORelayCallbackPage on productA decodes and stores tokens, redirects to /dashboard\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { resolveAuthBridgeConfig } from \"@burdenoff/fe-libs/shared/config\";\nimport { resolveGatewayUrls } from \"@burdenoff/fe-libs/shared/config/gatewayUrls\";\nimport { mintSsoSessionTokens } from \"@burdenoff/fe-libs/shared/utils\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\n\nconst RELAY_CHAIN_PARAM = \"relayChain\";\nconst RELAY_FAILED_PARAM = \"sso_failed\";\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\"]);\n\nconst SSO_COOKIE_PATTERNS = {\n \"bf-sso\": /(?:^|;\\s*)bf-sso=([^;]*)/,\n \"bf-sso-profile\": /(?:^|;\\s*)bf-sso-profile=([^;]*)/,\n} as const;\n\nfunction clearRelayCookies(): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n document.cookie = `${cookieName}=; path=/; max-age=0`;\n if (domain) {\n document.cookie = `${cookieName}=; path=/; domain=${domain}; max-age=0`;\n }\n if (hostname && !LOCAL_HOSTNAMES.has(hostname)) {\n document.cookie = `${cookieName}=; path=/; domain=${hostname}; max-age=0`;\n }\n }\n } catch {\n /* best effort */\n }\n}\n\nfunction getRelayChain(): string[] {\n try {\n return (\n new URLSearchParams(window.location.search).get(RELAY_CHAIN_PARAM) || \"\"\n )\n .split(\"|\")\n .filter((origin) => origin.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction getFailedReturnTo(returnTo: string): string {\n try {\n const targetUrl = new URL(returnTo, window.location.origin);\n if (\n targetUrl.origin !== window.location.origin &&\n !isAllowedSsoOrigin(targetUrl)\n ) {\n return \"/auth/login\";\n }\n targetUrl.searchParams.set(RELAY_FAILED_PARAM, \"1\");\n return targetUrl.href;\n } catch {\n return \"/auth/login?sso_failed=1\";\n }\n}\n\nfunction readSsoCookieProfileId(): string | null {\n return readSsoCookieData()?.id ?? null;\n}\n\nfunction readSsoCookieData(): {\n id: string | null;\n origin: string | null;\n} | null {\n try {\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n const match = document.cookie.match(SSO_COOKIE_PATTERNS[cookieName]);\n if (!match) continue;\n\n const parsed = JSON.parse(decodeURIComponent(match[1])) as\n | string\n | { id?: unknown; origin?: unknown };\n if (typeof parsed === \"string\") return { id: parsed, origin: null };\n if (typeof parsed.id === \"string\" && parsed.id.length > 0) {\n return {\n id: parsed.id,\n origin: typeof parsed.origin === \"string\" ? parsed.origin : null,\n };\n }\n }\n } catch {\n return null;\n }\n return null;\n}\n\nfunction resolveBridgeOrigin(): string | null {\n try {\n return new URL(resolveAuthBridgeConfig().url).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * Follow the SSO cookie's origin hint only when it is safe to do so.\n *\n * - On a product app shell, the only safe cross-origin redirect is to the\n * central bridge host. A stale cookie pointing at a different product must\n * not strand the user on another product's login page (BOFF-6701).\n * - On the central bridge host, the cookie may legitimately point at a product\n * origin that holds the session, so we allow the hop out to that product.\n */\nexport function redirectToCookieRelayOrigin(returnTo: string): boolean {\n try {\n const ssoCookie = readSsoCookieData();\n if (!ssoCookie?.origin) return false;\n\n const relayUrl = new URL(ssoCookie.origin);\n if (!isAllowedSsoOrigin(relayUrl)) return false;\n if (relayUrl.origin === window.location.origin) return false;\n\n const bridgeOrigin = resolveBridgeOrigin();\n const currentOrigin = window.location.origin;\n const isBridgeHost = bridgeOrigin === currentOrigin;\n if (!isBridgeHost && relayUrl.origin !== bridgeOrigin) {\n return false;\n }\n\n const relayChain = getRelayChain();\n if (relayChain.includes(relayUrl.origin) || relayChain.length >= 3) {\n return false;\n }\n\n const nextRelayUrl = new URL(\"/auth/sso-relay\", relayUrl.origin);\n nextRelayUrl.searchParams.set(\"returnTo\", returnTo);\n nextRelayUrl.searchParams.set(\n RELAY_CHAIN_PARAM,\n Array.from(new Set([...relayChain, window.location.origin])).join(\"|\"),\n );\n\n window.location.replace(nextRelayUrl.href);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction readProfilesRaw(): string | null {\n return localStorage.getItem(\"bf-profiles\");\n}\n\nfunction readProfileIds(): string[] {\n try {\n const profilesRaw = readProfilesRaw();\n const profiles = profilesRaw\n ? (JSON.parse(profilesRaw) as Array<{ id?: unknown }>)\n : [];\n return profiles\n .map((profile) => profile.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction resolveRelayProfileId(): string | null {\n const candidates = [\n readSsoCookieProfileId(),\n sessionStorage.getItem(\"bf-active-profile\"),\n ...readProfileIds(),\n ].filter((id): id is string => typeof id === \"string\" && id.length > 0);\n\n for (const profileId of Array.from(new Set(candidates))) {\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n if (tokensRaw && userRaw) {\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n return profileId;\n }\n }\n\n return null;\n}\n\n/** Wipe a dead/stale profile session from THIS origin's storage so future\n * relays stop offering it (BOFF-4463). */\nfunction clearStaleRelayProfile(profileId: string): void {\n try {\n localStorage.removeItem(`bf-p-${profileId}-tokens`);\n localStorage.removeItem(`bf-p-${profileId}-user`);\n localStorage.removeItem(`bf-p-${profileId}-workspace-token`);\n localStorage.removeItem(`bf-p-${profileId}-auth-storage`);\n sessionStorage.removeItem(\"bf-active-profile\");\n } catch {\n /* best effort */\n }\n}\n\nfunction parseStoredTokens(tokensRaw: string): {\n accessToken?: string;\n refreshToken?: string;\n expiresAt?: number;\n} | null {\n try {\n return JSON.parse(tokensRaw) as {\n accessToken?: string;\n refreshToken?: string;\n expiresAt?: number;\n };\n } catch {\n return null;\n }\n}\n\nexport function SSORelayPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n let cancelled = false;\n\n const run = async () => {\n const returnTo = searchParams.get(\"returnTo\");\n if (!returnTo) {\n // No relay target — there is nothing to relay to. Do NOT strand the user\n // on a perpetual spinner (HM-016): send them to the app root, which routes\n // to the dashboard when authenticated or the landing/login when not.\n window.location.replace(\"/\");\n return;\n }\n\n // Read tokens from localStorage (this origin has them). Prefer the shared\n // SSO cookie profile because a relay often runs in a new tab with empty\n // sessionStorage and stale historical profiles in localStorage.\n const profileId = resolveRelayProfileId();\n if (!profileId) {\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n const profilesRaw = readProfilesRaw();\n\n if (!tokensRaw || !userRaw) {\n // No tokens — redirect back without them\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n // Validate the target BEFORE any server round-trip.\n let targetUrl: URL;\n try {\n targetUrl = new URL(returnTo);\n } catch {\n // returnTo is a relative path — can't do cross-domain relay\n window.location.replace(returnTo);\n return;\n }\n if (!isAllowedSsoOrigin(targetUrl)) {\n window.location.replace(\"/\");\n return;\n }\n\n // BOFF-4463: relay a FRESH, dedicated token lineage to the target\n // instead of copying this origin's pair. Copied pairs share one\n // rotating refresh token — the first origin to refresh revoked it for\n // every other origin, and this page previously relayed even EXPIRED\n // pairs, dropping the target into a session that force-logged-out\n // minutes later.\n const stored = parseStoredTokens(tokensRaw);\n let relayTokensJson: string | null = null;\n\n if (stored?.refreshToken) {\n const { globalBaseUrl } = resolveGatewayUrls();\n const { tokens: minted, denied } = await mintSsoSessionTokens(\n `${globalBaseUrl}/global/graphql`,\n stored.refreshToken,\n );\n if (cancelled) return;\n if (minted) {\n relayTokensJson = JSON.stringify(minted);\n } else if (denied) {\n // The session behind this stored pair is dead (revoked/rotated\n // away). Never hand it to the target — clear it and fail over.\n clearStaleRelayProfile(profileId);\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n }\n\n if (!relayTokensJson) {\n // Mint unavailable (rollout gap / transient). Legacy fallback is only\n // safe while the stored access token is actually still valid.\n const accessStillValid =\n !!stored?.accessToken &&\n typeof stored.expiresAt === \"number\" &&\n stored.expiresAt - Date.now() > 60_000;\n if (accessStillValid) {\n relayTokensJson = tokensRaw;\n } else {\n clearStaleRelayProfile(profileId);\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n }\n\n // Encode the auth state as a base64 URL fragment\n const payload = {\n profileId,\n tokens: relayTokensJson,\n user: userRaw,\n profiles: profilesRaw,\n };\n const encoded = btoa(JSON.stringify(payload));\n\n const returnPath = targetUrl.pathname + targetUrl.search;\n const callbackUrl = `${targetUrl.origin}/auth/sso-callback?returnTo=${encodeURIComponent(returnPath)}#sso=${encoded}`;\n\n // Redirect to the callback\n window.location.replace(callbackUrl);\n };\n\n void run();\n return () => {\n cancelled = true;\n };\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAwBA,IAAMA,IAAoB,cACpBC,IAAqB,cACrBC,oBAAkB,IAAI,IAAI,CAAC,aAAa,WAAW,CAAC,GAEpDC,IAAsB;CAC1B,UAAU;CACV,kBAAkB;AACpB;AAEA,SAASC,IAA0B;CACjC,IAAI;EACF,IAAMC,IAAW,OAAO,SAAS,UAC3BC,IAASD,EAAS,SAAS,sBAAsB,IACnD,yBACA,KAAA;EACJ,KAAK,IAAME,KAAc,CAAC,UAAU,gBAAgB,GAKlD,AAJA,SAAS,SAAS,GAAGA,EAAW,uBAC5BD,MACF,SAAS,SAAS,GAAGC,EAAW,oBAAoBD,EAAO,eAEzDD,KAAY,CAACH,EAAgB,IAAIG,CAAQ,MAC3C,SAAS,SAAS,GAAGE,EAAW,oBAAoBF,EAAS;CAGnE,QAAQ,CAER;AACF;AAEA,SAASG,IAA0B;CACjC,IAAI;EACF,QACE,IAAI,gBAAgB,OAAO,SAAS,MAAM,CAAC,CAAC,IAAIR,CAAiB,KAAK,GAAA,CAErE,MAAM,GAAG,CAAC,CACV,QAAQ,MAAWS,EAAO,SAAS,CAAC;CACzC,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAASC,EAAkB,GAA0B;CACnD,IAAI;EACF,IAAMC,IAAY,IAAI,IAAIC,GAAU,OAAO,SAAS,MAAM;EAQ1D,OANED,EAAU,WAAW,OAAO,SAAS,UACrC,CAACE,EAAmBF,CAAS,IAEtB,iBAETA,EAAU,aAAa,IAAIV,GAAoB,GAAG,GAC3CU,EAAU;CACnB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAASG,IAAwC;CAC/C,OAAOC,EAAkB,CAAC,EAAE,MAAM;AACpC;AAEA,SAASA,IAGA;CACP,IAAI;EACF,KAAK,IAAMR,KAAc,CAAC,UAAU,gBAAgB,GAAY;GAC9D,IAAMS,IAAQ,SAAS,OAAO,MAAMb,EAAoBI,EAAW;GACnE,IAAI,CAACS,GAAO;GAEZ,IAAMC,IAAS,KAAK,MAAM,mBAAmBD,EAAM,EAAE,CAAC;GAGtD,IAAI,OAAOC,KAAW,UAAU,OAAO;IAAE,IAAIA;IAAQ,QAAQ;GAAK;GAClE,IAAI,OAAOA,EAAO,MAAO,YAAYA,EAAO,GAAG,SAAS,GACtD,OAAO;IACL,IAAIA,EAAO;IACX,QAAQ,OAAOA,EAAO,UAAW,WAAWA,EAAO,SAAS;GAC9D;EAEJ;CACF,QAAQ;EACN,OAAO;CACT;CACA,OAAO;AACT;AAEA,SAASC,IAAqC;CAC5C,IAAI;EACF,OAAO,IAAI,IAAIC,EAAwB,CAAC,CAAC,GAAG,CAAC,CAAC;CAChD,QAAQ;EACN,OAAO;CACT;AACF;AAWA,SAAgBC,EAA4B,GAA2B;CACrE,IAAI;EACF,IAAMC,IAAYN,EAAkB;EACpC,IAAI,CAACM,GAAW,QAAQ,OAAO;EAE/B,IAAMC,IAAW,IAAI,IAAID,EAAU,MAAM;EAEzC,IADI,CAACR,EAAmBS,CAAQ,KAC5BA,EAAS,WAAW,OAAO,SAAS,QAAQ,OAAO;EAEvD,IAAMC,IAAeL,EAAoB;EAGzC,IADqBK,MADC,OAAO,SAAS,UAEjBD,EAAS,WAAWC,GACvC,OAAO;EAGT,IAAMC,IAAahB,EAAc;EACjC,IAAIgB,EAAW,SAASF,EAAS,MAAM,KAAKE,EAAW,UAAU,GAC/D,OAAO;EAGT,IAAMC,IAAe,IAAI,IAAI,mBAAmBH,EAAS,MAAM;EAQ/D,OAPAG,EAAa,aAAa,IAAI,YAAYb,CAAQ,GAClDa,EAAa,aAAa,IACxBzB,GACA,MAAM,qBAAK,IAAI,IAAI,CAAC,GAAGwB,GAAY,OAAO,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CACvE,GAEA,OAAO,SAAS,QAAQC,EAAa,IAAI,GAClC;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAASC,IAAiC;CACxC,OAAO,aAAa,QAAQ,aAAa;AAC3C;AAEA,SAASC,IAA2B;CAClC,IAAI;EACF,IAAMC,IAAcF,EAAgB;EAIpC,QAHiBE,IACZ,KAAK,MAAMA,CAAW,IACvB,CAAC,EAAA,CAEF,KAAK,MAAYC,EAAQ,EAAE,CAAC,CAC5B,QAAQ,MAAqB,OAAOC,KAAO,YAAYA,EAAG,SAAS,CAAC;CACzE,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAASC,IAAuC;CAC9C,IAAMC,IAAa;EACjBlB,EAAuB;EACvB,eAAe,QAAQ,mBAAmB;EAC1C,GAAGa,EAAe;CACpB,CAAC,CAAC,QAAQ,MAAqB,OAAOG,KAAO,YAAYA,EAAG,SAAS,CAAC;CAEtE,KAAK,IAAMG,KAAa,MAAM,KAAK,IAAI,IAAID,CAAU,CAAC,GAAG;EACvD,IAAME,IAAY,aAAa,QAAQ,QAAQD,EAAU,QAAQ,GAC3DE,IAAU,aAAa,QAAQ,QAAQF,EAAU,MAAM;EAC7D,IAAIC,KAAaC,GAEf,OADA,eAAe,QAAQ,qBAAqBF,CAAS,GAC9CA;CAEX;CAEA,OAAO;AACT;AAIA,SAASG,EAAuB,GAAyB;CACvD,IAAI;EAKF,AAJA,aAAa,WAAW,QAAQH,EAAU,QAAQ,GAClD,aAAa,WAAW,QAAQA,EAAU,MAAM,GAChD,aAAa,WAAW,QAAQA,EAAU,iBAAiB,GAC3D,aAAa,WAAW,QAAQA,EAAU,cAAc,GACxD,eAAe,WAAW,mBAAmB;CAC/C,QAAQ,CAER;AACF;AAEA,SAASI,EAAkB,GAIlB;CACP,IAAI;EACF,OAAO,KAAK,MAAMH,CAAS;CAK7B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgBI,IAAe;CAC7B,IAAM,CAACC,KAAgBC,EAAgB;CAyHvC,OAvHAC,QAAgB;EACd,IAAIC,IAAY;EAiHhB,QADA,YA9GwB;GACtB,IAAM9B,IAAW2B,EAAa,IAAI,UAAU;GAC5C,IAAI,CAAC3B,GAAU;IAIb,OAAO,SAAS,QAAQ,GAAG;IAC3B;GACF;GAKA,IAAMqB,IAAYF,EAAsB;GACxC,IAAI,CAACE,GAAW;IACd,IAAIb,EAA4BR,CAAQ,GAAG;IAE3C,AADAR,EAAkB,GAClB,OAAO,SAAS,QAAQM,EAAkBE,CAAQ,CAAC;IACnD;GACF;GAEA,IAAMsB,IAAY,aAAa,QAAQ,QAAQD,EAAU,QAAQ,GAC3DE,IAAU,aAAa,QAAQ,QAAQF,EAAU,MAAM,GACvDL,IAAcF,EAAgB;GAEpC,IAAI,CAACQ,KAAa,CAACC,GAAS;IAE1B,IAAIf,EAA4BR,CAAQ,GAAG;IAE3C,AADAR,EAAkB,GAClB,OAAO,SAAS,QAAQM,EAAkBE,CAAQ,CAAC;IACnD;GACF;GAGA,IAAID;GACJ,IAAI;IACF,IAAY,IAAI,IAAIC,CAAQ;GAC9B,QAAQ;IAEN,OAAO,SAAS,QAAQA,CAAQ;IAChC;GACF;GACA,IAAI,CAACC,EAAmBF,CAAS,GAAG;IAClC,OAAO,SAAS,QAAQ,GAAG;IAC3B;GACF;GAQA,IAAMgC,IAASN,EAAkBH,CAAS,GACtCU,IAAiC;GAErC,IAAID,GAAQ,cAAc;IACxB,IAAM,EAAE,qBAAkBE,EAAmB,GACvC,EAAE,QAAQC,GAAQ,cAAW,MAAMC,EACvC,GAAGC,EAAc,kBACjBL,EAAO,YACT;IACA,IAAID,GAAW;IACf,IAAII,GACF,IAAkB,KAAK,UAAUA,CAAM;SAClC,IAAIG,GAAQ;KAIjB,IADAb,EAAuBH,CAAS,GAC5Bb,EAA4BR,CAAQ,GAAG;KAE3C,AADAR,EAAkB,GAClB,OAAO,SAAS,QAAQM,EAAkBE,CAAQ,CAAC;KACnD;IACF;GACF;GAEA,IAAI,CAACgC,GAAiB;IAOpB,IAHID,GAAQ,eACV,OAAOA,EAAO,aAAc,YAC5BA,EAAO,YAAY,KAAK,IAAI,IAAI,KAEhC,IAAkBT;SACb;KAEL,IADAE,EAAuBH,CAAS,GAC5Bb,EAA4BR,CAAQ,GAAG;KAE3C,AADAR,EAAkB,GAClB,OAAO,SAAS,QAAQM,EAAkBE,CAAQ,CAAC;KACnD;IACF;GACF;GASA,IAAMsC,IAAU,KAAK,KAAK,UAAU;IALlC;IACA,QAAQN;IACR,MAAMT;IACN,UAAUP;GAEwB,CAAO,CAAC,GAEtCuB,IAAaxC,EAAU,WAAWA,EAAU,QAC5CyC,IAAc,GAAGzC,EAAU,OAAO,8BAA8B,mBAAmBwC,CAAU,EAAE,OAAOD;GAG5G,OAAO,SAAS,QAAQE,CAAW;EACrC,EAEK,CAAI,SACI;GACX,IAAY;EACd;CACF,GAAG,CAACb,CAAY,CAAC,GAGf,kBAAC,OAAD;EAAK,WAAU;EACb,UAAA,kBAAC,OAAD,EAAK,WAAU,0FAA2F,CAAA;CACvG,CAAA;AAET"}