@burdenoff/microfe-auth 2026.627.2 → 2026.704.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.
- package/dist/auth/pages/SSOClearPage.js +13 -15
- package/dist/auth/pages/SSOClearPage.js.map +1 -1
- package/dist/auth/pages/SSODepositPage.js +16 -18
- package/dist/auth/pages/SSODepositPage.js.map +1 -1
- package/dist/auth/pages/SSORelayCallbackPage.js +12 -14
- package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
- package/dist/auth/pages/SSORelayPage.js +4 -1
- package/dist/auth/pages/SSORelayPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,37 +3,35 @@ import { clearLocalAuthState as t } from "@burdenoff/fe-libs/shared/components";
|
|
|
3
3
|
import { useSearchParams as n } from "react-router-dom";
|
|
4
4
|
import { jsx as r } from "react/jsx-runtime";
|
|
5
5
|
import { clearShellSsoCookies as i } from "@burdenoff/fe-libs/shared/utils";
|
|
6
|
-
import {
|
|
6
|
+
import { BrandedSplash as a } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
import { getAuthBridge as o } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
|
|
7
8
|
//#region src/auth/pages/SSOClearPage.tsx
|
|
8
|
-
var
|
|
9
|
-
function
|
|
9
|
+
var s = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai,atheletebridge.com,adaptercloud.com,coolandlovely.com,subscriberbot.com,labsofscience.com,hookmovies.com".split(","));
|
|
10
|
+
function c(e) {
|
|
10
11
|
if (e.hostname === "localhost" || e.hostname === "127.0.0.1" || e.hostname.endsWith(".local.burdenoff.com")) return !0;
|
|
11
12
|
if (e.protocol !== "https:") return !1;
|
|
12
|
-
for (let t of
|
|
13
|
+
for (let t of s) if (e.hostname === t || e.hostname === `app.${t}` || e.hostname === `alphaapp.${t}` || e.hostname === `www.${t}`) return !0;
|
|
13
14
|
return !1;
|
|
14
15
|
}
|
|
15
|
-
function
|
|
16
|
+
function l(e) {
|
|
16
17
|
if (!e) return "/auth/login?reason=logout";
|
|
17
18
|
try {
|
|
18
19
|
let t = new URL(e, window.location.origin);
|
|
19
|
-
if (t.origin === window.location.origin ||
|
|
20
|
+
if (t.origin === window.location.origin || c(t)) return t.href;
|
|
20
21
|
} catch {}
|
|
21
22
|
return "/auth/login?reason=logout";
|
|
22
23
|
}
|
|
23
|
-
function
|
|
24
|
-
let [
|
|
24
|
+
function u() {
|
|
25
|
+
let [s] = n();
|
|
25
26
|
return e(() => {
|
|
26
|
-
let e =
|
|
27
|
+
let e = l(s.get("returnTo"));
|
|
27
28
|
try {
|
|
28
|
-
t(), i(),
|
|
29
|
+
t(), i(), o()?.notifyLogoutAll();
|
|
29
30
|
} catch {}
|
|
30
31
|
window.location.replace(e);
|
|
31
|
-
}, [
|
|
32
|
-
className: "flex items-center justify-center min-h-screen bg-bg-canvas",
|
|
33
|
-
children: /* @__PURE__ */ r("div", { className: "animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent" })
|
|
34
|
-
});
|
|
32
|
+
}, [s]), /* @__PURE__ */ r(a, { label: "Signing you out…" });
|
|
35
33
|
}
|
|
36
34
|
//#endregion
|
|
37
|
-
export {
|
|
35
|
+
export { u as SSOClearPage };
|
|
38
36
|
|
|
39
37
|
//# 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 { useSearchParams } from \"react-router-dom\";\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\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\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 || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\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
|
|
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-dom\";\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\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\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 || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\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 <BrandedSplash label=\"Signing you out…\" />;\n}\n"],"mappings":";;;;;;;;AA4BA,IAAM,IAAyB,IAAI,IAAI,8qBAwCtC,CAAC;AAEF,SAAS,EAAmB,GAAmB;AAE7C,KADI,EAAI,aAAa,eAAe,EAAI,aAAa,eACjD,EAAI,SAAS,SAAS,uBAAuB,CAAE,QAAO;AAC1D,KAAI,EAAI,aAAa,SAAU,QAAO;AACtC,MAAK,IAAM,KAAU,EACnB,KACE,EAAI,aAAa,KACjB,EAAI,aAAa,OAAO,OACxB,EAAI,aAAa,YAAY,OAC7B,EAAI,aAAa,OAAO,IAExB,QAAO;AAGX,QAAO;;AAGT,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAe;CAC7B,IAAM,CAAC,KAAgB,GAAiB;AAoBxC,QAlBA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC;AAEhE,MAAI;AAOF,GANA,GAAqB,EACrB,GAAsB,EAKtB,GAAe,EAAE,iBAAiB;UAC5B;AAIR,SAAO,SAAS,QAAQ,EAAS;IAChC,CAAC,EAAa,CAAC,EAEX,kBAAC,GAAD,EAAe,OAAM,oBAAqB,CAAA"}
|
|
@@ -3,52 +3,50 @@ import { useEffect as t } from "react";
|
|
|
3
3
|
import { useSearchParams as n } from "react-router-dom";
|
|
4
4
|
import { jsx as r } from "react/jsx-runtime";
|
|
5
5
|
import { setSSoCookie as i } from "@burdenoff/fe-libs/shared/utils";
|
|
6
|
-
import {
|
|
6
|
+
import { BrandedSplash as a } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
import { getAuthBridge as o } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
|
|
7
8
|
//#region src/auth/pages/SSODepositPage.tsx
|
|
8
|
-
var
|
|
9
|
-
function
|
|
9
|
+
var s = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai,atheletebridge.com,adaptercloud.com,coolandlovely.com,subscriberbot.com,labsofscience.com,hookmovies.com".split(","));
|
|
10
|
+
function c(e) {
|
|
10
11
|
if (e.hostname === "localhost" || e.hostname === "127.0.0.1" || e.hostname.endsWith(".local.burdenoff.com")) return !0;
|
|
11
12
|
if (e.protocol !== "https:") return !1;
|
|
12
|
-
for (let t of
|
|
13
|
+
for (let t of s) if (e.hostname === t || e.hostname === `app.${t}` || e.hostname === `alphaapp.${t}` || e.hostname === `admin.${t}` || e.hostname === `alphaadmin.${t}` || e.hostname === `www.${t}`) return !0;
|
|
13
14
|
return !1;
|
|
14
15
|
}
|
|
15
|
-
function
|
|
16
|
+
function l(e) {
|
|
16
17
|
if (!e) return "/";
|
|
17
18
|
try {
|
|
18
19
|
let t = new URL(e, window.location.origin);
|
|
19
|
-
if (t.origin === window.location.origin ||
|
|
20
|
+
if (t.origin === window.location.origin || c(t)) return t.href;
|
|
20
21
|
} catch {}
|
|
21
22
|
return "/";
|
|
22
23
|
}
|
|
23
|
-
function
|
|
24
|
-
let [
|
|
24
|
+
function u() {
|
|
25
|
+
let [s] = n();
|
|
25
26
|
return t(() => {
|
|
26
|
-
let t =
|
|
27
|
+
let t = l(s.get("returnTo")), n = window.location.hash;
|
|
27
28
|
if (!n.startsWith("#sso=")) {
|
|
28
29
|
window.location.replace(t);
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
try {
|
|
32
|
-
let r = n.slice(5), { profileId:
|
|
33
|
-
if (!
|
|
33
|
+
let r = n.slice(5), { profileId: a, tokens: s, user: c, profiles: l } = JSON.parse(atob(r));
|
|
34
|
+
if (!a || !s) {
|
|
34
35
|
window.location.replace(t);
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
|
-
localStorage.setItem(`bf-p-${
|
|
38
|
+
localStorage.setItem(`bf-p-${a}-tokens`, s), c && localStorage.setItem(`bf-p-${a}-user`, c), l && localStorage.setItem("bf-profiles", l), sessionStorage.setItem("bf-active-profile", a), i(a), e.getState().loadFromProfile(a), window.dispatchEvent(new Event("bf-profile-storage-change")), o()?.notifyLogin({
|
|
38
39
|
user: c ? JSON.parse(c) : null,
|
|
39
40
|
tokens: s ? JSON.parse(s) : null,
|
|
40
|
-
activeProfileId:
|
|
41
|
+
activeProfileId: a,
|
|
41
42
|
profiles: l ? JSON.parse(l) : []
|
|
42
43
|
}), window.location.replace(t);
|
|
43
44
|
} catch {
|
|
44
45
|
window.location.replace(t);
|
|
45
46
|
}
|
|
46
|
-
}, [
|
|
47
|
-
className: "flex items-center justify-center min-h-screen bg-bg-canvas",
|
|
48
|
-
children: /* @__PURE__ */ r("div", { className: "animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent" })
|
|
49
|
-
});
|
|
47
|
+
}, [s]), /* @__PURE__ */ r(a, { label: "Signing you in…" });
|
|
50
48
|
}
|
|
51
49
|
//#endregion
|
|
52
|
-
export {
|
|
50
|
+
export { u as SSODepositPage };
|
|
53
51
|
|
|
54
52
|
//# 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 { useSearchParams } from \"react-router-dom\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // valid post-deposit return targets. Without these, a login that lands on\n // the bridge host falls back to \"/\" instead of returning to the admin app.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\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 || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\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
|
|
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-dom\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // valid post-deposit return targets. Without these, a login that lands on\n // the bridge host falls back to \"/\" instead of returning to the admin app.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\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 || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\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 <BrandedSplash label=\"Signing you in…\" />;\n}\n"],"mappings":";;;;;;;;AA8BA,IAAM,IAAyB,IAAI,IAAI,8qBAwCtC,CAAC;AAEF,SAAS,EAAmB,GAAmB;AAE7C,KADI,EAAI,aAAa,eAAe,EAAI,aAAa,eACjD,EAAI,SAAS,SAAS,uBAAuB,CAAE,QAAO;AAC1D,KAAI,EAAI,aAAa,SAAU,QAAO;AACtC,MAAK,IAAM,KAAU,EACnB,KACE,EAAI,aAAa,KACjB,EAAI,aAAa,OAAO,OACxB,EAAI,aAAa,YAAY,OAI7B,EAAI,aAAa,SAAS,OAC1B,EAAI,aAAa,cAAc,OAC/B,EAAI,aAAa,OAAO,IAExB,QAAO;AAGX,QAAO;;AAGT,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAiB;CAC/B,IAAM,CAAC,KAAgB,GAAiB;AAyDxC,QAvDA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC,EAC1D,IAAO,OAAO,SAAS;AAE7B,MAAI,CAAC,EAAK,WAAW,QAAQ,EAAE;AAC7B,UAAO,SAAS,QAAQ,EAAS;AACjC;;AAGF,MAAI;GACF,IAAM,IAAU,EAAK,MAAM,EAAE,EAOvB,EAAE,cAAW,WAAQ,SAAM,gBANjB,KAAK,MAAM,KAAK,EAAQ,CAAC;AAQzC,OAAI,CAAC,KAAa,CAAC,GAAQ;AACzB,WAAO,SAAS,QAAQ,EAAS;AACjC;;AA4BF,GAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,EAAO,EACpD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,EAAK,EAElD,KACF,aAAa,QAAQ,eAAe,EAAS,EAE/C,eAAe,QAAQ,qBAAqB,EAAU,EACtD,EAAa,EAAU,EACvB,EAAa,UAAU,CAAC,gBAAgB,EAAU,EAClD,OAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC,EAI5D,GAAe,EAAE,YAAY;IAC3B,MAAM,IAAQ,KAAK,MAAM,EAAK,GAA+B;IAC7D,QAAQ,IAAU,KAAK,MAAM,EAAO,GAA+B;IACnE,iBAAiB;IACjB,UAAU,IACL,KAAK,MAAM,EAAS,GACrB,EAAE;IACP,CAAC,EAEF,OAAO,SAAS,QAAQ,EAAS;UAC3B;AACN,UAAO,SAAS,QAAQ,EAAS;;IAElC,CAAC,EAAa,CAAC,EAEX,kBAAC,GAAD,EAAe,OAAM,mBAAoB,CAAA"}
|
|
@@ -3,9 +3,10 @@ import { useEffect as t } from "react";
|
|
|
3
3
|
import { useSearchParams as n } from "react-router-dom";
|
|
4
4
|
import { jsx as r } from "react/jsx-runtime";
|
|
5
5
|
import { setSSoCookie as i } from "@burdenoff/fe-libs/shared/utils";
|
|
6
|
-
import {
|
|
6
|
+
import { BrandedSplash as a } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
import { getAuthBridge as o } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
|
|
7
8
|
//#region src/auth/pages/SSORelayCallbackPage.tsx
|
|
8
|
-
function
|
|
9
|
+
function s(e) {
|
|
9
10
|
if (!e) return "/dashboard";
|
|
10
11
|
try {
|
|
11
12
|
let t = new URL(e, window.location.origin);
|
|
@@ -14,35 +15,32 @@ function o(e) {
|
|
|
14
15
|
return "/dashboard";
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
function
|
|
18
|
-
let [
|
|
18
|
+
function c() {
|
|
19
|
+
let [c] = n();
|
|
19
20
|
return t(() => {
|
|
20
|
-
let t =
|
|
21
|
+
let t = s(c.get("returnTo")), n = window.location.hash;
|
|
21
22
|
if (!n.startsWith("#sso=")) {
|
|
22
23
|
window.location.replace("/auth/login");
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
try {
|
|
26
|
-
let r = n.slice(5), { profileId:
|
|
27
|
-
if (!
|
|
27
|
+
let r = n.slice(5), { profileId: a, tokens: s, user: c, profiles: l } = JSON.parse(atob(r));
|
|
28
|
+
if (!a || !s) {
|
|
28
29
|
window.location.replace("/auth/login");
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
|
-
localStorage.setItem(`bf-p-${
|
|
32
|
+
localStorage.setItem(`bf-p-${a}-tokens`, s), c && localStorage.setItem(`bf-p-${a}-user`, c), l && localStorage.setItem("bf-profiles", l), sessionStorage.setItem("bf-active-profile", a), i(a), e.getState().loadFromProfile(a), window.dispatchEvent(new Event("bf-profile-storage-change")), o()?.notifyLogin({
|
|
32
33
|
user: c ? JSON.parse(c) : null,
|
|
33
34
|
tokens: s ? JSON.parse(s) : null,
|
|
34
|
-
activeProfileId:
|
|
35
|
+
activeProfileId: a,
|
|
35
36
|
profiles: l ? JSON.parse(l) : []
|
|
36
37
|
}), window.location.replace(t);
|
|
37
38
|
} catch {
|
|
38
39
|
window.location.replace("/auth/login");
|
|
39
40
|
}
|
|
40
|
-
}, [
|
|
41
|
-
className: "flex items-center justify-center min-h-screen bg-bg-canvas",
|
|
42
|
-
children: /* @__PURE__ */ r("div", { className: "animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent" })
|
|
43
|
-
});
|
|
41
|
+
}, [c]), /* @__PURE__ */ r(a, { label: "Signing you in…" });
|
|
44
42
|
}
|
|
45
43
|
//#endregion
|
|
46
|
-
export {
|
|
44
|
+
export { c as SSORelayCallbackPage };
|
|
47
45
|
|
|
48
46
|
//# 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 { useSearchParams } from \"react-router-dom\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\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 [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
|
|
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-dom\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\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 [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 <BrandedSplash label=\"Signing you in…\" />;\n}\n"],"mappings":";;;;;;;;AAcA,SAAS,EAAgB,GAAiC;AACxD,KAAI,CAAC,EAAU,QAAO;AAEtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AAErD,SADI,EAAI,WAAW,OAAO,SAAS,SAC5B,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,SADQ;SAE5C;AACN,SAAO;;;AAIX,SAAgB,IAAuB;CACrC,IAAM,CAAC,KAAgB,GAAiB;AAoDxC,QAlDA,QAAgB;EACd,IAAM,IAAW,EAAgB,EAAa,IAAI,WAAW,CAAC,EACxD,IAAO,OAAO,SAAS;AAE7B,MAAI,CAAC,EAAK,WAAW,QAAQ,EAAE;AAE7B,UAAO,SAAS,QAAQ,cAAc;AACtC;;AAGF,MAAI;GACF,IAAM,IAAU,EAAK,MAAM,EAAE,EAEvB,EAAE,cAAW,WAAQ,SAAM,gBADjB,KAAK,MAAM,KAAK,EAAQ,CAAC;AAGzC,OAAI,CAAC,KAAa,CAAC,GAAQ;AACzB,WAAO,SAAS,QAAQ,cAAc;AACtC;;AA2BF,GAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,EAAO,EACpD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,EAAK,EAElD,KACF,aAAa,QAAQ,eAAe,EAAS,EAE/C,eAAe,QAAQ,qBAAqB,EAAU,EACtD,EAAa,EAAU,EACvB,EAAa,UAAU,CAAC,gBAAgB,EAAU,EAGlD,OAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC,EAG5D,GAAe,EAAE,YAAY;IAC3B,MAAM,IAAO,KAAK,MAAM,EAAK,GAAG;IAChC,QAAQ,IAAS,KAAK,MAAM,EAAO,GAAG;IACtC,iBAAiB;IACjB,UAAU,IAAW,KAAK,MAAM,EAAS,GAAG,EAAE;IAC/C,CAAC,EAGF,OAAO,SAAS,QAAQ,EAAS;UAC3B;AACN,UAAO,SAAS,QAAQ,cAAc;;IAEvC,CAAC,EAAa,CAAC,EAEX,kBAAC,GAAD,EAAe,OAAM,mBAAoB,CAAA"}
|
|
@@ -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-dom\";\n\nconst ALLOWED_RELAY_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\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 isAllowedRelayTarget(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n return true;\n }\n if (url.hostname.endsWith(\".local.burdenoff.com\")) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n for (const domain of ALLOWED_RELAY_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // first-party SSO relay targets. Without these the relay-back to the\n // admin shell fails isAllowedRelayTarget() and SSORelayPage falls through\n // to window.location.replace(\"/\"), stranding the user on app.burdenoff.com.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\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 !isAllowedRelayTarget(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 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 (!isAllowedRelayTarget(relayUrl)) return false;\n if (relayUrl.origin === window.location.origin) return false;\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\nexport function SSORelayPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = searchParams.get(\"returnTo\");\n if (!returnTo) {\n // No return URL — just show a blank page\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 // Encode the auth state as a base64 URL fragment\n const payload = {\n profileId,\n tokens: tokensRaw,\n user: userRaw,\n profiles: profilesRaw,\n };\n const encoded = btoa(JSON.stringify(payload));\n\n // Build the callback URL on the target domain\n // Parse returnTo to get the target origin\n let callbackUrl: string;\n try {\n const targetUrl = new URL(returnTo);\n if (!isAllowedRelayTarget(targetUrl)) {\n window.location.replace(\"/\");\n return;\n }\n const returnPath = targetUrl.pathname + targetUrl.search;\n callbackUrl = `${targetUrl.origin}/auth/sso-callback?returnTo=${encodeURIComponent(returnPath)}#sso=${encoded}`;\n } catch {\n // returnTo is a relative path — can't do cross-domain relay\n window.location.replace(returnTo);\n return;\n }\n\n // Redirect to the callback\n window.location.replace(callbackUrl);\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":[],"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-dom\";\n\nconst ALLOWED_RELAY_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\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 isAllowedRelayTarget(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n return true;\n }\n if (url.hostname.endsWith(\".local.burdenoff.com\")) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n for (const domain of ALLOWED_RELAY_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // first-party SSO relay targets. Without these the relay-back to the\n // admin shell fails isAllowedRelayTarget() and SSORelayPage falls through\n // to window.location.replace(\"/\"), stranding the user on app.burdenoff.com.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\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 !isAllowedRelayTarget(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 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 (!isAllowedRelayTarget(relayUrl)) return false;\n if (relayUrl.origin === window.location.origin) return false;\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\nexport function SSORelayPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\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 // Encode the auth state as a base64 URL fragment\n const payload = {\n profileId,\n tokens: tokensRaw,\n user: userRaw,\n profiles: profilesRaw,\n };\n const encoded = btoa(JSON.stringify(payload));\n\n // Build the callback URL on the target domain\n // Parse returnTo to get the target origin\n let callbackUrl: string;\n try {\n const targetUrl = new URL(returnTo);\n if (!isAllowedRelayTarget(targetUrl)) {\n window.location.replace(\"/\");\n return;\n }\n const returnPath = targetUrl.pathname + targetUrl.search;\n callbackUrl = `${targetUrl.origin}/auth/sso-callback?returnTo=${encodeURIComponent(returnPath)}#sso=${encoded}`;\n } catch {\n // returnTo is a relative path — can't do cross-domain relay\n window.location.replace(returnTo);\n return;\n }\n\n // Redirect to the callback\n window.location.replace(callbackUrl);\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-auth",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.704.2",
|
|
4
4
|
"description": "Auth microfrontend for Burdenoff products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"imports": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@apollo/client": "^4.0.9",
|
|
47
|
-
"@burdenoff/fe-libs": "2026.
|
|
47
|
+
"@burdenoff/fe-libs": "2026.704.2",
|
|
48
48
|
"@types/qrcode.react": "^3.0.0",
|
|
49
49
|
"clsx": "^2.1.1",
|
|
50
50
|
"graphql": "^16.12.0",
|