@burdenoff/microfe-auth 2026.601.1 → 2026.601.3
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/hooks/useOAuthOperations.js +32 -27
- package/dist/auth/hooks/useOAuthOperations.js.map +1 -1
- package/dist/auth/pages/SSODepositPage.js +1 -1
- package/dist/auth/pages/SSODepositPage.js.map +1 -1
- package/dist/auth/pages/SSORelayPage.js +1 -1
- package/dist/auth/pages/SSORelayPage.js.map +1 -1
- package/dist/auth/pages/SessionBridgePage.js +1 -1
- package/dist/auth/pages/SessionBridgePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -32,7 +32,20 @@ function a() {
|
|
|
32
32
|
let e = new Uint8Array(32);
|
|
33
33
|
return crypto.getRandomValues(e), Array.from(e, (e) => e.toString(16).padStart(2, "0")).join("");
|
|
34
34
|
}
|
|
35
|
-
function o() {
|
|
35
|
+
function o(e) {
|
|
36
|
+
try {
|
|
37
|
+
return {
|
|
38
|
+
BASE_URL: "/",
|
|
39
|
+
DEV: !1,
|
|
40
|
+
MODE: "production",
|
|
41
|
+
PROD: !0,
|
|
42
|
+
SSR: !1
|
|
43
|
+
}?.[e];
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function s() {
|
|
36
49
|
if (typeof window > "u") return !1;
|
|
37
50
|
let e = window;
|
|
38
51
|
if (typeof e.Capacitor?.isNativePlatform == "function") try {
|
|
@@ -40,8 +53,8 @@ function o() {
|
|
|
40
53
|
} catch {}
|
|
41
54
|
return !!(typeof navigator < "u" && /Electron/i.test(navigator.userAgent || ""));
|
|
42
55
|
}
|
|
43
|
-
var
|
|
44
|
-
async function
|
|
56
|
+
var c = "vibecontrols://app/auth/oauth/callback";
|
|
57
|
+
async function l(e) {
|
|
45
58
|
if (typeof window > "u") return;
|
|
46
59
|
let t = window.__burdenoffOpenExternal;
|
|
47
60
|
if (typeof t == "function") try {
|
|
@@ -49,31 +62,27 @@ async function c(e) {
|
|
|
49
62
|
} catch {}
|
|
50
63
|
window.location.href = e;
|
|
51
64
|
}
|
|
52
|
-
function
|
|
53
|
-
let
|
|
54
|
-
let t = i[e], n =
|
|
55
|
-
VITE_GOOGLE_CLIENT_ID: void 0,
|
|
56
|
-
VITE_GITHUB_CLIENT_ID: void 0,
|
|
57
|
-
VITE_MICROSOFT_CLIENT_ID: void 0
|
|
58
|
-
}[t.clientIdEnvKey];
|
|
65
|
+
function u() {
|
|
66
|
+
let u = e(), [d, { loading: f }] = r(n), p = (e) => u.oauthClientIds?.[e] || o(i[e].clientIdEnvKey), m = (e) => {
|
|
67
|
+
let t = i[e], n = p(e);
|
|
59
68
|
if (!n) throw console.error(`OAuth client ID not configured for ${e}`), Error(`${e} OAuth is not configured. Please set ${t.clientIdEnvKey} environment variable.`);
|
|
60
69
|
let r = a();
|
|
61
70
|
localStorage.setItem("oauth_state", r), localStorage.setItem("oauth_provider", e);
|
|
62
|
-
let
|
|
63
|
-
localStorage.setItem("oauth_redirect_uri",
|
|
71
|
+
let o = s() ? c : `${window.location.origin}/auth/oauth/callback`;
|
|
72
|
+
localStorage.setItem("oauth_redirect_uri", o);
|
|
64
73
|
let u = new URLSearchParams({
|
|
65
74
|
client_id: n,
|
|
66
|
-
redirect_uri:
|
|
75
|
+
redirect_uri: o,
|
|
67
76
|
response_type: "code",
|
|
68
77
|
scope: t.scopes.join(" "),
|
|
69
78
|
state: r,
|
|
70
79
|
...e === "google" && { prompt: "select_account" },
|
|
71
80
|
...e === "google" && { access_type: "offline" }
|
|
72
81
|
});
|
|
73
|
-
|
|
82
|
+
l(`${t.authorizationUrl}?${u.toString()}`);
|
|
74
83
|
};
|
|
75
84
|
return {
|
|
76
|
-
initiateOAuth:
|
|
85
|
+
initiateOAuth: m,
|
|
77
86
|
completeOAuth: async (e, n) => {
|
|
78
87
|
let r = t.getState(), i = localStorage.getItem("oauth_state");
|
|
79
88
|
if (!i || i !== n) throw console.error("[OAuth Complete] State mismatch!", {
|
|
@@ -85,7 +94,7 @@ function l() {
|
|
|
85
94
|
localStorage.removeItem("oauth_state"), localStorage.removeItem("oauth_provider"), localStorage.removeItem("oauth_redirect_uri");
|
|
86
95
|
try {
|
|
87
96
|
r.setLoading(!0), r.setError(null);
|
|
88
|
-
let { data: t, error: n } = await
|
|
97
|
+
let { data: t, error: n } = await d({ variables: { input: {
|
|
89
98
|
provider: a.toUpperCase(),
|
|
90
99
|
code: e,
|
|
91
100
|
redirectUri: o
|
|
@@ -95,7 +104,7 @@ function l() {
|
|
|
95
104
|
throw Error(e);
|
|
96
105
|
}
|
|
97
106
|
let i = t.authenticateOAuth;
|
|
98
|
-
return i.requiresMFA ? (r.setMfaRequired(!0, i.mfaChallengeId || void 0), i) : (i.success && i.user && i.token ? (r.setUser(i.user), r.setAuthTokens(i.token, i.refreshToken || ""), r.setAuthenticated(!0), r.setMfaRequired(!1),
|
|
107
|
+
return i.requiresMFA ? (r.setMfaRequired(!0, i.mfaChallengeId || void 0), i) : (i.success && i.user && i.token ? (r.setUser(i.user), r.setAuthTokens(i.token, i.refreshToken || ""), r.setAuthenticated(!0), r.setMfaRequired(!1), u.onAuthSuccess?.(i.user, i.token, i.refreshToken || void 0)) : i.error && r.setError(i.error), i);
|
|
99
108
|
} catch (e) {
|
|
100
109
|
let t = e instanceof Error ? e.message : "OAuth authentication failed";
|
|
101
110
|
throw r.setError(t), e;
|
|
@@ -103,18 +112,14 @@ function l() {
|
|
|
103
112
|
r.setLoading(!1);
|
|
104
113
|
}
|
|
105
114
|
},
|
|
106
|
-
initiateGoogleOAuth: () =>
|
|
107
|
-
initiateGitHubOAuth: () =>
|
|
108
|
-
initiateMicrosoftOAuth: () =>
|
|
109
|
-
isProviderConfigured: (e) => !!
|
|
110
|
-
|
|
111
|
-
VITE_GITHUB_CLIENT_ID: void 0,
|
|
112
|
-
VITE_MICROSOFT_CLIENT_ID: void 0
|
|
113
|
-
}[i[e].clientIdEnvKey],
|
|
114
|
-
isLoading: d
|
|
115
|
+
initiateGoogleOAuth: () => m("google"),
|
|
116
|
+
initiateGitHubOAuth: () => m("github"),
|
|
117
|
+
initiateMicrosoftOAuth: () => m("microsoft"),
|
|
118
|
+
isProviderConfigured: (e) => !!p(e),
|
|
119
|
+
isLoading: f
|
|
115
120
|
};
|
|
116
121
|
}
|
|
117
122
|
//#endregion
|
|
118
|
-
export {
|
|
123
|
+
export { u as useOAuthOperations };
|
|
119
124
|
|
|
120
125
|
//# sourceMappingURL=useOAuthOperations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOAuthOperations.js","names":[],"sources":["../../../src/auth/hooks/useOAuthOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport type { AuthResult, OAuthInput } from \"../types\";\n\nimport { AuthenticateOAuthDocument } from \"../../generated/global-operations\";\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface AuthenticateOAuthResponse {\n authenticateOAuth: AuthResult;\n}\n\ninterface AuthenticateOAuthVariables {\n input: OAuthInput;\n}\n\n/**\n * Supported OAuth providers\n */\nexport type OAuthProvider = \"google\" | \"github\" | \"microsoft\";\n\n/**\n * OAuth configuration for each provider\n */\ninterface OAuthConfig {\n authorizationUrl: string;\n scopes: string[];\n clientIdEnvKey: string;\n}\n\nconst OAUTH_CONFIGS: Record<OAuthProvider, OAuthConfig> = {\n google: {\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_GOOGLE_CLIENT_ID\",\n },\n github: {\n authorizationUrl: \"https://github.com/login/oauth/authorize\",\n scopes: [\"user:email\", \"read:user\"],\n clientIdEnvKey: \"VITE_GITHUB_CLIENT_ID\",\n },\n microsoft: {\n authorizationUrl:\n \"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_MICROSOFT_CLIENT_ID\",\n },\n};\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nfunction generateState(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return Array.from(array, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n \"\",\n );\n}\n\n/**\n * Detect whether the bundle is running inside a native shell (Capacitor on\n * iOS/Android, or Electron). Mirrors the helper in LoginPage so OAuth\n * initiation can swap to a custom-scheme redirect_uri + system-browser\n * hand-off — the only flow OAuth providers will accept from a single-device\n * install.\n */\nfunction isNativeShell(): boolean {\n if (typeof window === \"undefined\") return false;\n const w = window as typeof window & {\n Capacitor?: { isNativePlatform?: () => boolean };\n };\n if (typeof w.Capacitor?.isNativePlatform === \"function\") {\n try {\n if (w.Capacitor.isNativePlatform()) return true;\n } catch {\n /* fall through */\n }\n }\n if (\n typeof navigator !== \"undefined\" &&\n /Electron/i.test(navigator.userAgent || \"\")\n ) {\n return true;\n }\n return false;\n}\n\n/**\n * Custom-scheme redirect URI used in native shells. The scheme is registered\n * by the host shell (Electron `setAsDefaultProtocolClient`, iOS Info.plist\n * CFBundleURLTypes, Android intent-filter). The OS routes the provider's\n * callback back to the running app, which forwards it to React Router via\n * the host's deep-link handler.\n *\n * IMPORTANT: This redirect URI MUST be registered for the OAuth client in\n * each provider's console:\n * - Google Cloud Console → Credentials → OAuth client → add\n * `vibecontrols://app/auth/oauth/callback` (Installed App).\n * - Microsoft Entra App registration → Authentication → register the\n * same custom-scheme redirect URI as a mobile/desktop redirect.\n * The backend `authenticateOAuth` mutation must also accept this redirectUri\n * verbatim during the code exchange.\n */\nconst NATIVE_OAUTH_REDIRECT_URI = \"vibecontrols://app/auth/oauth/callback\";\n\nasync function openOAuthAuthorizationUrl(authUrl: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n const opener = (\n window as typeof window & {\n __burdenoffOpenExternal?: (url: string) => Promise<boolean>;\n }\n ).__burdenoffOpenExternal;\n if (typeof opener === \"function\") {\n try {\n const ok = await opener(authUrl);\n if (ok) return;\n } catch {\n /* fall through to legacy hand-off */\n }\n }\n // Browser fallback: same-window navigation (current production behavior).\n window.location.href = authUrl;\n}\n\n/**\n * Hook for OAuth authentication operations\n */\nexport function useOAuthOperations() {\n const context = useAuth();\n const [authenticateOAuthMutation, { loading }] = useMutation<\n AuthenticateOAuthResponse,\n AuthenticateOAuthVariables\n >(AuthenticateOAuthDocument);\n\n /**\n * Initiate OAuth flow by redirecting to the provider\n * @param provider - The OAuth provider to use\n */\n const initiateOAuth = (provider: OAuthProvider) => {\n const config = OAUTH_CONFIGS[provider];\n\n // Get client ID from environment\n // In Vite, environment variables are accessed via import.meta.env\n const envVars: Record<string, string | undefined> = {\n VITE_GOOGLE_CLIENT_ID: import.meta.env.VITE_GOOGLE_CLIENT_ID,\n VITE_GITHUB_CLIENT_ID: import.meta.env.VITE_GITHUB_CLIENT_ID,\n VITE_MICROSOFT_CLIENT_ID: import.meta.env.VITE_MICROSOFT_CLIENT_ID,\n };\n const clientId = envVars[config.clientIdEnvKey];\n\n if (!clientId) {\n console.error(`OAuth client ID not configured for ${provider}`);\n throw new Error(\n `${provider} OAuth is not configured. Please set ${config.clientIdEnvKey} environment variable.`,\n );\n }\n\n // Generate state for CSRF protection and store it\n // Use localStorage instead of sessionStorage for better persistence across redirects\n const state = generateState();\n localStorage.setItem(\"oauth_state\", state);\n localStorage.setItem(\"oauth_provider\", provider);\n\n // Native shells (Electron, Capacitor) can't use the current origin as the\n // redirect_uri — `vibecontrols://app` / `https://localhost` aren't valid\n // registered URIs at OAuth providers. Use a custom-scheme URL that the\n // host shell has registered as a deep-link target; the OS will route the\n // provider's callback back into the running app.\n const redirectUri = isNativeShell()\n ? NATIVE_OAUTH_REDIRECT_URI\n : `${window.location.origin}/auth/oauth/callback`;\n localStorage.setItem(\"oauth_redirect_uri\", redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: config.scopes.join(\" \"),\n state: state,\n // Google-specific: force account selection\n ...(provider === \"google\" && { prompt: \"select_account\" }),\n // Google-specific: enable offline access for refresh token\n ...(provider === \"google\" && { access_type: \"offline\" }),\n });\n\n const authUrl = `${config.authorizationUrl}?${params.toString()}`;\n\n // Hand off to the system browser in native shells (keeps the embedded\n // webview in place so the deep-link callback can return cleanly). On the\n // web this falls back to same-window navigation.\n void openOAuthAuthorizationUrl(authUrl);\n };\n\n /**\n * Complete OAuth flow by exchanging authorization code for tokens\n * Called from the callback page after provider redirects back\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter for CSRF validation\n */\n const completeOAuth = async (\n code: string,\n state: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n\n // Validate state parameter\n const storedState = localStorage.getItem(\"oauth_state\");\n if (!storedState || storedState !== state) {\n console.error(\"[OAuth Complete] State mismatch!\", {\n storedState,\n urlState: state,\n });\n throw new Error(\"Invalid OAuth state. Please try again.\");\n }\n\n // Get provider and redirect URI from localStorage\n const provider = localStorage.getItem(\"oauth_provider\") as OAuthProvider;\n const redirectUri = localStorage.getItem(\"oauth_redirect_uri\");\n\n if (!provider || !redirectUri) {\n console.error(\"[OAuth Complete] Missing provider or redirectUri\");\n throw new Error(\"OAuth session data missing. Please try again.\");\n }\n\n // Clear localStorage\n localStorage.removeItem(\"oauth_state\");\n localStorage.removeItem(\"oauth_provider\");\n localStorage.removeItem(\"oauth_redirect_uri\");\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Call backend mutation to exchange code for tokens\n const { data, error } = await authenticateOAuthMutation({\n variables: {\n input: {\n provider: provider.toUpperCase(),\n code,\n redirectUri,\n },\n },\n });\n\n if (!data?.authenticateOAuth) {\n const errMsg =\n error?.message ?? \"OAuth authentication failed. Please try again.\";\n throw new Error(errMsg);\n }\n\n const result = data.authenticateOAuth;\n\n if (result.requiresMFA) {\n store.setMfaRequired(true, result.mfaChallengeId || undefined);\n return result;\n }\n\n if (result.success && result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n store.setMfaRequired(false);\n\n context.onAuthSuccess?.(\n result.user,\n result.token,\n result.refreshToken || undefined,\n );\n } else if (result.error) {\n store.setError(result.error);\n }\n\n return result;\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"OAuth authentication failed\";\n store.setError(message);\n throw err;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Convenience method to initiate Google OAuth\n */\n const initiateGoogleOAuth = () => initiateOAuth(\"google\");\n\n /**\n * Convenience method to initiate GitHub OAuth\n */\n const initiateGitHubOAuth = () => initiateOAuth(\"github\");\n\n /**\n * Convenience method to initiate Microsoft OAuth\n */\n const initiateMicrosoftOAuth = () => initiateOAuth(\"microsoft\");\n\n /**\n * Check if a provider is configured (has client ID)\n */\n const isProviderConfigured = (provider: OAuthProvider): boolean => {\n const envVars: Record<string, string | undefined> = {\n VITE_GOOGLE_CLIENT_ID: import.meta.env.VITE_GOOGLE_CLIENT_ID,\n VITE_GITHUB_CLIENT_ID: import.meta.env.VITE_GITHUB_CLIENT_ID,\n VITE_MICROSOFT_CLIENT_ID: import.meta.env.VITE_MICROSOFT_CLIENT_ID,\n };\n const config = OAUTH_CONFIGS[provider];\n const clientId = envVars[config.clientIdEnvKey];\n return !!clientId;\n };\n\n return {\n initiateOAuth,\n completeOAuth,\n initiateGoogleOAuth,\n initiateGitHubOAuth,\n initiateMicrosoftOAuth,\n isProviderConfigured,\n isLoading: loading,\n };\n}\n"],"mappings":";;;;;AAiCA,IAAM,IAAoD;CACxD,QAAQ;EACN,kBAAkB;EAClB,QAAQ;GAAC;GAAU;GAAS;GAAU;EACtC,gBAAgB;EACjB;CACD,QAAQ;EACN,kBAAkB;EAClB,QAAQ,CAAC,cAAc,YAAY;EACnC,gBAAgB;EACjB;CACD,WAAW;EACT,kBACE;EACF,QAAQ;GAAC;GAAU;GAAS;GAAU;EACtC,gBAAgB;EACjB;CACF;AAKD,SAAS,IAAwB;CAC/B,IAAM,IAAQ,IAAI,WAAW,GAAG;AAEhC,QADA,OAAO,gBAAgB,EAAM,EACtB,MAAM,KAAK,IAAQ,MAAS,EAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KACrE,GACD;;AAUH,SAAS,IAAyB;AAChC,KAAI,OAAO,SAAW,IAAa,QAAO;CAC1C,IAAM,IAAI;AAGV,KAAI,OAAO,EAAE,WAAW,oBAAqB,WAC3C,KAAI;AACF,MAAI,EAAE,UAAU,kBAAkB,CAAE,QAAO;SACrC;AAUV,QANA,GACE,OAAO,YAAc,OACrB,YAAY,KAAK,UAAU,aAAa,GAAG;;AAuB/C,IAAM,IAA4B;AAElC,eAAe,EAA0B,GAAgC;AACvE,KAAI,OAAO,SAAW,IAAa;CACnC,IAAM,IACJ,OAGA;AACF,KAAI,OAAO,KAAW,WACpB,KAAI;AAEF,MADW,MAAM,EAAO,EAAQ,CACxB;SACF;AAKV,QAAO,SAAS,OAAO;;AAMzB,SAAgB,IAAqB;CACnC,IAAM,IAAU,GAAS,EACnB,CAAC,GAA2B,EAAE,gBAAa,EAG/C,EAA0B,EAMtB,KAAiB,MAA4B;EACjD,IAAM,IAAS,EAAc,IASvB,IAL8C;GAClD,uBAAA,KAAA;GACA,uBAAA,KAAA;GACA,0BAAA,KAAA;GACD,CACwB,EAAO;AAEhC,MAAI,CAAC,EAEH,OADA,QAAQ,MAAM,sCAAsC,IAAW,EACrD,MACR,GAAG,EAAS,uCAAuC,EAAO,eAAe,wBAC1E;EAKH,IAAM,IAAQ,GAAe;AAE7B,EADA,aAAa,QAAQ,eAAe,EAAM,EAC1C,aAAa,QAAQ,kBAAkB,EAAS;EAOhD,IAAM,IAAc,GAAe,GAC/B,IACA,GAAG,OAAO,SAAS,OAAO;AAC9B,eAAa,QAAQ,sBAAsB,EAAY;EAGvD,IAAM,IAAS,IAAI,gBAAgB;GACjC,WAAW;GACX,cAAc;GACd,eAAe;GACf,OAAO,EAAO,OAAO,KAAK,IAAI;GACvB;GAEP,GAAI,MAAa,YAAY,EAAE,QAAQ,kBAAkB;GAEzD,GAAI,MAAa,YAAY,EAAE,aAAa,WAAW;GACxD,CAAC;AAOG,IALW,GAAG,EAAO,iBAAiB,GAAG,EAAO,UAAU,GAKxB;;AA0HzC,QAAO;EACL;EACA,eAnHoB,OACpB,GACA,MACwB;GACxB,IAAM,IAAQ,EAAa,UAAU,EAG/B,IAAc,aAAa,QAAQ,cAAc;AACvD,OAAI,CAAC,KAAe,MAAgB,EAKlC,OAJA,QAAQ,MAAM,oCAAoC;IAChD;IACA,UAAU;IACX,CAAC,EACQ,MAAM,yCAAyC;GAI3D,IAAM,IAAW,aAAa,QAAQ,iBAAiB,EACjD,IAAc,aAAa,QAAQ,qBAAqB;AAE9D,OAAI,CAAC,KAAY,CAAC,EAEhB,OADA,QAAQ,MAAM,mDAAmD,EACvD,MAAM,gDAAgD;AAMlE,GAFA,aAAa,WAAW,cAAc,EACtC,aAAa,WAAW,iBAAiB,EACzC,aAAa,WAAW,qBAAqB;AAE7C,OAAI;AAEF,IADA,EAAM,WAAW,GAAK,EACtB,EAAM,SAAS,KAAK;IAGpB,IAAM,EAAE,SAAM,aAAU,MAAM,EAA0B,EACtD,WAAW,EACT,OAAO;KACL,UAAU,EAAS,aAAa;KAChC;KACA;KACD,EACF,EACF,CAAC;AAEF,QAAI,CAAC,GAAM,mBAAmB;KAC5B,IAAM,IACJ,GAAO,WAAW;AACpB,WAAU,MAAM,EAAO;;IAGzB,IAAM,IAAS,EAAK;AAsBpB,WApBI,EAAO,eACT,EAAM,eAAe,IAAM,EAAO,kBAAkB,KAAA,EAAU,EACvD,MAGL,EAAO,WAAW,EAAO,QAAQ,EAAO,SAC1C,EAAM,QAAQ,EAAO,KAAK,EAC1B,EAAM,cAAc,EAAO,OAAO,EAAO,gBAAgB,GAAG,EAC5D,EAAM,iBAAiB,GAAK,EAC5B,EAAM,eAAe,GAAM,EAE3B,EAAQ,gBACN,EAAO,MACP,EAAO,OACP,EAAO,gBAAgB,KAAA,EACxB,IACQ,EAAO,SAChB,EAAM,SAAS,EAAO,MAAM,EAGvB;YACA,GAAK;IACZ,IAAM,IACJ,aAAe,QAAQ,EAAI,UAAU;AAEvC,UADA,EAAM,SAAS,EAAQ,EACjB;aACE;AACR,MAAM,WAAW,GAAM;;;EAoCzB,2BA7BgC,EAAc,SAAS;EA8BvD,2BAzBgC,EAAc,SAAS;EA0BvD,8BArBmC,EAAc,YAAY;EAsB7D,uBAjB4B,MAQrB,CAAC,CAP4C;GAClD,uBAAA,KAAA;GACA,uBAAA,KAAA;GACA,0BAAA,KAAA;GACD,CACc,EAAc,GACG;EAWhC,WAAW;EACZ"}
|
|
1
|
+
{"version":3,"file":"useOAuthOperations.js","names":[],"sources":["../../../src/auth/hooks/useOAuthOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport type { AuthResult, OAuthInput } from \"../types\";\n\nimport { AuthenticateOAuthDocument } from \"../../generated/global-operations\";\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface AuthenticateOAuthResponse {\n authenticateOAuth: AuthResult;\n}\n\ninterface AuthenticateOAuthVariables {\n input: OAuthInput;\n}\n\n/**\n * Supported OAuth providers\n */\nexport type OAuthProvider = \"google\" | \"github\" | \"microsoft\";\n\n/**\n * OAuth configuration for each provider\n */\ninterface OAuthConfig {\n authorizationUrl: string;\n scopes: string[];\n clientIdEnvKey: string;\n}\n\nconst OAUTH_CONFIGS: Record<OAuthProvider, OAuthConfig> = {\n google: {\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_GOOGLE_CLIENT_ID\",\n },\n github: {\n authorizationUrl: \"https://github.com/login/oauth/authorize\",\n scopes: [\"user:email\", \"read:user\"],\n clientIdEnvKey: \"VITE_GITHUB_CLIENT_ID\",\n },\n microsoft: {\n authorizationUrl:\n \"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_MICROSOFT_CLIENT_ID\",\n },\n};\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nfunction generateState(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return Array.from(array, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n \"\",\n );\n}\n\n/**\n * Read a Vite env var by key via a dynamic (non-statically-analyzable) lookup.\n *\n * Build-time composition compiles THIS package into its own npm dist first —\n * where `VITE_GOOGLE_CLIENT_ID` / `VITE_MICROSOFT_CLIENT_ID` are unset — and\n * only later bundles that dist into a host app shell. A direct\n * `import.meta.env.VITE_GOOGLE_CLIENT_ID` member read is statically replaced\n * with `undefined` during this package's build, so it ships dead and the host\n * shell's build value can never reach it (this is exactly why the SSO buttons\n * rendered permanently disabled).\n *\n * Reading the whole `import.meta.env` object and indexing it dynamically leaves\n * the lookup to runtime, so the value baked into `import.meta.env` by the HOST\n * app shell's build wins. Same pattern fe-libs uses for `VITE_GATEWAY_TARGET`.\n */\nfunction readViteEnv(key: string): string | undefined {\n try {\n const env = (import.meta as { env?: Record<string, string | undefined> })\n .env;\n return env?.[key];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Detect whether the bundle is running inside a native shell (Capacitor on\n * iOS/Android, or Electron). Mirrors the helper in LoginPage so OAuth\n * initiation can swap to a custom-scheme redirect_uri + system-browser\n * hand-off — the only flow OAuth providers will accept from a single-device\n * install.\n */\nfunction isNativeShell(): boolean {\n if (typeof window === \"undefined\") return false;\n const w = window as typeof window & {\n Capacitor?: { isNativePlatform?: () => boolean };\n };\n if (typeof w.Capacitor?.isNativePlatform === \"function\") {\n try {\n if (w.Capacitor.isNativePlatform()) return true;\n } catch {\n /* fall through */\n }\n }\n if (\n typeof navigator !== \"undefined\" &&\n /Electron/i.test(navigator.userAgent || \"\")\n ) {\n return true;\n }\n return false;\n}\n\n/**\n * Custom-scheme redirect URI used in native shells. The scheme is registered\n * by the host shell (Electron `setAsDefaultProtocolClient`, iOS Info.plist\n * CFBundleURLTypes, Android intent-filter). The OS routes the provider's\n * callback back to the running app, which forwards it to React Router via\n * the host's deep-link handler.\n *\n * IMPORTANT: This redirect URI MUST be registered for the OAuth client in\n * each provider's console:\n * - Google Cloud Console → Credentials → OAuth client → add\n * `vibecontrols://app/auth/oauth/callback` (Installed App).\n * - Microsoft Entra App registration → Authentication → register the\n * same custom-scheme redirect URI as a mobile/desktop redirect.\n * The backend `authenticateOAuth` mutation must also accept this redirectUri\n * verbatim during the code exchange.\n */\nconst NATIVE_OAUTH_REDIRECT_URI = \"vibecontrols://app/auth/oauth/callback\";\n\nasync function openOAuthAuthorizationUrl(authUrl: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n const opener = (\n window as typeof window & {\n __burdenoffOpenExternal?: (url: string) => Promise<boolean>;\n }\n ).__burdenoffOpenExternal;\n if (typeof opener === \"function\") {\n try {\n const ok = await opener(authUrl);\n if (ok) return;\n } catch {\n /* fall through to legacy hand-off */\n }\n }\n // Browser fallback: same-window navigation (current production behavior).\n window.location.href = authUrl;\n}\n\n/**\n * Hook for OAuth authentication operations\n */\nexport function useOAuthOperations() {\n const context = useAuth();\n const [authenticateOAuthMutation, { loading }] = useMutation<\n AuthenticateOAuthResponse,\n AuthenticateOAuthVariables\n >(AuthenticateOAuthDocument);\n\n /**\n * Resolve a provider's OAuth client ID.\n *\n * Priority:\n * 1. `oauthClientIds` injected by the host app shell — the only source that\n * works in published builds (see AuthRootProps.oauthClientIds for why a\n * direct `import.meta.env` read is baked dead at this package's build).\n * 2. `import.meta.env.VITE_*_CLIENT_ID` — fallback for when this package is\n * consumed from source in local dev (where the host's Vite build inlines\n * it into the same bundle).\n */\n const resolveClientId = (provider: OAuthProvider): string | undefined => {\n const fromHost = context.oauthClientIds?.[provider];\n if (fromHost) return fromHost;\n return readViteEnv(OAUTH_CONFIGS[provider].clientIdEnvKey);\n };\n\n /**\n * Initiate OAuth flow by redirecting to the provider\n * @param provider - The OAuth provider to use\n */\n const initiateOAuth = (provider: OAuthProvider) => {\n const config = OAUTH_CONFIGS[provider];\n\n // Client ID comes from the host app shell (see resolveClientId).\n const clientId = resolveClientId(provider);\n\n if (!clientId) {\n console.error(`OAuth client ID not configured for ${provider}`);\n throw new Error(\n `${provider} OAuth is not configured. Please set ${config.clientIdEnvKey} environment variable.`,\n );\n }\n\n // Generate state for CSRF protection and store it\n // Use localStorage instead of sessionStorage for better persistence across redirects\n const state = generateState();\n localStorage.setItem(\"oauth_state\", state);\n localStorage.setItem(\"oauth_provider\", provider);\n\n // Native shells (Electron, Capacitor) can't use the current origin as the\n // redirect_uri — `vibecontrols://app` / `https://localhost` aren't valid\n // registered URIs at OAuth providers. Use a custom-scheme URL that the\n // host shell has registered as a deep-link target; the OS will route the\n // provider's callback back into the running app.\n const redirectUri = isNativeShell()\n ? NATIVE_OAUTH_REDIRECT_URI\n : `${window.location.origin}/auth/oauth/callback`;\n localStorage.setItem(\"oauth_redirect_uri\", redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: config.scopes.join(\" \"),\n state: state,\n // Google-specific: force account selection\n ...(provider === \"google\" && { prompt: \"select_account\" }),\n // Google-specific: enable offline access for refresh token\n ...(provider === \"google\" && { access_type: \"offline\" }),\n });\n\n const authUrl = `${config.authorizationUrl}?${params.toString()}`;\n\n // Hand off to the system browser in native shells (keeps the embedded\n // webview in place so the deep-link callback can return cleanly). On the\n // web this falls back to same-window navigation.\n void openOAuthAuthorizationUrl(authUrl);\n };\n\n /**\n * Complete OAuth flow by exchanging authorization code for tokens\n * Called from the callback page after provider redirects back\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter for CSRF validation\n */\n const completeOAuth = async (\n code: string,\n state: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n\n // Validate state parameter\n const storedState = localStorage.getItem(\"oauth_state\");\n if (!storedState || storedState !== state) {\n console.error(\"[OAuth Complete] State mismatch!\", {\n storedState,\n urlState: state,\n });\n throw new Error(\"Invalid OAuth state. Please try again.\");\n }\n\n // Get provider and redirect URI from localStorage\n const provider = localStorage.getItem(\"oauth_provider\") as OAuthProvider;\n const redirectUri = localStorage.getItem(\"oauth_redirect_uri\");\n\n if (!provider || !redirectUri) {\n console.error(\"[OAuth Complete] Missing provider or redirectUri\");\n throw new Error(\"OAuth session data missing. Please try again.\");\n }\n\n // Clear localStorage\n localStorage.removeItem(\"oauth_state\");\n localStorage.removeItem(\"oauth_provider\");\n localStorage.removeItem(\"oauth_redirect_uri\");\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Call backend mutation to exchange code for tokens\n const { data, error } = await authenticateOAuthMutation({\n variables: {\n input: {\n provider: provider.toUpperCase(),\n code,\n redirectUri,\n },\n },\n });\n\n if (!data?.authenticateOAuth) {\n const errMsg =\n error?.message ?? \"OAuth authentication failed. Please try again.\";\n throw new Error(errMsg);\n }\n\n const result = data.authenticateOAuth;\n\n if (result.requiresMFA) {\n store.setMfaRequired(true, result.mfaChallengeId || undefined);\n return result;\n }\n\n if (result.success && result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n store.setMfaRequired(false);\n\n context.onAuthSuccess?.(\n result.user,\n result.token,\n result.refreshToken || undefined,\n );\n } else if (result.error) {\n store.setError(result.error);\n }\n\n return result;\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"OAuth authentication failed\";\n store.setError(message);\n throw err;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Convenience method to initiate Google OAuth\n */\n const initiateGoogleOAuth = () => initiateOAuth(\"google\");\n\n /**\n * Convenience method to initiate GitHub OAuth\n */\n const initiateGitHubOAuth = () => initiateOAuth(\"github\");\n\n /**\n * Convenience method to initiate Microsoft OAuth\n */\n const initiateMicrosoftOAuth = () => initiateOAuth(\"microsoft\");\n\n /**\n * Check if a provider is configured (has client ID)\n */\n const isProviderConfigured = (provider: OAuthProvider): boolean => {\n return !!resolveClientId(provider);\n };\n\n return {\n initiateOAuth,\n completeOAuth,\n initiateGoogleOAuth,\n initiateGitHubOAuth,\n initiateMicrosoftOAuth,\n isProviderConfigured,\n isLoading: loading,\n };\n}\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ var o = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,b
|
|
|
9
9
|
function s(e) {
|
|
10
10
|
if (e.hostname === "localhost" || e.hostname === "127.0.0.1" || e.hostname.endsWith(".local.burdenoff.com")) return !0;
|
|
11
11
|
if (e.protocol !== "https:") return !1;
|
|
12
|
-
for (let t of o) if (e.hostname === t || e.hostname === `app.${t}` || e.hostname === `alphaapp.${t}` || e.hostname === `www.${t}`) return !0;
|
|
12
|
+
for (let t of o) 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
13
|
return !1;
|
|
14
14
|
}
|
|
15
15
|
function c(e) {
|
|
@@ -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]);\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 \"/\";\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 (\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":";;;;;;;AA6BA,IAAM,IAAyB,IAAI,IAAI,qkBAkCtC,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,
|
|
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]);\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 (\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":";;;;;;;AA6BA,IAAM,IAAyB,IAAI,IAAI,qkBAkCtC,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,EAGhB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,2FAA4F,CAAA;EACvG,CAAA"}
|
|
@@ -9,7 +9,7 @@ var r = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,b
|
|
|
9
9
|
function c(e) {
|
|
10
10
|
if (e.hostname === "localhost" || e.hostname === "127.0.0.1" || e.hostname.endsWith(".local.burdenoff.com")) return !0;
|
|
11
11
|
if (e.protocol !== "https:") return !1;
|
|
12
|
-
for (let t of r) if (e.hostname === t || e.hostname === `app.${t}` || e.hostname === `alphaapp.${t}` || e.hostname === `www.${t}`) return !0;
|
|
12
|
+
for (let t of r) 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
13
|
return !1;
|
|
14
14
|
}
|
|
15
15
|
function l() {
|
|
@@ -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]);\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 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]);\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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"}
|
|
@@ -6,7 +6,7 @@ function c(e) {
|
|
|
6
6
|
try {
|
|
7
7
|
let t = new URL(e);
|
|
8
8
|
if (t.hostname === "localhost" || t.hostname === "127.0.0.1" || t.hostname.endsWith(".local.burdenoff.com")) return !0;
|
|
9
|
-
for (let e of s) if (t.hostname === e || t.hostname === `app.${e}` || t.hostname === `alphaapp.${e}` || t.hostname === `www.${e}`) return !0;
|
|
9
|
+
for (let e of s) if (t.hostname === e || t.hostname === `app.${e}` || t.hostname === `alphaapp.${e}` || t.hostname === `admin.${e}` || t.hostname === `alphaadmin.${e}` || t.hostname === `www.${e}`) return !0;
|
|
10
10
|
return !1;
|
|
11
11
|
} catch {
|
|
12
12
|
return !1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionBridgePage.js","names":[],"sources":["../../../src/auth/pages/SessionBridgePage.tsx"],"sourcesContent":["/**\n * SessionBridgePage — Cross-Domain Auth Session Bridge (Profile-Scoped)\n *\n * Embedded as hidden iframe by other app shells. Manages profile-scoped\n * auth tokens in localStorage and relays state via postMessage.\n *\n * Storage uses ProfileStorageManager keys:\n * bf-profiles, bf-p-{id}-user, bf-p-{id}-tokens, bf-p-{id}-workspace-token\n * bf-active-profile (sessionStorage, per-tab)\n *\n * URL: /auth/session-bridge\n */\n\nimport { useEffect, useRef } from \"react\";\n\nconst BRIDGE_SOURCE = \"burdenoff-auth-bridge\";\nconst BF_PREFIX = \"bf-p\";\nconst BF_PROFILES = \"bf-profiles\";\nconst BF_ACTIVE = \"bf-active-profile\";\n\ninterface BridgeMessage {\n type: string;\n payload?: Record<string, unknown>;\n source?: string;\n}\n\n/**\n * Dynamically validates allowed origins for the auth bridge.\n * Accepts all Burdenoff product domains (prod + alpha + local dev).\n */\nconst ALLOWED_PRODUCT_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]);\n\nfunction isAllowedOrigin(origin: string): boolean {\n try {\n const url = new URL(origin);\n // Allow all localhost ports (local dev)\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n return true;\n }\n // Allow *.local.burdenoff.com (local dev with nginx)\n if (url.hostname.endsWith(\".local.burdenoff.com\")) {\n return true;\n }\n // Allow app.{domain} and alphaapp.{domain} for all known product domains\n for (const domain of ALLOWED_PRODUCT_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 } catch {\n return false;\n }\n}\n\nfunction readAuthState(): Record<string, unknown> {\n try {\n const profiles = localStorage.getItem(BF_PROFILES);\n let activeId = sessionStorage.getItem(BF_ACTIVE);\n\n // Fall back: if sessionStorage is empty but localStorage has profiles,\n // pick the first profile (same logic as resolveActiveProfileId)\n if (!activeId && profiles) {\n try {\n const profileList = JSON.parse(profiles) as Array<{ id: string }>;\n if (profileList.length > 0) {\n activeId = profileList[0].id;\n sessionStorage.setItem(BF_ACTIVE, activeId);\n }\n } catch {\n // ignore\n }\n }\n\n if (!activeId) {\n return {\n user: null,\n tokens: null,\n profiles: profiles ? JSON.parse(profiles) : [],\n activeProfileId: null,\n };\n }\n const user = localStorage.getItem(`${BF_PREFIX}-${activeId}-user`);\n const tokens = localStorage.getItem(`${BF_PREFIX}-${activeId}-tokens`);\n const wsToken = localStorage.getItem(\n `${BF_PREFIX}-${activeId}-workspace-token`,\n );\n\n return {\n user: user ? JSON.parse(user) : null,\n tokens: tokens\n ? { ...JSON.parse(tokens), workspaceToken: wsToken ?? undefined }\n : null,\n profiles: profiles ? JSON.parse(profiles) : [],\n activeProfileId: activeId,\n };\n } catch {\n return { user: null, tokens: null, profiles: [], activeProfileId: null };\n }\n}\n\nfunction writeAuthState(payload: Record<string, unknown>): void {\n const profileId =\n (payload.activeProfileId as string) ??\n ((payload.user as Record<string, unknown>)?.id as string) ??\n sessionStorage.getItem(BF_ACTIVE);\n if (!profileId) return;\n\n if (payload.user) {\n localStorage.setItem(\n `${BF_PREFIX}-${profileId}-user`,\n JSON.stringify(payload.user),\n );\n }\n if (payload.tokens && typeof payload.tokens === \"object\") {\n const t = payload.tokens as Record<string, unknown>;\n const { workspaceToken, ...rest } = t;\n localStorage.setItem(\n `${BF_PREFIX}-${profileId}-tokens`,\n JSON.stringify(rest),\n );\n if (workspaceToken) {\n localStorage.setItem(\n `${BF_PREFIX}-${profileId}-workspace-token`,\n String(workspaceToken),\n );\n }\n }\n if (payload.profiles) {\n localStorage.setItem(BF_PROFILES, JSON.stringify(payload.profiles));\n }\n sessionStorage.setItem(BF_ACTIVE, profileId);\n}\n\nfunction clearAuthState(): void {\n const activeId = sessionStorage.getItem(BF_ACTIVE);\n if (activeId) {\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-user`);\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-tokens`);\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-workspace-token`);\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-auth-storage`);\n // Remove from profiles list\n try {\n const raw = localStorage.getItem(BF_PROFILES);\n const profiles = raw ? JSON.parse(raw) : [];\n localStorage.setItem(\n BF_PROFILES,\n JSON.stringify(\n profiles.filter((p: { id: string }) => p.id !== activeId),\n ),\n );\n } catch {\n /* ignore */\n }\n sessionStorage.removeItem(BF_ACTIVE);\n }\n localStorage.removeItem(\"auth-storage\");\n}\n\nfunction clearAllAuthState(): void {\n // Get all profiles and remove their data\n try {\n const raw = localStorage.getItem(BF_PROFILES);\n const profiles: Array<{ id: string }> = raw ? JSON.parse(raw) : [];\n for (const p of profiles) {\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-user`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-tokens`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-workspace-token`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-auth-storage`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-context`);\n }\n } catch {\n /* ignore */\n }\n for (const key of Object.keys(localStorage)) {\n if (key.startsWith(`${BF_PREFIX}-`) && key.endsWith(\"-auth-storage\")) {\n localStorage.removeItem(key);\n }\n }\n localStorage.removeItem(BF_PROFILES);\n localStorage.removeItem(\"auth-storage\");\n sessionStorage.removeItem(BF_ACTIVE);\n}\n\n// SSO cookie helpers (shared domain cookie for local dev fast-path)\nconst SSO_COOKIE_NAME = \"bf-sso-profile\";\nconst SHELL_SSO_COOKIE_NAME = \"bf-sso\";\nfunction setSSoCookieBridge(profileId: string, origin: string): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart = window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const cookieValue = JSON.stringify({ id: profileId, origin });\n document.cookie = `${SSO_COOKIE_NAME}=${encodeURIComponent(cookieValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n } catch {\n /* ignore */\n }\n}\n\nfunction setShellSsoCookieBridge(profileId: string, origin: string): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart = window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const ssoValue = JSON.stringify({ id: profileId, origin });\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n } catch {\n /* ignore */\n }\n}\n\nfunction clearSSoCookieBridge(): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n document.cookie = `${SSO_COOKIE_NAME}=; path=/; max-age=0`;\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=; path=/; max-age=0`;\n if (domain) {\n document.cookie = `${SSO_COOKIE_NAME}=; path=/; domain=${domain}; max-age=0`;\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=; path=/; domain=${domain}; max-age=0`;\n }\n if (hostname && ![\"localhost\", \"127.0.0.1\"].includes(hostname)) {\n document.cookie = `${SSO_COOKIE_NAME}=; path=/; domain=${hostname}; max-age=0`;\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=; path=/; domain=${hostname}; max-age=0`;\n }\n } catch {\n /* ignore */\n }\n}\n\nexport function SessionBridgePage() {\n const connectedParents = useRef<Set<MessageEventSource>>(new Set());\n const initialReadySent = useRef(false);\n const readySentTo = useRef<Set<MessageEventSource>>(new Set());\n const storageAccessGranted = useRef(false);\n\n // Request unpartitioned storage access (Storage Access API).\n // This is critical for cross-domain SSO in modern browsers that\n // partition third-party iframe localStorage.\n useEffect(() => {\n async function requestAccess() {\n try {\n // Check if Storage Access API is available\n if (!document.requestStorageAccess) {\n storageAccessGranted.current = true;\n return;\n }\n // Check if we already have access\n const hasAccess = await document.hasStorageAccess();\n if (hasAccess) {\n storageAccessGranted.current = true;\n return;\n }\n // Request unpartitioned storage access\n await document.requestStorageAccess();\n storageAccessGranted.current = true;\n } catch {\n // Storage Access API not available or denied — fall through\n // to cookie-based fallback. localStorage will be partitioned.\n storageAccessGranted.current = false;\n }\n }\n void requestAccess();\n }, []);\n\n useEffect(() => {\n function isOriginAllowed(origin: string): boolean {\n return isAllowedOrigin(origin);\n }\n\n function broadcastToAll(\n message: BridgeMessage,\n excludeSource?: MessageEventSource | null,\n ): void {\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, \"*\");\n }\n connectedParents.current.forEach((parent) => {\n if (parent !== excludeSource && \"postMessage\" in parent) {\n (parent as Window).postMessage(message, \"*\");\n }\n });\n }\n\n function handleMessage(event: MessageEvent): void {\n if (!isOriginAllowed(event.origin)) return;\n const message = event.data as BridgeMessage;\n if (!message?.type || message.source === BRIDGE_SOURCE) return;\n\n switch (message.type) {\n case \"AUTH_INIT\": {\n if (event.source) connectedParents.current.add(event.source);\n\n // If the parent sends its current auth state in AUTH_INIT,\n // store it so other cross-domain shells can pick it up.\n if (message.payload && typeof message.payload === \"object\") {\n const initPayload = message.payload as Record<string, unknown>;\n if (initPayload.user && initPayload.tokens) {\n writeAuthState(initPayload);\n }\n }\n\n const state = readAuthState();\n if (event.source && \"postMessage\" in event.source) {\n (event.source as Window).postMessage(\n { type: \"AUTH_STATE\", payload: state, source: BRIDGE_SOURCE },\n event.origin,\n );\n if (!readySentTo.current.has(event.source)) {\n readySentTo.current.add(event.source);\n (event.source as Window).postMessage(\n { type: \"AUTH_BRIDGE_READY\", source: BRIDGE_SOURCE },\n event.origin,\n );\n }\n }\n break;\n }\n case \"AUTH_LOGIN\": {\n if (message.payload) {\n writeAuthState(message.payload);\n // Set SSO cookie for cross-domain fast-path detection\n const loginPayload = message.payload as Record<string, unknown>;\n const loginProfileId =\n (loginPayload.activeProfileId as string) ??\n ((loginPayload.user as Record<string, unknown>)?.id as string);\n if (loginProfileId) {\n clearSSoCookieBridge();\n setSSoCookieBridge(loginProfileId, event.origin);\n setShellSsoCookieBridge(loginProfileId, event.origin);\n }\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"login\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n }\n break;\n }\n case \"AUTH_LOGOUT\": {\n clearAuthState();\n clearSSoCookieBridge();\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"logout\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n break;\n }\n case \"AUTH_LOGOUT_ALL\": {\n clearAllAuthState();\n clearSSoCookieBridge();\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"logout_all\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n break;\n }\n case \"AUTH_TOKEN_REFRESH\": {\n if (message.payload?.tokens) {\n writeAuthState({ tokens: message.payload.tokens });\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"refresh\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n }\n break;\n }\n case \"AUTH_ACCOUNT_SWITCH\": {\n if (message.payload) {\n writeAuthState(message.payload);\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"account_switch\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n }\n break;\n }\n }\n }\n\n function handleStorageChange(event: StorageEvent): void {\n if (event.key?.startsWith(BF_PREFIX) || event.key === BF_PROFILES) {\n broadcastToAll({\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"storage_sync\" },\n source: BRIDGE_SOURCE,\n });\n }\n }\n\n window.addEventListener(\"message\", handleMessage);\n window.addEventListener(\"storage\", handleStorageChange);\n\n // Send initial AUTH_BRIDGE_READY to parent (only once, guard against StrictMode double-mount)\n if (\n window.parent &&\n window.parent !== window &&\n !initialReadySent.current\n ) {\n initialReadySent.current = true;\n window.parent.postMessage(\n { type: \"AUTH_BRIDGE_READY\", source: BRIDGE_SOURCE },\n \"*\",\n );\n }\n\n return () => {\n window.removeEventListener(\"message\", handleMessage);\n window.removeEventListener(\"storage\", handleStorageChange);\n // Don't clear connectedParents or readySentTo on cleanup to maintain state across StrictMode remounts\n };\n }, []);\n\n return (\n <div\n id=\"auth-session-bridge-root\"\n data-bridge=\"active\"\n style={{ display: \"none\" }}\n />\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SessionBridgePage.js","names":[],"sources":["../../../src/auth/pages/SessionBridgePage.tsx"],"sourcesContent":["/**\n * SessionBridgePage — Cross-Domain Auth Session Bridge (Profile-Scoped)\n *\n * Embedded as hidden iframe by other app shells. Manages profile-scoped\n * auth tokens in localStorage and relays state via postMessage.\n *\n * Storage uses ProfileStorageManager keys:\n * bf-profiles, bf-p-{id}-user, bf-p-{id}-tokens, bf-p-{id}-workspace-token\n * bf-active-profile (sessionStorage, per-tab)\n *\n * URL: /auth/session-bridge\n */\n\nimport { useEffect, useRef } from \"react\";\n\nconst BRIDGE_SOURCE = \"burdenoff-auth-bridge\";\nconst BF_PREFIX = \"bf-p\";\nconst BF_PROFILES = \"bf-profiles\";\nconst BF_ACTIVE = \"bf-active-profile\";\n\ninterface BridgeMessage {\n type: string;\n payload?: Record<string, unknown>;\n source?: string;\n}\n\n/**\n * Dynamically validates allowed origins for the auth bridge.\n * Accepts all Burdenoff product domains (prod + alpha + local dev).\n */\nconst ALLOWED_PRODUCT_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]);\n\nfunction isAllowedOrigin(origin: string): boolean {\n try {\n const url = new URL(origin);\n // Allow all localhost ports (local dev)\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n return true;\n }\n // Allow *.local.burdenoff.com (local dev with nginx)\n if (url.hostname.endsWith(\".local.burdenoff.com\")) {\n return true;\n }\n // Allow app.{domain} and alphaapp.{domain} for all known product domains\n for (const domain of ALLOWED_PRODUCT_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)\n // embed this bridge iframe too. Without trusting their origin, the\n // bridge ignores their postMessages and the admin shell never receives\n // the shared session.\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 } catch {\n return false;\n }\n}\n\nfunction readAuthState(): Record<string, unknown> {\n try {\n const profiles = localStorage.getItem(BF_PROFILES);\n let activeId = sessionStorage.getItem(BF_ACTIVE);\n\n // Fall back: if sessionStorage is empty but localStorage has profiles,\n // pick the first profile (same logic as resolveActiveProfileId)\n if (!activeId && profiles) {\n try {\n const profileList = JSON.parse(profiles) as Array<{ id: string }>;\n if (profileList.length > 0) {\n activeId = profileList[0].id;\n sessionStorage.setItem(BF_ACTIVE, activeId);\n }\n } catch {\n // ignore\n }\n }\n\n if (!activeId) {\n return {\n user: null,\n tokens: null,\n profiles: profiles ? JSON.parse(profiles) : [],\n activeProfileId: null,\n };\n }\n const user = localStorage.getItem(`${BF_PREFIX}-${activeId}-user`);\n const tokens = localStorage.getItem(`${BF_PREFIX}-${activeId}-tokens`);\n const wsToken = localStorage.getItem(\n `${BF_PREFIX}-${activeId}-workspace-token`,\n );\n\n return {\n user: user ? JSON.parse(user) : null,\n tokens: tokens\n ? { ...JSON.parse(tokens), workspaceToken: wsToken ?? undefined }\n : null,\n profiles: profiles ? JSON.parse(profiles) : [],\n activeProfileId: activeId,\n };\n } catch {\n return { user: null, tokens: null, profiles: [], activeProfileId: null };\n }\n}\n\nfunction writeAuthState(payload: Record<string, unknown>): void {\n const profileId =\n (payload.activeProfileId as string) ??\n ((payload.user as Record<string, unknown>)?.id as string) ??\n sessionStorage.getItem(BF_ACTIVE);\n if (!profileId) return;\n\n if (payload.user) {\n localStorage.setItem(\n `${BF_PREFIX}-${profileId}-user`,\n JSON.stringify(payload.user),\n );\n }\n if (payload.tokens && typeof payload.tokens === \"object\") {\n const t = payload.tokens as Record<string, unknown>;\n const { workspaceToken, ...rest } = t;\n localStorage.setItem(\n `${BF_PREFIX}-${profileId}-tokens`,\n JSON.stringify(rest),\n );\n if (workspaceToken) {\n localStorage.setItem(\n `${BF_PREFIX}-${profileId}-workspace-token`,\n String(workspaceToken),\n );\n }\n }\n if (payload.profiles) {\n localStorage.setItem(BF_PROFILES, JSON.stringify(payload.profiles));\n }\n sessionStorage.setItem(BF_ACTIVE, profileId);\n}\n\nfunction clearAuthState(): void {\n const activeId = sessionStorage.getItem(BF_ACTIVE);\n if (activeId) {\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-user`);\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-tokens`);\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-workspace-token`);\n localStorage.removeItem(`${BF_PREFIX}-${activeId}-auth-storage`);\n // Remove from profiles list\n try {\n const raw = localStorage.getItem(BF_PROFILES);\n const profiles = raw ? JSON.parse(raw) : [];\n localStorage.setItem(\n BF_PROFILES,\n JSON.stringify(\n profiles.filter((p: { id: string }) => p.id !== activeId),\n ),\n );\n } catch {\n /* ignore */\n }\n sessionStorage.removeItem(BF_ACTIVE);\n }\n localStorage.removeItem(\"auth-storage\");\n}\n\nfunction clearAllAuthState(): void {\n // Get all profiles and remove their data\n try {\n const raw = localStorage.getItem(BF_PROFILES);\n const profiles: Array<{ id: string }> = raw ? JSON.parse(raw) : [];\n for (const p of profiles) {\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-user`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-tokens`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-workspace-token`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-auth-storage`);\n localStorage.removeItem(`${BF_PREFIX}-${p.id}-context`);\n }\n } catch {\n /* ignore */\n }\n for (const key of Object.keys(localStorage)) {\n if (key.startsWith(`${BF_PREFIX}-`) && key.endsWith(\"-auth-storage\")) {\n localStorage.removeItem(key);\n }\n }\n localStorage.removeItem(BF_PROFILES);\n localStorage.removeItem(\"auth-storage\");\n sessionStorage.removeItem(BF_ACTIVE);\n}\n\n// SSO cookie helpers (shared domain cookie for local dev fast-path)\nconst SSO_COOKIE_NAME = \"bf-sso-profile\";\nconst SHELL_SSO_COOKIE_NAME = \"bf-sso\";\nfunction setSSoCookieBridge(profileId: string, origin: string): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart = window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const cookieValue = JSON.stringify({ id: profileId, origin });\n document.cookie = `${SSO_COOKIE_NAME}=${encodeURIComponent(cookieValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n } catch {\n /* ignore */\n }\n}\n\nfunction setShellSsoCookieBridge(profileId: string, origin: string): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart = window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const ssoValue = JSON.stringify({ id: profileId, origin });\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n } catch {\n /* ignore */\n }\n}\n\nfunction clearSSoCookieBridge(): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n document.cookie = `${SSO_COOKIE_NAME}=; path=/; max-age=0`;\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=; path=/; max-age=0`;\n if (domain) {\n document.cookie = `${SSO_COOKIE_NAME}=; path=/; domain=${domain}; max-age=0`;\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=; path=/; domain=${domain}; max-age=0`;\n }\n if (hostname && ![\"localhost\", \"127.0.0.1\"].includes(hostname)) {\n document.cookie = `${SSO_COOKIE_NAME}=; path=/; domain=${hostname}; max-age=0`;\n document.cookie = `${SHELL_SSO_COOKIE_NAME}=; path=/; domain=${hostname}; max-age=0`;\n }\n } catch {\n /* ignore */\n }\n}\n\nexport function SessionBridgePage() {\n const connectedParents = useRef<Set<MessageEventSource>>(new Set());\n const initialReadySent = useRef(false);\n const readySentTo = useRef<Set<MessageEventSource>>(new Set());\n const storageAccessGranted = useRef(false);\n\n // Request unpartitioned storage access (Storage Access API).\n // This is critical for cross-domain SSO in modern browsers that\n // partition third-party iframe localStorage.\n useEffect(() => {\n async function requestAccess() {\n try {\n // Check if Storage Access API is available\n if (!document.requestStorageAccess) {\n storageAccessGranted.current = true;\n return;\n }\n // Check if we already have access\n const hasAccess = await document.hasStorageAccess();\n if (hasAccess) {\n storageAccessGranted.current = true;\n return;\n }\n // Request unpartitioned storage access\n await document.requestStorageAccess();\n storageAccessGranted.current = true;\n } catch {\n // Storage Access API not available or denied — fall through\n // to cookie-based fallback. localStorage will be partitioned.\n storageAccessGranted.current = false;\n }\n }\n void requestAccess();\n }, []);\n\n useEffect(() => {\n function isOriginAllowed(origin: string): boolean {\n return isAllowedOrigin(origin);\n }\n\n function broadcastToAll(\n message: BridgeMessage,\n excludeSource?: MessageEventSource | null,\n ): void {\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, \"*\");\n }\n connectedParents.current.forEach((parent) => {\n if (parent !== excludeSource && \"postMessage\" in parent) {\n (parent as Window).postMessage(message, \"*\");\n }\n });\n }\n\n function handleMessage(event: MessageEvent): void {\n if (!isOriginAllowed(event.origin)) return;\n const message = event.data as BridgeMessage;\n if (!message?.type || message.source === BRIDGE_SOURCE) return;\n\n switch (message.type) {\n case \"AUTH_INIT\": {\n if (event.source) connectedParents.current.add(event.source);\n\n // If the parent sends its current auth state in AUTH_INIT,\n // store it so other cross-domain shells can pick it up.\n if (message.payload && typeof message.payload === \"object\") {\n const initPayload = message.payload as Record<string, unknown>;\n if (initPayload.user && initPayload.tokens) {\n writeAuthState(initPayload);\n }\n }\n\n const state = readAuthState();\n if (event.source && \"postMessage\" in event.source) {\n (event.source as Window).postMessage(\n { type: \"AUTH_STATE\", payload: state, source: BRIDGE_SOURCE },\n event.origin,\n );\n if (!readySentTo.current.has(event.source)) {\n readySentTo.current.add(event.source);\n (event.source as Window).postMessage(\n { type: \"AUTH_BRIDGE_READY\", source: BRIDGE_SOURCE },\n event.origin,\n );\n }\n }\n break;\n }\n case \"AUTH_LOGIN\": {\n if (message.payload) {\n writeAuthState(message.payload);\n // Set SSO cookie for cross-domain fast-path detection\n const loginPayload = message.payload as Record<string, unknown>;\n const loginProfileId =\n (loginPayload.activeProfileId as string) ??\n ((loginPayload.user as Record<string, unknown>)?.id as string);\n if (loginProfileId) {\n clearSSoCookieBridge();\n setSSoCookieBridge(loginProfileId, event.origin);\n setShellSsoCookieBridge(loginProfileId, event.origin);\n }\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"login\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n }\n break;\n }\n case \"AUTH_LOGOUT\": {\n clearAuthState();\n clearSSoCookieBridge();\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"logout\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n break;\n }\n case \"AUTH_LOGOUT_ALL\": {\n clearAllAuthState();\n clearSSoCookieBridge();\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"logout_all\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n break;\n }\n case \"AUTH_TOKEN_REFRESH\": {\n if (message.payload?.tokens) {\n writeAuthState({ tokens: message.payload.tokens });\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"refresh\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n }\n break;\n }\n case \"AUTH_ACCOUNT_SWITCH\": {\n if (message.payload) {\n writeAuthState(message.payload);\n broadcastToAll(\n {\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"account_switch\" },\n source: BRIDGE_SOURCE,\n },\n event.source,\n );\n }\n break;\n }\n }\n }\n\n function handleStorageChange(event: StorageEvent): void {\n if (event.key?.startsWith(BF_PREFIX) || event.key === BF_PROFILES) {\n broadcastToAll({\n type: \"AUTH_STATE_CHANGED\",\n payload: { ...readAuthState(), trigger: \"storage_sync\" },\n source: BRIDGE_SOURCE,\n });\n }\n }\n\n window.addEventListener(\"message\", handleMessage);\n window.addEventListener(\"storage\", handleStorageChange);\n\n // Send initial AUTH_BRIDGE_READY to parent (only once, guard against StrictMode double-mount)\n if (\n window.parent &&\n window.parent !== window &&\n !initialReadySent.current\n ) {\n initialReadySent.current = true;\n window.parent.postMessage(\n { type: \"AUTH_BRIDGE_READY\", source: BRIDGE_SOURCE },\n \"*\",\n );\n }\n\n return () => {\n window.removeEventListener(\"message\", handleMessage);\n window.removeEventListener(\"storage\", handleStorageChange);\n // Don't clear connectedParents or readySentTo on cleanup to maintain state across StrictMode remounts\n };\n }, []);\n\n return (\n <div\n id=\"auth-session-bridge-root\"\n data-bridge=\"active\"\n style={{ display: \"none\" }}\n />\n );\n}\n"],"mappings":"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"}
|