@burdenoff/microfe-auth 2026.1023.1 → 2026.1030.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -9,8 +9,12 @@ import { clearWorkspaceWideSelection as p, isWorkspaceWideSelection as m, select
|
|
|
9
9
|
import { getUrlParam as g } from "@burdenoff/fe-libs/shared/providers/shell/ActiveContextStorage";
|
|
10
10
|
import { useActiveContext as _ } from "@burdenoff/fe-libs/shared/providers/shell/ActiveContextProvider";
|
|
11
11
|
//#region src/auth/hooks/useContextSwitcherData.ts
|
|
12
|
-
var v = "\n query MyOrganizations {\n myOrganizations {\n total\n items {\n id\n name\n slug\n tenantId\n description\n logo\n website\n status\n type\n createdAt\n updatedAt\n }\n }\n }\n", y = "\n query WorkspacesByOrg {\n workspaces: myWorkspaces(limit: 100) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n", b = "\n query MyWorkspacesPage($limit: Int, $offset: Int) {\n workspaces: myWorkspaces(limit: $limit, offset: $offset) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n", x =
|
|
13
|
-
function
|
|
12
|
+
var v = "\n query MyOrganizations {\n myOrganizations {\n total\n items {\n id\n name\n slug\n tenantId\n description\n logo\n website\n status\n type\n createdAt\n updatedAt\n }\n }\n }\n", y = "\n query WorkspacesByOrg {\n workspaces: myWorkspaces(limit: 100) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n", b = "\n query MyWorkspacesPage($limit: Int, $offset: Int) {\n workspaces: myWorkspaces(limit: $limit, offset: $offset) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n", x = new Set(["NOT_PROJECT_MEMBER", "PROJECT_NOT_FOUND"]);
|
|
13
|
+
function S(e) {
|
|
14
|
+
return !!e && x.has(e);
|
|
15
|
+
}
|
|
16
|
+
var C = 100, w = 20, ee = "\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n expiresAt\n organizationId\n workspaceId\n projectId\n scopes\n error {\n code\n message\n }\n }\n }\n", te = "\n query MyProjectsForContext {\n projects: myProjects {\n total\n items {\n id\n name\n key\n workspaceId\n status\n archived\n createdAt\n updatedAt\n }\n }\n }\n";
|
|
17
|
+
function T(e, t = 3e4) {
|
|
14
18
|
try {
|
|
15
19
|
let n = JSON.parse(atob(e.split(".")[1]));
|
|
16
20
|
return n.exp ? n.exp * 1e3 < Date.now() + t : !1;
|
|
@@ -18,7 +22,7 @@ function C(e, t = 3e4) {
|
|
|
18
22
|
return !0;
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
|
-
function
|
|
25
|
+
function E(e) {
|
|
22
26
|
try {
|
|
23
27
|
let t = JSON.parse(atob(e.split(".")[1]));
|
|
24
28
|
return {
|
|
@@ -31,33 +35,33 @@ function w(e) {
|
|
|
31
35
|
return null;
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
|
-
function
|
|
35
|
-
if (!e ||
|
|
36
|
-
let n =
|
|
38
|
+
function D(e, t) {
|
|
39
|
+
if (!e || T(e)) return !1;
|
|
40
|
+
let n = E(e);
|
|
37
41
|
return !(!n || t.workspaceId && n.workspaceId !== t.workspaceId || t.organizationId && n.organizationId !== t.organizationId || t.workspaceWide && n.projectId || t.projectId && n.projectId !== t.projectId);
|
|
38
42
|
}
|
|
39
|
-
var
|
|
43
|
+
var O = class extends Error {
|
|
40
44
|
constructor(e) {
|
|
41
45
|
super(e), this.name = "AuthError";
|
|
42
46
|
}
|
|
43
47
|
};
|
|
44
|
-
function
|
|
48
|
+
function k(e) {
|
|
45
49
|
return e.some((e) => e.extensions?.code === "UNAUTHENTICATED" || e.message?.toLowerCase().includes("unauthorized") || e.message?.toLowerCase().includes("not authenticated"));
|
|
46
50
|
}
|
|
47
|
-
function
|
|
51
|
+
function A() {
|
|
48
52
|
let t = e.getState().accessToken;
|
|
49
|
-
if (t && !
|
|
53
|
+
if (t && !T(t)) return t;
|
|
50
54
|
let n = d();
|
|
51
|
-
return n && !
|
|
55
|
+
return n && !T(n) ? n : t;
|
|
52
56
|
}
|
|
53
|
-
async function
|
|
57
|
+
async function j(e, t, n) {
|
|
54
58
|
let r = [...e.items], i = e.total;
|
|
55
|
-
for (let e = 1; e <
|
|
59
|
+
for (let e = 1; e < w && r.length < i; e += 1) {
|
|
56
60
|
let e = await f({
|
|
57
61
|
gateway: "global",
|
|
58
62
|
query: b,
|
|
59
63
|
variables: {
|
|
60
|
-
limit:
|
|
64
|
+
limit: C,
|
|
61
65
|
offset: r.length
|
|
62
66
|
},
|
|
63
67
|
authToken: t,
|
|
@@ -74,7 +78,7 @@ async function k(e, t, n) {
|
|
|
74
78
|
items: r
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
|
-
function
|
|
81
|
+
function M(e) {
|
|
78
82
|
return Array.from(new Set(e.flatMap((e) => e.organizationId ? [e.organizationId] : []))).map((t, n) => ({
|
|
79
83
|
id: t,
|
|
80
84
|
name: e.find((e) => e.organizationId === t)?.name || `Organization ${n + 1}`,
|
|
@@ -82,36 +86,36 @@ function A(e) {
|
|
|
82
86
|
tenantId: e.find((e) => e.organizationId === t)?.tenantId || ""
|
|
83
87
|
}));
|
|
84
88
|
}
|
|
85
|
-
var
|
|
89
|
+
var N = new Set([
|
|
86
90
|
"project.created",
|
|
87
91
|
"project.updated",
|
|
88
92
|
"project.deleted",
|
|
89
93
|
"project.archived",
|
|
90
94
|
"project.unarchived"
|
|
91
|
-
]),
|
|
95
|
+
]), P = new Set([
|
|
92
96
|
"workspace.created",
|
|
93
97
|
"workspace.updated",
|
|
94
98
|
"workspace.deleted",
|
|
95
99
|
"workspace.archived",
|
|
96
100
|
"workspace.reactivated"
|
|
97
101
|
]);
|
|
98
|
-
function
|
|
102
|
+
function F(e) {
|
|
99
103
|
if (e && typeof e == "object" && "workspaceId" in e) {
|
|
100
104
|
let t = e.workspaceId;
|
|
101
105
|
return typeof t == "string" ? t : void 0;
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
|
-
function
|
|
105
|
-
let { organizationId: b, workspaceId: x, projectId: S, tenantId: C, setContext: w } = _(),
|
|
108
|
+
function I(d) {
|
|
109
|
+
let { organizationId: b, workspaceId: x, projectId: S, tenantId: C, setContext: w } = _(), T = u(), E = e((e) => e.organizations), I = e((e) => e.currentOrganizationId), L = e((e) => e.isLoadingOrganizations), R = e((e) => e.workspaces), ne = e((e) => e.currentWorkspaceId), re = e((e) => e.workspaceToken), ie = e((e) => e.isLoadingWorkspaces), ae = e((e) => e.projects), z = e((e) => e.currentProjectId), B = e((e) => e.isLoadingProjects);
|
|
106
110
|
o(() => {
|
|
107
|
-
let e =
|
|
111
|
+
let e = R.find((e) => e.id === x && e.organizationId === b), t = E.find((e) => e.id === b);
|
|
108
112
|
!e || !t || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(e.tenantId) || e.tenantId !== t.tenantId || C === e.tenantId || w({ tenantId: e.tenantId });
|
|
109
113
|
}, [
|
|
110
114
|
b,
|
|
111
115
|
x,
|
|
112
116
|
C,
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
E,
|
|
118
|
+
R,
|
|
115
119
|
w
|
|
116
120
|
]);
|
|
117
121
|
let [oe, V] = c(null), [se, H] = c(0), U = s(0), W = s(null), G = s(/* @__PURE__ */ new Map()), K = s(x), q = s(b);
|
|
@@ -124,7 +128,7 @@ function P(d) {
|
|
|
124
128
|
if (b && b !== t.currentOrganizationId && b !== le.current && (le.current = b, t.setCurrentOrganizationId(b), t.workspaces.length > 0 && !t.workspaces.some((e) => e.organizationId === b) && t.setWorkspaces([])), x && x !== t.currentWorkspaceId && x !== ue.current && (ue.current = x, t.setCurrentWorkspaceId(x), t.projects.length > 0 && !t.projects.some((e) => e.workspaceId === x) && (t.setProjects([]), t.setCurrentProjectId(null))), m({
|
|
125
129
|
organizationId: b,
|
|
126
130
|
workspaceId: x
|
|
127
|
-
}) && !S && t.currentProjectId && (t.setCurrentProjectId(null), t.setWorkspaceToken(null)), S && S !== t.currentProjectId && S !== de.current && (de.current = S, t.setCurrentProjectId(S)), t.workspaceToken && !
|
|
131
|
+
}) && !S && t.currentProjectId && (t.setCurrentProjectId(null), t.setWorkspaceToken(null)), S && S !== t.currentProjectId && S !== de.current && (de.current = S, t.setCurrentProjectId(S)), t.workspaceToken && !D(t.workspaceToken, {
|
|
128
132
|
workspaceId: x,
|
|
129
133
|
organizationId: b,
|
|
130
134
|
projectId: S,
|
|
@@ -143,7 +147,7 @@ function P(d) {
|
|
|
143
147
|
]);
|
|
144
148
|
let X = a(async () => {
|
|
145
149
|
if (W.current) return W.current;
|
|
146
|
-
let t = e.getState(), a =
|
|
150
|
+
let t = e.getState(), a = A();
|
|
147
151
|
return a ? (t.setLoadingOrganizations(!0), V(null), W.current = (async () => {
|
|
148
152
|
try {
|
|
149
153
|
let o = q.current ?? t.currentOrganizationId, s = K.current, c = !!s && g("workspace") === s, l = !!o && g("org") === o, u = o || s ? await (async () => {
|
|
@@ -178,7 +182,7 @@ function P(d) {
|
|
|
178
182
|
baseUrl: J
|
|
179
183
|
})), u.errors?.length) {
|
|
180
184
|
let e = u.errors[0]?.message ?? "GraphQL error";
|
|
181
|
-
throw
|
|
185
|
+
throw k(u.errors) ? new O(e) : (t.workspaces.length === 0 && console.error("Failed to fetch organizations:", u.errors), Error(e));
|
|
182
186
|
}
|
|
183
187
|
let d = u.data?.myOrganizations?.items ?? [];
|
|
184
188
|
if (o && d.length === 0 && t.workspaces.length === 0) {
|
|
@@ -198,13 +202,13 @@ function P(d) {
|
|
|
198
202
|
else {
|
|
199
203
|
console.warn("[ContextSwitcher] Stale-context recovery failed:", e.errors);
|
|
200
204
|
let t = e.errors?.[0]?.message ?? "Failed to fetch organizations";
|
|
201
|
-
throw
|
|
205
|
+
throw k(e.errors ?? []) ? new O(t) : Error(t);
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
|
-
let p = d.length === 0 && t.workspaces.length > 0 ?
|
|
208
|
+
let p = d.length === 0 && t.workspaces.length > 0 ? M(t.workspaces) : [], m = p.length > 0 ? p : d;
|
|
205
209
|
t.setOrganizations(m);
|
|
206
210
|
let h = o, _ = u.data?.workspaces ?? null;
|
|
207
|
-
c && _ && !_.items.some((e) => e.id === s) && !i(_) && (_ = await
|
|
211
|
+
c && _ && !_.items.some((e) => e.id === s) && !i(_) && (_ = await j(_, a, J));
|
|
208
212
|
let b = r({
|
|
209
213
|
requestedWorkspaceId: s,
|
|
210
214
|
fromUrl: c,
|
|
@@ -266,10 +270,10 @@ function P(d) {
|
|
|
266
270
|
}
|
|
267
271
|
return U.current = 0, m;
|
|
268
272
|
} catch (e) {
|
|
269
|
-
if (e instanceof
|
|
273
|
+
if (e instanceof O) return [];
|
|
270
274
|
let n = e instanceof Error ? e.message : "Failed to fetch organizations";
|
|
271
275
|
if (t.workspaces.length === 0 && console.error("Failed to fetch organizations:", e), V(n), H((e) => e + 1), t.workspaces.length > 0) {
|
|
272
|
-
let e =
|
|
276
|
+
let e = M(t.workspaces);
|
|
273
277
|
if (e.length > 0) return t.setOrganizations(e), t.currentOrganizationId || t.setCurrentOrganizationId(e[0].id), e;
|
|
274
278
|
}
|
|
275
279
|
return [];
|
|
@@ -279,7 +283,7 @@ function P(d) {
|
|
|
279
283
|
})(), W.current) : [];
|
|
280
284
|
}, [J, w]);
|
|
281
285
|
o(() => {
|
|
282
|
-
if (se === 0 || U.current >= 5 || !
|
|
286
|
+
if (se === 0 || U.current >= 5 || !A()) return;
|
|
283
287
|
U.current += 1;
|
|
284
288
|
let e = Math.min(1500 * 2 ** (U.current - 1), 12e3), t = window.setTimeout(() => {
|
|
285
289
|
X();
|
|
@@ -289,14 +293,14 @@ function P(d) {
|
|
|
289
293
|
};
|
|
290
294
|
}, [se, X]), o(() => {
|
|
291
295
|
let t = () => {
|
|
292
|
-
e.getState().organizations.length === 0 &&
|
|
296
|
+
e.getState().organizations.length === 0 && A() && (U.current = 0, X());
|
|
293
297
|
};
|
|
294
298
|
return window.addEventListener("online", t), () => {
|
|
295
299
|
window.removeEventListener("online", t);
|
|
296
300
|
};
|
|
297
301
|
}, [X]);
|
|
298
302
|
let Z = a(async (t) => {
|
|
299
|
-
let n = e.getState(), r =
|
|
303
|
+
let n = e.getState(), r = A(), i = l(), a = n.user?.id, o = JSON.stringify([
|
|
300
304
|
i,
|
|
301
305
|
a,
|
|
302
306
|
r,
|
|
@@ -305,7 +309,7 @@ function P(d) {
|
|
|
305
309
|
if (s) return s;
|
|
306
310
|
let c = () => {
|
|
307
311
|
let n = e.getState();
|
|
308
|
-
return l() === i && n.user?.id === a &&
|
|
312
|
+
return l() === i && n.user?.id === a && A() === r && n.currentOrganizationId === t;
|
|
309
313
|
};
|
|
310
314
|
if (!r) return [];
|
|
311
315
|
n.setLoadingWorkspaces(!0), V(null);
|
|
@@ -320,12 +324,12 @@ function P(d) {
|
|
|
320
324
|
if (!c()) return [];
|
|
321
325
|
if (e.data?.workspaces?.items && (e.data.workspaces.items = e.data.workspaces.items.filter((e) => e.organizationId === t)), e.errors?.length) {
|
|
322
326
|
let t = e.errors[0]?.message ?? "GraphQL error";
|
|
323
|
-
throw
|
|
327
|
+
throw k(e.errors) ? new O(t) : (console.error("Failed to fetch workspaces:", e.errors), Error(t));
|
|
324
328
|
}
|
|
325
329
|
let i = (e.data?.workspaces?.items ?? []).filter((e) => e.organizationId === t);
|
|
326
330
|
return n.setWorkspaces(i), i;
|
|
327
331
|
} catch (e) {
|
|
328
|
-
if (!c() || e instanceof
|
|
332
|
+
if (!c() || e instanceof O) return [];
|
|
329
333
|
let t = e instanceof Error ? e.message : "Failed to fetch workspaces";
|
|
330
334
|
return console.error("Failed to fetch workspaces:", e), V(t), [];
|
|
331
335
|
} finally {
|
|
@@ -341,7 +345,7 @@ function P(d) {
|
|
|
341
345
|
], s = m({
|
|
342
346
|
organizationId: t,
|
|
343
347
|
workspaceId: n ?? null
|
|
344
|
-
}), c =
|
|
348
|
+
}), c = A(), u = l(), d = a.user?.id;
|
|
345
349
|
if (!c) return {
|
|
346
350
|
success: !1,
|
|
347
351
|
error: "No valid access token available"
|
|
@@ -362,7 +366,7 @@ function P(d) {
|
|
|
362
366
|
});
|
|
363
367
|
if (p.errors?.length) {
|
|
364
368
|
let e = p.errors[0]?.message ?? "GraphQL error";
|
|
365
|
-
return
|
|
369
|
+
return k(p.errors) || console.error("Failed to issue workspace token:", p.errors), {
|
|
366
370
|
success: !1,
|
|
367
371
|
error: e
|
|
368
372
|
};
|
|
@@ -370,14 +374,14 @@ function P(d) {
|
|
|
370
374
|
let h = p.data?.issueWorkspaceToken;
|
|
371
375
|
if (h?.success && h.token) {
|
|
372
376
|
let i = e.getState();
|
|
373
|
-
if (l() !== u || e.getState().user?.id !== d ||
|
|
377
|
+
if (l() !== u || e.getState().user?.id !== d || A() !== c || o.some((e, t) => e !== [
|
|
374
378
|
i.currentOrganizationId,
|
|
375
379
|
i.currentWorkspaceId,
|
|
376
380
|
i.currentProjectId
|
|
377
381
|
][t]) || m({
|
|
378
382
|
organizationId: t,
|
|
379
383
|
workspaceId: n ?? null
|
|
380
|
-
}) !== s || !
|
|
384
|
+
}) !== s || !D(h.token, {
|
|
381
385
|
organizationId: t,
|
|
382
386
|
workspaceId: n,
|
|
383
387
|
projectId: r,
|
|
@@ -405,12 +409,12 @@ function P(d) {
|
|
|
405
409
|
};
|
|
406
410
|
}
|
|
407
411
|
}, [Y]), $ = a(async () => {
|
|
408
|
-
let t = e.getState(), n =
|
|
412
|
+
let t = e.getState(), n = A(), r = t.workspaceToken, i = t.currentWorkspaceId, a = l(), o = t.user?.id, s = t.currentOrganizationId, c = t.currentProjectId, u = m({
|
|
409
413
|
organizationId: s,
|
|
410
414
|
workspaceId: i
|
|
411
415
|
}), d = () => {
|
|
412
416
|
let t = e.getState();
|
|
413
|
-
return l() === a && t.user?.id === o &&
|
|
417
|
+
return l() === a && t.user?.id === o && A() === n && t.workspaceToken === r && t.currentOrganizationId === s && t.currentWorkspaceId === i && t.currentProjectId === c && m({
|
|
414
418
|
organizationId: s,
|
|
415
419
|
workspaceId: i
|
|
416
420
|
}) === u;
|
|
@@ -428,12 +432,12 @@ function P(d) {
|
|
|
428
432
|
if (!d()) return [];
|
|
429
433
|
if (e.errors?.length) {
|
|
430
434
|
let t = e.errors[0]?.message ?? "GraphQL error";
|
|
431
|
-
throw
|
|
435
|
+
throw k(e.errors) ? new O(t) : (console.error("Failed to fetch projects:", e.errors), Error(t));
|
|
432
436
|
}
|
|
433
437
|
let i = e.data?.projects?.items ?? [];
|
|
434
438
|
return t.setProjects(i), i;
|
|
435
439
|
} catch (e) {
|
|
436
|
-
if (e instanceof
|
|
440
|
+
if (e instanceof O) return [];
|
|
437
441
|
let t = e instanceof Error ? e.message : "Failed to fetch projects";
|
|
438
442
|
return console.error("Failed to fetch projects:", e), V(t), [];
|
|
439
443
|
} finally {
|
|
@@ -453,12 +457,12 @@ function P(d) {
|
|
|
453
457
|
success: !1,
|
|
454
458
|
error: r.error
|
|
455
459
|
};
|
|
456
|
-
let i = e.getState(), a = l(), o = i.user?.id, s =
|
|
460
|
+
let i = e.getState(), a = l(), o = i.user?.id, s = A(), c = i.workspaceToken, u = i.currentProjectId, d = m({
|
|
457
461
|
organizationId: t.organizationId,
|
|
458
462
|
workspaceId: t.id
|
|
459
463
|
}), f = () => {
|
|
460
464
|
let n = e.getState();
|
|
461
|
-
return l() === a && n.user?.id === o &&
|
|
465
|
+
return l() === a && n.user?.id === o && A() === s && n.workspaceToken === c && n.currentOrganizationId === t.organizationId && n.currentWorkspaceId === t.id && n.currentProjectId === u && m({
|
|
462
466
|
organizationId: t.organizationId,
|
|
463
467
|
workspaceId: t.id
|
|
464
468
|
}) === d;
|
|
@@ -478,7 +482,7 @@ function P(d) {
|
|
|
478
482
|
p({
|
|
479
483
|
organizationId: n.currentOrganizationId,
|
|
480
484
|
workspaceId: n.currentWorkspaceId
|
|
481
|
-
}), n.setCurrentProjectId(t.id), n.workspaceToken && !
|
|
485
|
+
}), n.setCurrentProjectId(t.id), n.workspaceToken && !D(n.workspaceToken, {
|
|
482
486
|
workspaceId: n.currentWorkspaceId,
|
|
483
487
|
organizationId: n.currentOrganizationId,
|
|
484
488
|
projectId: t.id
|
|
@@ -509,38 +513,38 @@ function P(d) {
|
|
|
509
513
|
}, r = n(() => $()), i = n(() => {
|
|
510
514
|
let t = e.getState().currentOrganizationId;
|
|
511
515
|
return t ? Z(t) : Promise.resolve();
|
|
512
|
-
}), a =
|
|
516
|
+
}), a = T.onAny((t) => {
|
|
513
517
|
let n = t.name;
|
|
514
|
-
if (
|
|
515
|
-
let n =
|
|
518
|
+
if (N.has(n)) {
|
|
519
|
+
let n = F(t.payload), { currentWorkspaceId: i } = e.getState();
|
|
516
520
|
if (n && i && n !== i) return;
|
|
517
521
|
r();
|
|
518
522
|
return;
|
|
519
523
|
}
|
|
520
|
-
|
|
524
|
+
P.has(n) && i();
|
|
521
525
|
});
|
|
522
526
|
return () => {
|
|
523
527
|
t = !0, a();
|
|
524
528
|
};
|
|
525
529
|
}, [
|
|
526
|
-
|
|
530
|
+
T,
|
|
527
531
|
$,
|
|
528
532
|
Z
|
|
529
533
|
]), {
|
|
530
|
-
organizations:
|
|
531
|
-
workspaces:
|
|
532
|
-
projects:
|
|
534
|
+
organizations: E,
|
|
535
|
+
workspaces: R,
|
|
536
|
+
projects: ae,
|
|
533
537
|
currentOrganizationId: I,
|
|
534
|
-
currentWorkspaceId:
|
|
535
|
-
currentProjectId:
|
|
536
|
-
currentOrganization:
|
|
537
|
-
currentWorkspace:
|
|
538
|
-
currentProject:
|
|
538
|
+
currentWorkspaceId: ne,
|
|
539
|
+
currentProjectId: z,
|
|
540
|
+
currentOrganization: E.find((e) => e.id === I),
|
|
541
|
+
currentWorkspace: R.find((e) => e.id === ne),
|
|
542
|
+
currentProject: ae.find((e) => e.id === z),
|
|
539
543
|
workspaceToken: re,
|
|
540
|
-
isLoadingOrganizations:
|
|
541
|
-
isLoadingWorkspaces:
|
|
542
|
-
isLoadingProjects:
|
|
543
|
-
isLoading:
|
|
544
|
+
isLoadingOrganizations: L,
|
|
545
|
+
isLoadingWorkspaces: ie,
|
|
546
|
+
isLoadingProjects: B,
|
|
547
|
+
isLoading: L || ie || B,
|
|
544
548
|
error: oe,
|
|
545
549
|
fetchOrganizations: X,
|
|
546
550
|
fetchWorkspacesForOrg: Z,
|
|
@@ -552,7 +556,7 @@ function P(d) {
|
|
|
552
556
|
selectWorkspaceWide: ge
|
|
553
557
|
};
|
|
554
558
|
}
|
|
555
|
-
function
|
|
559
|
+
function L(n) {
|
|
556
560
|
let r = e((e) => e.currentOrganizationId), i = e((e) => e.workspaces), a = e((e) => e.accessToken), s = e((e) => e.isLoadingWorkspaces), c = t(), l = n?.globalBaseUrl ?? c.globalBaseUrl, u = n?.disableWorkspaceSync ?? !1;
|
|
557
561
|
o(() => {
|
|
558
562
|
if (u || !r || !a || s) return;
|
|
@@ -582,13 +586,13 @@ function F(n) {
|
|
|
582
586
|
u
|
|
583
587
|
]);
|
|
584
588
|
}
|
|
585
|
-
function
|
|
586
|
-
let r = e((e) => e.currentWorkspaceId), i = e((e) => e.currentOrganizationId), a = e((e) => e.workspaces), u = e((e) => e.projects), d = e((e) => e.currentProjectId), h = e((e) => e.workspaceToken),
|
|
589
|
+
function R(n) {
|
|
590
|
+
let r = e((e) => e.currentWorkspaceId), i = e((e) => e.currentOrganizationId), a = e((e) => e.workspaces), u = e((e) => e.projects), d = e((e) => e.currentProjectId), h = e((e) => e.workspaceToken), v = e((e) => e.accessToken), { setContext: y } = _(), b = t(), x = n?.workspaceBaseUrl ?? b.workspaceBaseUrl, C = n?.disableProjectSync ?? !1, w = n?.skipProjectFetch ?? !1, E = s(!1), [O, k] = c(0), A = s(null), j = s(/* @__PURE__ */ new Map()), M = s(/* @__PURE__ */ new Map()), N = 3e4;
|
|
587
591
|
o(() => {
|
|
588
|
-
if (
|
|
589
|
-
let t =
|
|
590
|
-
if (t && t.count >= 8 && Date.now() - t.lastFailureAt <
|
|
591
|
-
t && Date.now() - t.lastFailureAt >=
|
|
592
|
+
if (C || !r || !v || E.current) return;
|
|
593
|
+
let t = M.current.get(r);
|
|
594
|
+
if (t && t.count >= 8 && Date.now() - t.lastFailureAt < N) return;
|
|
595
|
+
t && Date.now() - t.lastFailureAt >= N && M.current.delete(r);
|
|
592
596
|
let n = a.find((e) => e.id === r);
|
|
593
597
|
if (n && i && n.organizationId !== i) return;
|
|
594
598
|
let o = n?.organizationId ?? i;
|
|
@@ -598,45 +602,45 @@ function I(n) {
|
|
|
598
602
|
organizationId: o,
|
|
599
603
|
workspaceId: r
|
|
600
604
|
});
|
|
601
|
-
let c = l(),
|
|
605
|
+
let c = l(), _ = s.user?.id, b = m({
|
|
602
606
|
organizationId: o,
|
|
603
607
|
workspaceId: r
|
|
604
608
|
});
|
|
605
|
-
if (
|
|
609
|
+
if (b && s.currentProjectId) {
|
|
606
610
|
s.setCurrentProjectId(null), s.setWorkspaceToken(null);
|
|
607
611
|
return;
|
|
608
612
|
}
|
|
609
|
-
let
|
|
613
|
+
let O = !h || T(h) || !D(h, {
|
|
610
614
|
workspaceId: r,
|
|
611
615
|
organizationId: i,
|
|
612
616
|
projectId: d,
|
|
613
|
-
workspaceWide:
|
|
614
|
-
}),
|
|
617
|
+
workspaceWide: b
|
|
618
|
+
}), P = JSON.stringify([
|
|
615
619
|
c,
|
|
616
|
-
v,
|
|
617
620
|
_,
|
|
621
|
+
v,
|
|
618
622
|
o,
|
|
619
623
|
r
|
|
620
|
-
]),
|
|
621
|
-
if (
|
|
622
|
-
if (!s.currentProjectId && !
|
|
624
|
+
]), F = u.some((e) => e.workspaceId === r) || A.current?.scopeKey === P && A.current.projects === u;
|
|
625
|
+
if (F && !O) {
|
|
626
|
+
if (!s.currentProjectId && !b) {
|
|
623
627
|
let e = u.find((e) => e.workspaceId === r);
|
|
624
628
|
e && s.setCurrentProjectId(e.id);
|
|
625
629
|
}
|
|
626
630
|
return;
|
|
627
631
|
}
|
|
628
|
-
let
|
|
632
|
+
let I = () => {
|
|
629
633
|
let t = e.getState();
|
|
630
|
-
return t.accessToken ===
|
|
634
|
+
return t.accessToken === v && t.user?.id === _ && l() === c && t.currentOrganizationId === o && t.currentWorkspaceId === r && t.currentProjectId === d && m({
|
|
631
635
|
organizationId: o,
|
|
632
636
|
workspaceId: r
|
|
633
|
-
}) ===
|
|
634
|
-
},
|
|
635
|
-
|
|
637
|
+
}) === b;
|
|
638
|
+
}, L = async () => {
|
|
639
|
+
E.current = !0, s.setLoadingProjects(!0);
|
|
636
640
|
let t = !1;
|
|
637
641
|
try {
|
|
638
642
|
let n = h;
|
|
639
|
-
if (
|
|
643
|
+
if (O) {
|
|
640
644
|
let e = await f({
|
|
641
645
|
gateway: "workspace",
|
|
642
646
|
query: ee,
|
|
@@ -645,32 +649,35 @@ function I(n) {
|
|
|
645
649
|
workspaceId: r,
|
|
646
650
|
...d ? { projectId: d } : {}
|
|
647
651
|
} },
|
|
648
|
-
authToken:
|
|
649
|
-
baseUrl:
|
|
652
|
+
authToken: v,
|
|
653
|
+
baseUrl: x,
|
|
650
654
|
tenantId: a.find((e) => e.id === r)?.tenantId
|
|
651
655
|
});
|
|
652
656
|
if (e.data?.issueWorkspaceToken?.success && e.data.issueWorkspaceToken.token) {
|
|
653
|
-
if (n = e.data.issueWorkspaceToken.token, !
|
|
657
|
+
if (n = e.data.issueWorkspaceToken.token, !I() || !D(n, {
|
|
654
658
|
workspaceId: r,
|
|
655
659
|
organizationId: o,
|
|
656
660
|
projectId: d,
|
|
657
|
-
workspaceWide:
|
|
661
|
+
workspaceWide: b
|
|
658
662
|
})) return;
|
|
659
663
|
s.setWorkspaceToken(n);
|
|
660
|
-
} else
|
|
664
|
+
} else {
|
|
665
|
+
d && I() && S(e.data?.issueWorkspaceToken?.error?.code) && (s.setCurrentProjectId(null), s.setWorkspaceToken(null), y({ projectId: null }));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
661
668
|
}
|
|
662
|
-
if (n && !
|
|
669
|
+
if (n && !F && !w) {
|
|
663
670
|
let t = await f({
|
|
664
671
|
gateway: "workspace",
|
|
665
672
|
query: te,
|
|
666
|
-
authToken:
|
|
673
|
+
authToken: v,
|
|
667
674
|
workspaceToken: n,
|
|
668
|
-
baseUrl:
|
|
675
|
+
baseUrl: x
|
|
669
676
|
});
|
|
670
|
-
if (!
|
|
677
|
+
if (!I() || t.errors?.length) return;
|
|
671
678
|
let i = t.data?.projects?.items ?? [];
|
|
672
|
-
|
|
673
|
-
scopeKey:
|
|
679
|
+
A.current = i.length === 0 ? {
|
|
680
|
+
scopeKey: P,
|
|
674
681
|
projects: i
|
|
675
682
|
} : null, s.setProjects(i), d && !i.some((e) => e.id === d) && (s.setCurrentProjectId(null), s.setWorkspaceToken(null)), i.length > 0 && !e.getState().currentProjectId && !m({
|
|
676
683
|
organizationId: o,
|
|
@@ -679,53 +686,54 @@ function I(n) {
|
|
|
679
686
|
}
|
|
680
687
|
t = !0;
|
|
681
688
|
} catch {} finally {
|
|
682
|
-
s.setLoadingProjects(!1),
|
|
683
|
-
let n =
|
|
684
|
-
n ||
|
|
685
|
-
let i =
|
|
689
|
+
s.setLoadingProjects(!1), E.current = !1;
|
|
690
|
+
let n = I();
|
|
691
|
+
n || k((e) => e + 1);
|
|
692
|
+
let i = M.current;
|
|
686
693
|
if (n && t) {
|
|
687
694
|
i.delete(r);
|
|
688
|
-
let e =
|
|
689
|
-
e && (clearTimeout(e),
|
|
695
|
+
let e = j.current.get(r);
|
|
696
|
+
e && (clearTimeout(e), j.current.delete(r));
|
|
690
697
|
} else if (n) {
|
|
691
698
|
let t = (i.get(r)?.count ?? 0) + 1;
|
|
692
699
|
if (i.set(r, {
|
|
693
700
|
count: t,
|
|
694
701
|
lastFailureAt: Date.now()
|
|
695
702
|
}), t < 8) {
|
|
696
|
-
let n =
|
|
703
|
+
let n = j.current.get(r);
|
|
697
704
|
n && clearTimeout(n);
|
|
698
705
|
let i = Math.min(1e3 * 2 ** (t - 1), 8e3), a = setTimeout(() => {
|
|
699
|
-
|
|
706
|
+
j.current.delete(r);
|
|
700
707
|
let t = e.getState();
|
|
701
|
-
|
|
708
|
+
I() && t.currentWorkspaceId === r && !t.projects.some((e) => e.workspaceId === r) && !E.current && L();
|
|
702
709
|
}, i);
|
|
703
|
-
|
|
710
|
+
j.current.set(r, a);
|
|
704
711
|
}
|
|
705
712
|
}
|
|
706
713
|
}
|
|
707
714
|
};
|
|
708
|
-
|
|
715
|
+
L();
|
|
709
716
|
}, [
|
|
710
|
-
|
|
717
|
+
O,
|
|
711
718
|
r,
|
|
712
719
|
i,
|
|
713
720
|
a,
|
|
714
721
|
u,
|
|
715
722
|
d,
|
|
716
723
|
h,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
724
|
+
v,
|
|
725
|
+
x,
|
|
726
|
+
C,
|
|
727
|
+
w,
|
|
728
|
+
y
|
|
721
729
|
]), o(() => {
|
|
722
|
-
let e =
|
|
730
|
+
let e = j.current;
|
|
723
731
|
return () => {
|
|
724
732
|
e.forEach((e) => clearTimeout(e)), e.clear();
|
|
725
733
|
};
|
|
726
734
|
}, []);
|
|
727
735
|
}
|
|
728
736
|
//#endregion
|
|
729
|
-
export {
|
|
737
|
+
export { I as useContextSwitcherData, R as useProjectSyncEffect, L as useWorkspaceSyncEffect };
|
|
730
738
|
|
|
731
739
|
//# sourceMappingURL=useContextSwitcherData.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useContextSwitcherData.js","names":[],"sources":["../../../src/auth/hooks/useContextSwitcherData.ts"],"sourcesContent":["import {\n isWorkspaceWideSelection,\n selectWorkspaceWide as persistWorkspaceWideSelection,\n clearWorkspaceWideSelection,\n} from \"@burdenoff/fe-libs/shared/providers/shell/WorkspaceWideSelection\";\nimport { resolveActiveProfileId } from \"@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager\";\nimport { getUrlParam } from \"@burdenoff/fe-libs/shared/providers/shell/ActiveContextStorage\";\nimport { useCallback, useState, useEffect, useRef } from \"react\";\nimport {\n graphqlFetch,\n getStoredAuthToken,\n} from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useActiveContext } from \"@burdenoff/fe-libs/shared/providers/shell/ActiveContextProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { Organization, Workspace, Project } from \"../types\";\nimport { resolveGatewayBaseUrls } from \"../utils/gatewayBaseUrls\";\nimport {\n decideOrganizationDeepLink,\n decideWorkspaceDeepLink,\n isCompleteListing,\n} from \"./contextDeepLink\";\n\n// =============================================================================\n// Hook Options\n// =============================================================================\n\nexport interface UseContextSwitcherDataOptions {\n /**\n * Optional Global API Gateway base URL override.\n * If not provided, uses GATEWAYS config from fe-libs.\n */\n globalBaseUrl?: string;\n /**\n * Optional Workspaces API Gateway base URL override.\n * If not provided, uses GATEWAYS config from fe-libs.\n */\n workspaceBaseUrl?: string;\n /** Disable initial organization/workspace bootstrap fetches */\n disableBootstrap?: boolean;\n /** Disable the workspace sync effect */\n disableWorkspaceSync?: boolean;\n /** Disable the project sync effect */\n disableProjectSync?: boolean;\n /** Only hydrate the workspace token; do not eagerly fetch projects */\n skipProjectFetch?: boolean;\n}\n\n// =============================================================================\n// GraphQL Response Types\n// =============================================================================\n\ninterface MyOrganizationsResponse {\n myOrganizations: {\n total: number;\n items: Organization[];\n };\n}\n\ninterface WorkspacesByOrgResponse {\n workspaces: {\n total: number;\n items: Workspace[];\n };\n}\n\ntype GraphQLFetchResult<T> = Awaited<ReturnType<typeof graphqlFetch<T>>>;\n\ninterface IssueWorkspaceTokenResult {\n success: boolean;\n token: string | null;\n expiresAt: string | null;\n organizationId: string | null;\n workspaceId: string | null;\n projectId: string | null;\n scopes: string[] | null;\n error: {\n code: string;\n message: string;\n } | null;\n}\n\ninterface ProjectsResponse {\n projects: {\n total: number;\n items: Project[];\n };\n}\n\n// =============================================================================\n// GraphQL Query/Mutation Strings\n// =============================================================================\n\nconst MY_ORGANIZATIONS_QUERY = `\n query MyOrganizations {\n myOrganizations {\n total\n items {\n id\n name\n slug\n tenantId\n description\n logo\n website\n status\n type\n createdAt\n updatedAt\n }\n }\n }\n`;\n\n// Both queries below historically used `workspaces(filter: $filter)` with\n// a `WorkspaceFilterInput` to fetch workspaces for a specific organization.\n// That Query field was removed/renamed in the global subgraph; calling it\n// now throws GRAPHQL_VALIDATION_FAILED (\"Cannot query field 'workspaces'\n// on type 'Query'\") which crashes the context switcher and cascades into a\n// \"Verifying workspace access...\" stuck-spinner on every Platform module\n// route (/billing, /support, /notifications, /devportal).\n//\n// The supported field is `myWorkspaces(limit, offset)` on the global\n// subgraph — self-scoped, returns the caller's workspaces across all\n// orgs. It does NOT accept a filter, so we fetch up to 100 of \"my\"\n// workspaces and let the existing downstream auto-pick logic filter by\n// organizationId client-side. The recovery path at line ~624 already\n// uses this exact aliased pattern; we're just promoting it to the\n// primary path.\nconst WORKSPACES_BY_ORG_QUERY = `\n query WorkspacesByOrg {\n workspaces: myWorkspaces(limit: 100) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n`;\n\n// A further page of `myWorkspaces`, read only when a deep-linked workspace is\n// not on the first page of a caller with more than 100 workspaces.\nconst MY_WORKSPACES_PAGE_QUERY = `\n query MyWorkspacesPage($limit: Int, $offset: Int) {\n workspaces: myWorkspaces(limit: $limit, offset: $offset) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n`;\n\nconst WORKSPACES_PAGE_SIZE = 100;\n// Bounds the read for a deep link: 20 pages = 2,000 workspaces.\nconst WORKSPACES_MAX_PAGES = 20;\n\nconst ISSUE_WORKSPACE_TOKEN_MUTATION = `\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n expiresAt\n organizationId\n workspaceId\n projectId\n scopes\n error {\n code\n message\n }\n }\n }\n`;\n\n// The workspace gateway exposes `myProjects` (public RBAC, caller-scoped to\n// the active workspace via x-workspace-id) and the privileged\n// `projects(filter)` (list:project). The header has no `list:project`\n// permission seeded for the typical operator, so the privileged form\n// returns FORBIDDEN even though the caller is a member of the workspace.\n// `myProjects` is the right field for \"give me the projects I can see in\n// this workspace\" — workspace scope is already established by the\n// workspace token + x-workspace-id header that we pass with the request.\nconst PROJECTS_QUERY = `\n query MyProjectsForContext {\n projects: myProjects {\n total\n items {\n id\n name\n key\n workspaceId\n status\n archived\n createdAt\n updatedAt\n }\n }\n }\n`;\n\n// =============================================================================\n// Auth Helpers\n// =============================================================================\n\n/**\n * Check whether a JWT is expired (or within bufferMs of expiry).\n * Returns true if the token is expired, close to expiry, or malformed.\n */\nfunction isTokenExpired(token: string, bufferMs = 30_000): boolean {\n try {\n const payload = JSON.parse(atob(token.split(\".\")[1]));\n if (!payload.exp) return false;\n return payload.exp * 1000 < Date.now() + bufferMs;\n } catch {\n return true;\n }\n}\n\nfunction getWorkspaceTokenPayload(token: string): {\n workspaceId?: string;\n organizationId?: string;\n projectId?: string;\n exp?: number;\n} | null {\n try {\n const payload = JSON.parse(atob(token.split(\".\")[1]));\n return {\n workspaceId:\n typeof payload.workspaceId === \"string\"\n ? payload.workspaceId\n : undefined,\n organizationId:\n typeof payload.organizationId === \"string\"\n ? payload.organizationId\n : undefined,\n projectId:\n typeof payload.projectId === \"string\" ? payload.projectId : undefined,\n exp: typeof payload.exp === \"number\" ? payload.exp : undefined,\n };\n } catch {\n return null;\n }\n}\n\nfunction isWorkspaceTokenValidForContext(\n token: string | null | undefined,\n expected: {\n workspaceId?: string | null;\n organizationId?: string | null;\n projectId?: string | null;\n workspaceWide?: boolean;\n },\n): token is string {\n if (!token || isTokenExpired(token)) {\n return false;\n }\n\n const payload = getWorkspaceTokenPayload(token);\n if (!payload) {\n return false;\n }\n\n if (expected.workspaceId && payload.workspaceId !== expected.workspaceId) {\n return false;\n }\n\n if (\n expected.organizationId &&\n payload.organizationId !== expected.organizationId\n ) {\n return false;\n }\n\n if (expected.workspaceWide && payload.projectId) return false;\n\n if (expected.projectId && payload.projectId !== expected.projectId) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Custom error class for auth-related failures (expired token, unauthorized, etc.).\n * Used to distinguish auth errors in outer catch blocks so they can be\n * suppressed from console.error — they are expected during token refresh cycles\n * and do not indicate application bugs.\n */\nclass AuthError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AuthError\";\n }\n}\n\n/**\n * Detect whether a set of GraphQL errors are authentication-related.\n */\nfunction isAuthGraphQLError(\n errors: Array<{ message?: string; extensions?: { code?: string } }>,\n): boolean {\n return errors.some(\n (e) =>\n e.extensions?.code === \"UNAUTHENTICATED\" ||\n e.message?.toLowerCase().includes(\"unauthorized\") ||\n e.message?.toLowerCase().includes(\"not authenticated\"),\n );\n}\n\n/**\n * Resolve the best available access token:\n * 1. Try the zustand store token\n * 2. If expired, fall back to profile storage (may have been refreshed)\n * 3. Return null if no valid token is available\n */\nfunction resolveAccessToken(): string | null {\n const storeToken = useAuthStore.getState().accessToken;\n\n // If store token is valid (or has no exp claim), use it\n if (storeToken && !isTokenExpired(storeToken)) {\n return storeToken;\n }\n\n // Store token is expired or missing — try profile storage (may be fresher)\n const profileToken = getStoredAuthToken();\n if (profileToken && !isTokenExpired(profileToken)) {\n return profileToken;\n }\n\n // Both expired or missing — if store token exists at all, return it as a last\n // resort (the gateway will reject it and the useEffect will re-trigger after\n // token refresh). Returning null would silently swallow the first load.\n return storeToken;\n}\n\n/**\n * The rest of the caller's workspaces after `first` (a first page of\n * `myWorkspaces`), bounded by WORKSPACES_MAX_PAGES. A failed page ends the read\n * with what was gathered, marked complete only if every page was read.\n */\nasync function readRemainingWorkspacePages(\n first: WorkspacesByOrgResponse[\"workspaces\"],\n token: string,\n globalBaseUrl: string,\n): Promise<WorkspacesByOrgResponse[\"workspaces\"]> {\n const items = [...first.items];\n const total = first.total;\n for (\n let page = 1;\n page < WORKSPACES_MAX_PAGES && items.length < total;\n page += 1\n ) {\n const next = await graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: MY_WORKSPACES_PAGE_QUERY,\n variables: { limit: WORKSPACES_PAGE_SIZE, offset: items.length },\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n const pageItems = next.errors?.length\n ? []\n : (next.data?.workspaces?.items ?? []);\n if (pageItems.length === 0) {\n // Unreadable or short: report what we have as a partial listing so no\n // denial is concluded from it.\n return { total: Math.max(total, items.length + 1), items };\n }\n items.push(...pageItems);\n }\n return { total, items };\n}\n\nfunction buildFallbackOrganizations(workspaces: Workspace[]): Organization[] {\n const uniqueOrgIds = Array.from(\n new Set(\n workspaces.flatMap((workspace) =>\n workspace.organizationId ? [workspace.organizationId] : [],\n ),\n ),\n );\n\n return uniqueOrgIds.map((organizationId, index) => ({\n id: organizationId,\n name:\n workspaces.find(\n (workspace) => workspace.organizationId === organizationId,\n )?.name || `Organization ${index + 1}`,\n slug: organizationId,\n tenantId:\n workspaces.find(\n (workspace) => workspace.organizationId === organizationId,\n )?.tenantId || \"\",\n }));\n}\n\n// =============================================================================\n// Hook Implementation\n// =============================================================================\n\n/**\n * Hook for managing the context switcher data (organizations, workspaces, projects)\n *\n * Data flow:\n * 1. fetchOrganizations() - Get all orgs user has access to (Global Gateway)\n * 2. selectOrganization(org) - Set current org & fetch workspaces for that org\n * 3. selectWorkspace(ws) - Issue workspace token & fetch projects\n * 4. selectProject(proj) - Set current project\n *\n * All GraphQL calls go through graphqlFetch from fe-libs which automatically\n * resolves gateway URLs and injects auth + locale headers.\n *\n * @param options - Optional configuration (gateway base URL overrides)\n */\n// Domain events emitted by microfe-workspaces that should refresh the switcher's\n// cached lists. Module-scoped so they aren't reallocated on every effect run.\nconst PROJECT_REFRESH_EVENTS = new Set<string>([\n \"project.created\",\n \"project.updated\",\n \"project.deleted\",\n \"project.archived\",\n \"project.unarchived\",\n]);\nconst WORKSPACE_REFRESH_EVENTS = new Set<string>([\n \"workspace.created\",\n \"workspace.updated\",\n \"workspace.deleted\",\n \"workspace.archived\",\n \"workspace.reactivated\",\n]);\n\n// Safely read a string `workspaceId` off a cross-MFE event payload of unknown\n// shape, rather than trusting a bare cast (the payload is produced by another\n// MFE and could drift). Returns undefined when absent or not a string.\nfunction readEventWorkspaceId(payload: unknown): string | undefined {\n if (payload && typeof payload === \"object\" && \"workspaceId\" in payload) {\n const value = (payload as { workspaceId?: unknown }).workspaceId;\n return typeof value === \"string\" ? value : undefined;\n }\n return undefined;\n}\n\nexport function useContextSwitcherData(\n options?: UseContextSwitcherDataOptions,\n) {\n const {\n organizationId: activeOrganizationId,\n workspaceId: activeWorkspaceId,\n projectId: activeProjectId,\n tenantId: activeTenantId,\n setContext: setActiveContext,\n } = useActiveContext();\n\n const eventBus = useEventBus();\n\n // Use selectors for reactive state\n const organizations = useAuthStore((state) => state.organizations);\n const currentOrganizationId = useAuthStore(\n (state) => state.currentOrganizationId,\n );\n const isLoadingOrganizations = useAuthStore(\n (state) => state.isLoadingOrganizations,\n );\n\n const workspaces = useAuthStore((state) => state.workspaces);\n const currentWorkspaceId = useAuthStore((state) => state.currentWorkspaceId);\n const workspaceToken = useAuthStore((state) => state.workspaceToken);\n const isLoadingWorkspaces = useAuthStore(\n (state) => state.isLoadingWorkspaces,\n );\n\n const projects = useAuthStore((state) => state.projects);\n const currentProjectId = useAuthStore((state) => state.currentProjectId);\n const isLoadingProjects = useAuthStore((state) => state.isLoadingProjects);\n\n // Restored workspaces can already be present in the profile store, so the\n // first-workspace bootstrap branch is not sufficient to hydrate tenant.\n // Resolve it from the selected accessible rows, never from a placeholder,\n // URL tenant value or a workspace belonging to another organization.\n useEffect(() => {\n const workspace = workspaces.find(\n (row) =>\n row.id === activeWorkspaceId &&\n row.organizationId === activeOrganizationId,\n );\n const organization = organizations.find(\n (row) => row.id === activeOrganizationId,\n );\n if (\n !workspace ||\n !organization ||\n !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(\n workspace.tenantId,\n ) ||\n workspace.tenantId !== organization.tenantId ||\n activeTenantId === workspace.tenantId\n ) {\n return;\n }\n // Updating only tenant preserves the selected project, including an\n // explicit workspace-wide (null-project) selection.\n setActiveContext({ tenantId: workspace.tenantId });\n }, [\n activeOrganizationId,\n activeWorkspaceId,\n activeTenantId,\n organizations,\n workspaces,\n setActiveContext,\n ]);\n\n // Local error state\n const [error, setError] = useState<string | null>(null);\n\n // Bootstrap-retry signal: incremented on every TRANSIENT bootstrap failure\n // (orgs fetch, stale-context recovery, or the inner workspace bootstrap).\n // A monotonically increasing nonce — rather than the `error` string — keys\n // the retry effect, because two consecutive failures with the identical\n // message would not re-trigger a string-keyed effect and the retry chain\n // would silently stop after one attempt.\n const [bootstrapFailureNonce, setBootstrapFailureNonce] = useState(0);\n const bootstrapRetryAttemptRef = useRef(0);\n\n // Deduplication refs to prevent duplicate GraphQL calls\n const fetchOrganizationsPromiseRef = useRef<Promise<Organization[]> | null>(\n null,\n );\n const fetchWorkspacesPromisesRef = useRef<Map<string, Promise<Workspace[]>>>(\n new Map(),\n );\n\n // Stable refs for latest context values — avoids stale closures in callbacks\n // without adding them to useCallback dep arrays (which would recreate callbacks\n // on every context change and trigger spurious fetch re-runs).\n const activeWorkspaceIdRef = useRef(activeWorkspaceId);\n const activeOrganizationIdRef = useRef(activeOrganizationId);\n useEffect(() => {\n activeWorkspaceIdRef.current = activeWorkspaceId;\n activeOrganizationIdRef.current = activeOrganizationId;\n });\n\n const resolvedBaseUrls = resolveGatewayBaseUrls();\n const globalBaseUrl =\n options?.globalBaseUrl ?? resolvedBaseUrls.globalBaseUrl;\n const workspaceBaseUrl =\n options?.workspaceBaseUrl ?? resolvedBaseUrls.workspaceBaseUrl;\n\n // Refs to break feedback loops with the Header's \"store → URL\" sync effect.\n // Without these the pair can ping-pong: this effect writes (URL → store),\n // Header writes (store → URL), Zustand re-fires our subscribers, and React\n // hits \"Maximum update depth exceeded\" — especially when a downstream call\n // (workspace token issuance) returns 504 and leaves the two halves in\n // disagreement. The refs record the last value we pushed for each axis;\n // identical pushes are skipped so the loop has a hard floor.\n const lastAppliedOrgIdRef = useRef<string | null>(null);\n const lastAppliedWorkspaceIdRef = useRef<string | null>(null);\n const lastAppliedProjectIdRef = useRef<string | null>(null);\n const lastClearedTokenContextRef = useRef<string | null>(null);\n\n useEffect(() => {\n const store = useAuthStore.getState();\n\n if (\n activeOrganizationId &&\n activeOrganizationId !== store.currentOrganizationId &&\n activeOrganizationId !== lastAppliedOrgIdRef.current\n ) {\n lastAppliedOrgIdRef.current = activeOrganizationId;\n store.setCurrentOrganizationId(activeOrganizationId);\n if (\n store.workspaces.length > 0 &&\n !store.workspaces.some(\n (workspace) => workspace.organizationId === activeOrganizationId,\n )\n ) {\n store.setWorkspaces([]);\n }\n }\n\n if (\n activeWorkspaceId &&\n activeWorkspaceId !== store.currentWorkspaceId &&\n activeWorkspaceId !== lastAppliedWorkspaceIdRef.current\n ) {\n lastAppliedWorkspaceIdRef.current = activeWorkspaceId;\n store.setCurrentWorkspaceId(activeWorkspaceId);\n if (\n store.projects.length > 0 &&\n !store.projects.some(\n (project) => project.workspaceId === activeWorkspaceId,\n )\n ) {\n store.setProjects([]);\n store.setCurrentProjectId(null);\n }\n }\n\n if (\n isWorkspaceWideSelection({\n organizationId: activeOrganizationId,\n workspaceId: activeWorkspaceId,\n }) &&\n !activeProjectId &&\n store.currentProjectId\n ) {\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n }\n\n if (\n activeProjectId &&\n activeProjectId !== store.currentProjectId &&\n activeProjectId !== lastAppliedProjectIdRef.current\n ) {\n lastAppliedProjectIdRef.current = activeProjectId;\n store.setCurrentProjectId(activeProjectId);\n }\n\n if (\n store.workspaceToken &&\n !isWorkspaceTokenValidForContext(store.workspaceToken, {\n workspaceId: activeWorkspaceId,\n organizationId: activeOrganizationId,\n projectId: activeProjectId,\n workspaceWide:\n !activeProjectId &&\n isWorkspaceWideSelection({\n organizationId: activeOrganizationId,\n workspaceId: activeWorkspaceId,\n }),\n })\n ) {\n // Clear-token-once-per-context guard: when the upstream gateway is\n // flaky (504s during workspace token issuance) the surrounding state\n // machine can oscillate between \"got token but invalid\" and \"clearing\n // token\". The downstream re-issue path then loops. Only clear once per\n // (org, ws, proj) tuple — a successful re-issue resets the ref via\n // setWorkspaceToken landing a new token for the same context.\n const ctxKey = `${activeOrganizationId ?? \"\"}|${activeWorkspaceId ?? \"\"}|${activeProjectId ?? \"\"}`;\n if (lastClearedTokenContextRef.current !== ctxKey) {\n lastClearedTokenContextRef.current = ctxKey;\n store.setWorkspaceToken(null);\n }\n }\n }, [activeOrganizationId, activeWorkspaceId, activeProjectId]);\n\n // ===========================================================================\n // Fetch Organizations (Global Gateway)\n // ===========================================================================\n\n const fetchOrganizations = useCallback(async (): Promise<Organization[]> => {\n // Deduplicate: if a fetch is already in progress, return the same promise\n if (fetchOrganizationsPromiseRef.current) {\n return fetchOrganizationsPromiseRef.current;\n }\n\n const store = useAuthStore.getState();\n const token = resolveAccessToken();\n\n if (!token) {\n // Not an error — user may not be authenticated yet (e.g., page load before login).\n // The useEffect in Header.tsx will re-trigger when accessToken becomes available.\n return [];\n }\n\n // NOTE: an EXPIRED token is deliberately NOT a bail-out. fe-libs\n // graphqlFetch now transparently refreshes on UNAUTHENTICATED and retries\n // the request once, and a definitive refresh failure triggers the shell's\n // forced logout (SESSION_EXPIRED_EVENT). The previous `isTokenExpired →\n // return []` short-circuit was the root cause of the persistently empty\n // context switcher: the expired token survives reloads in profile storage,\n // so every bootstrap silently produced [] — indistinguishable from \"you\n // have no organizations\" — until the user manually logged out.\n\n store.setLoadingOrganizations(true);\n setError(null);\n\n // Store the promise for deduplication before starting\n fetchOrganizationsPromiseRef.current = (async () => {\n try {\n const initialTargetOrgId =\n activeOrganizationIdRef.current ?? store.currentOrganizationId;\n // Deep links (contextDeepLink.ts): only ids that are in the URL right\n // now count as a link; restored ids fall back quietly as before.\n const requestedWorkspaceId = activeWorkspaceIdRef.current;\n const workspaceFromUrl =\n !!requestedWorkspaceId &&\n getUrlParam(\"workspace\") === requestedWorkspaceId;\n const orgFromUrl =\n !!initialTargetOrgId && getUrlParam(\"org\") === initialTargetOrgId;\n let result: GraphQLFetchResult<\n MyOrganizationsResponse & Partial<WorkspacesByOrgResponse>\n > =\n // The workspace list is read with the organizations whenever a\n // workspace is requested, because the workspace names its own\n // organization and may not be in the saved one.\n initialTargetOrgId || requestedWorkspaceId\n ? await (async () => {\n const [organizationsResult, workspacesResult] =\n await Promise.all([\n graphqlFetch<MyOrganizationsResponse>({\n gateway: \"global\",\n query: MY_ORGANIZATIONS_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n }),\n graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n }),\n ]);\n\n const data = organizationsResult.data?.myOrganizations\n ? {\n myOrganizations: organizationsResult.data.myOrganizations,\n ...(workspacesResult.data?.workspaces\n ? { workspaces: workspacesResult.data.workspaces }\n : {}),\n }\n : null;\n\n return {\n data,\n errors: [\n ...(organizationsResult.errors ?? []),\n ...(workspacesResult.errors ?? []),\n ],\n };\n })()\n : await graphqlFetch<MyOrganizationsResponse>({\n gateway: \"global\",\n query: MY_ORGANIZATIONS_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n\n // Self-heal stale-URL case: if the workspace side of the bootstrap\n // fails because the URL points at an inaccessible org/workspace, retry\n // with the organization-only query so the operator sees their actual\n // orgs and the URL params get corrected on next render.\n if (\n initialTargetOrgId &&\n result.errors?.length &&\n result.errors.some(\n (e) =>\n e?.extensions?.code === \"FORBIDDEN\" ||\n /not a member of this organization/i.test(e?.message ?? \"\") ||\n // Public gateway often drops the underlying FORBIDDEN error\n // and replaces it with the schema-level non-null violation\n // (\"Cannot return null for non-nullable field Query.myOrganizations\").\n // That happens when ANY field in the query throws — workspaces\n // here — and data:null cascades up. Treat that pattern as a\n // signal to retry without the workspaces filter.\n /Cannot return null for non-nullable field Query\\.myOrganizations/i.test(\n e?.message ?? \"\",\n ),\n )\n ) {\n // Fetch myOrganizations + ALL workspaces (no filter) so the\n // downstream auto-pick logic still has a workspace list to seed\n // context from. If we only refetched myOrganizations, the page\n // would render but every workspace-scoped query would fail with\n // \"Select a workspace…\".\n result = await graphqlFetch<\n MyOrganizationsResponse & Partial<WorkspacesByOrgResponse>\n >({\n gateway: \"global\",\n query: `\n query ContextSwitcherRecover {\n myOrganizations {\n total\n items { id name slug tenantId description logo website status type createdAt updatedAt }\n }\n workspaces: myWorkspaces {\n total\n items { id name slug tenantId organizationId type status region createdAt updatedAt }\n }\n }\n `,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n }\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (isAuthGraphQLError(result.errors)) {\n // Auth errors are expected during token refresh cycles — throw AuthError\n // so the outer catch can suppress console.error.\n throw new AuthError(errorMessage);\n }\n if (store.workspaces.length === 0) {\n console.error(\"Failed to fetch organizations:\", result.errors);\n }\n throw new Error(errorMessage);\n }\n\n let orgs: Organization[] = result.data?.myOrganizations?.items ?? [];\n\n // Stale-context recovery: bootstrap query was filtered by\n // initialTargetOrgId, so when the URL/persisted org doesn't belong to\n // this user the response is empty. Re-fetch the unfiltered list of\n // *all* orgs the user can access so the guard below can pick a real\n // default. Skip the recovery when no target was set (already\n // unfiltered) or when workspaces are already in the store (the\n // existing fallback path covers that case).\n if (\n initialTargetOrgId &&\n orgs.length === 0 &&\n store.workspaces.length === 0\n ) {\n const recovery = await graphqlFetch<MyOrganizationsResponse>({\n gateway: \"global\",\n query: MY_ORGANIZATIONS_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n if (!recovery.errors?.length) {\n orgs = recovery.data?.myOrganizations?.items ?? [];\n // discard the filtered workspaces payload — it was for the\n // ghost org and would only mislead the workspace selector below.\n result = {\n ...recovery,\n data: {\n myOrganizations: recovery.data?.myOrganizations ?? {\n items: [],\n total: 0,\n },\n },\n };\n } else {\n // The filtered bootstrap returned nothing AND the unfiltered\n // recovery failed — we genuinely do not know the user's org list.\n // Treat it as the failure it is (retryable via the bootstrap-retry\n // effect) instead of falling through to setOrganizations([]),\n // which presented a transient backend error as \"you have no\n // organizations\" until re-login.\n console.warn(\n \"[ContextSwitcher] Stale-context recovery failed:\",\n recovery.errors,\n );\n const recoveryMessage =\n recovery.errors?.[0]?.message ?? \"Failed to fetch organizations\";\n if (isAuthGraphQLError(recovery.errors ?? [])) {\n throw new AuthError(recoveryMessage);\n }\n throw new Error(recoveryMessage);\n }\n }\n\n const fallbackOrganizations =\n orgs.length === 0 && store.workspaces.length > 0\n ? buildFallbackOrganizations(store.workspaces)\n : [];\n const effectiveOrganizations =\n fallbackOrganizations.length > 0 ? fallbackOrganizations : orgs;\n\n store.setOrganizations(effectiveOrganizations);\n\n // Determine which organization to use for workspace fetching\n let targetOrgId = initialTargetOrgId;\n\n // The caller's workspaces across organizations, when this bootstrap\n // read them. A deep-linked workspace beyond the first page is looked\n // up in the remaining pages before anything is decided about it.\n let listedWorkspaces: WorkspacesByOrgResponse[\"workspaces\"] | null =\n result.data?.workspaces ?? null;\n if (\n workspaceFromUrl &&\n listedWorkspaces &&\n !listedWorkspaces.items.some((w) => w.id === requestedWorkspaceId) &&\n !isCompleteListing(listedWorkspaces)\n ) {\n listedWorkspaces = await readRemainingWorkspacePages(\n listedWorkspaces,\n token,\n globalBaseUrl,\n );\n }\n\n // A deep-linked workspace in another accessible organization switches\n // the organization to it, instead of being replaced by the saved\n // organization's first workspace (vc-fg-parity round 3). One the\n // caller cannot open is recorded so the shell can say so.\n const workspaceLink = decideWorkspaceDeepLink({\n requestedWorkspaceId,\n fromUrl: workspaceFromUrl,\n targetOrgId,\n organizations: orgs,\n workspaces: listedWorkspaces?.items ?? null,\n workspacesComplete: isCompleteListing(listedWorkspaces),\n });\n if (workspaceLink.kind === \"switch\") {\n const linked = workspaceLink.workspace;\n targetOrgId = linked.organizationId;\n setActiveContext({\n organizationId: linked.organizationId,\n workspaceId: linked.id,\n tenantId: linked.tenantId || null,\n projectId: getUrlParam(\"project\"),\n });\n store.setWorkspaces([]);\n store.setProjects([]);\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n store.setCurrentWorkspaceId(linked.id);\n } else {\n const denial =\n workspaceLink.kind === \"denied\"\n ? workspaceLink.denial\n : decideOrganizationDeepLink({\n requestedOrgId: initialTargetOrgId,\n fromUrl: orgFromUrl,\n organizations: orgs,\n });\n if (denial) store.setContextLinkDenial(denial);\n }\n\n // Stale-context guard: if the persisted/URL org isn't in the user's\n // accessible orgs, fall back to the first available one and clear the\n // stale workspace/project so they get re-derived. Without this, URL\n // params like ?org=<ghost-id> from a prior session leak into the new\n // user's request headers and cause \"Authentication required\" /\n // \"not a member\" errors at every subgraph.\n //\n // Check against `orgs` (freshly returned from the API in this same\n // closure) rather than `effectiveOrganizations`, which can fall back\n // to ids derived from `store.workspaces` — that fallback path could\n // contain stale ghost orgIds and mask the guard.\n const targetOrgIsAccessible =\n targetOrgId && orgs.some((org) => org.id === targetOrgId);\n if (targetOrgId && !targetOrgIsAccessible && orgs.length > 0) {\n targetOrgId = orgs[0].id;\n setActiveContext({\n organizationId: targetOrgId,\n workspaceId: null,\n tenantId: null,\n projectId: null,\n });\n store.setWorkspaces([]);\n store.setProjects([]);\n store.setCurrentWorkspaceId(null);\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n }\n\n if (targetOrgId && targetOrgId !== store.currentOrganizationId) {\n store.setCurrentOrganizationId(targetOrgId);\n }\n\n // Auto-select first organization if none is selected\n if (effectiveOrganizations.length > 0 && !targetOrgId) {\n store.setCurrentOrganizationId(effectiveOrganizations[0].id);\n targetOrgId = effectiveOrganizations[0].id;\n }\n\n // Fetch workspaces for the selected/auto-selected org if:\n // 1. We have a target org (either auto-selected or persisted from localStorage)\n // 2. AND workspaces array is empty (not persisted, or cleared)\n // This handles both initial login AND page refresh scenarios.\n // Read from useAuthStore.getState() rather than the captured `store`\n // snapshot so we see the post-guard cleared value when the org guard\n // above fired (the local `store.workspaces` ref is stale after\n // setWorkspaces([])).\n if (targetOrgId && useAuthStore.getState().workspaces.length === 0) {\n store.setLoadingWorkspaces(true);\n try {\n // myWorkspaces returns ALL workspaces the user is a member of\n // across every org. Filter to the target org client-side here.\n // The list read above covers every organization, so it serves the\n // target organization even when the guard or a deep link changed it.\n const bootstrappedWorkspaces = listedWorkspaces\n ? listedWorkspaces.items.filter(\n (w) => w.organizationId === targetOrgId,\n )\n : undefined;\n const ws = bootstrappedWorkspaces\n ? (bootstrappedWorkspaces as Workspace[])\n : await graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n }).then((wsResult) =>\n wsResult.errors?.length\n ? null\n : (wsResult.data?.workspaces?.items ?? []).filter(\n (w) => w.organizationId === targetOrgId,\n ),\n );\n\n if (!ws) {\n // Workspace bootstrap failed (errored result) while orgs\n // succeeded — retryable, otherwise the org renders with no\n // workspaces until manual interaction.\n setBootstrapFailureNonce((n) => n + 1);\n }\n if (ws) {\n store.setWorkspaces(ws);\n const persistedWorkspace = activeWorkspaceIdRef.current\n ? ws.find(\n (workspace) =>\n workspace.id === activeWorkspaceIdRef.current,\n )\n : null;\n\n if (persistedWorkspace) {\n store.setCurrentWorkspaceId(persistedWorkspace.id);\n } else if (ws.length > 0) {\n const nextActiveContext = {\n organizationId: targetOrgId,\n workspaceId: ws[0].id,\n tenantId: ws[0].tenantId,\n projectId: null,\n };\n\n if (activeWorkspaceIdRef.current) {\n // Stale-context guard: URL/persisted workspace ID is not in\n // the user's accessible workspaces for the active org. Fall\n // back to the first workspace and clear the stale URL param\n // so subsequent requests don't carry the ghost ID.\n store.setCurrentWorkspaceId(ws[0].id);\n store.setWorkspaceToken(null);\n setActiveContext(nextActiveContext);\n } else if (!store.currentWorkspaceId) {\n // Auto-select first workspace only if none is currently selected\n // (preserves selection from completeAuthFlow or persistence if applicable)\n store.setCurrentWorkspaceId(ws[0].id);\n setActiveContext(nextActiveContext);\n }\n }\n }\n } catch {\n // Non-critical for THIS call (user can still select a workspace\n // manually) but worth a bounded background retry.\n setBootstrapFailureNonce((n) => n + 1);\n } finally {\n store.setLoadingWorkspaces(false);\n }\n }\n\n // Full bootstrap succeeded — re-arm the retry budget for the next\n // genuine failure.\n bootstrapRetryAttemptRef.current = 0;\n return effectiveOrganizations;\n } catch (err) {\n // Auth errors are expected during token refresh / page load cycles.\n // Suppress from console.error — the useEffect will re-trigger when the\n // auth provider refreshes the token. (A DEFINITIVELY dead session\n // never reaches here as an AuthError loop: graphqlFetch dispatches\n // SESSION_EXPIRED_EVENT and the shell force-logs-out.)\n if (err instanceof AuthError) {\n return [];\n }\n const message =\n err instanceof Error ? err.message : \"Failed to fetch organizations\";\n if (store.workspaces.length === 0) {\n console.error(\"Failed to fetch organizations:\", err);\n }\n setError(message);\n setBootstrapFailureNonce((n) => n + 1);\n\n if (store.workspaces.length > 0) {\n const fallbackOrganizations = buildFallbackOrganizations(\n store.workspaces,\n );\n if (fallbackOrganizations.length > 0) {\n store.setOrganizations(fallbackOrganizations);\n if (!store.currentOrganizationId) {\n store.setCurrentOrganizationId(fallbackOrganizations[0].id);\n }\n return fallbackOrganizations;\n }\n }\n\n return [];\n } finally {\n store.setLoadingOrganizations(false);\n fetchOrganizationsPromiseRef.current = null;\n }\n })();\n\n return fetchOrganizationsPromiseRef.current;\n }, [globalBaseUrl, setActiveContext]);\n\n // ===========================================================================\n // Bootstrap retry (transient-failure self-heal)\n // ===========================================================================\n //\n // A transient backend failure during bootstrap (cold service, gateway blip,\n // Valkey/pg hiccup) used to leave the switcher empty for the whole session —\n // the shell's Header effect only re-fires on accessToken change, so nothing\n // ever retried. Retry with exponential backoff, bounded so a genuinely down\n // backend does not hammer: 1.5s, 3s, 6s, 12s, 12s (max 5 attempts, re-armed\n // by any subsequent successful bootstrap).\n useEffect(() => {\n if (bootstrapFailureNonce === 0) return undefined;\n if (bootstrapRetryAttemptRef.current >= 5) return undefined;\n if (!resolveAccessToken()) return undefined;\n bootstrapRetryAttemptRef.current += 1;\n const delayMs = Math.min(\n 1500 * 2 ** (bootstrapRetryAttemptRef.current - 1),\n 12000,\n );\n const timer = window.setTimeout(() => {\n void fetchOrganizations();\n }, delayMs);\n return () => {\n window.clearTimeout(timer);\n };\n }, [bootstrapFailureNonce, fetchOrganizations]);\n\n // Connectivity recovery: if the browser reports we are back online and the\n // switcher is still empty, re-run the bootstrap immediately with a fresh\n // retry budget.\n useEffect(() => {\n const onOnline = () => {\n if (\n useAuthStore.getState().organizations.length === 0 &&\n resolveAccessToken()\n ) {\n bootstrapRetryAttemptRef.current = 0;\n void fetchOrganizations();\n }\n };\n window.addEventListener(\"online\", onOnline);\n return () => {\n window.removeEventListener(\"online\", onOnline);\n };\n }, [fetchOrganizations]);\n\n // ===========================================================================\n // Fetch Workspaces for Organization (Global Gateway)\n // ===========================================================================\n\n const fetchWorkspacesForOrg = useCallback(\n async (organizationId: string): Promise<Workspace[]> => {\n const store = useAuthStore.getState();\n const token = resolveAccessToken();\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n // Account-scoped in-memory deduplication; this key is never persisted or logged.\n const requestKey = JSON.stringify([\n profileId,\n userId,\n token,\n organizationId,\n ]);\n const existingPromise =\n fetchWorkspacesPromisesRef.current.get(requestKey);\n if (existingPromise) return existingPromise;\n const currentSelection = () => {\n const current = useAuthStore.getState();\n return (\n resolveActiveProfileId() === profileId &&\n current.user?.id === userId &&\n resolveAccessToken() === token &&\n current.currentOrganizationId === organizationId\n );\n };\n\n // Only bail when there is NO token at all. An expired one is handled by\n // graphqlFetch's refresh-and-retry (or ends in the shell's forced\n // logout) — silently returning [] here masked expiry as an empty list.\n if (!token) {\n return [];\n }\n\n store.setLoadingWorkspaces(true);\n setError(null);\n\n // Store the promise for deduplication before starting\n const fetchPromise = (async () => {\n try {\n const result = await graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n if (!currentSelection()) return [];\n // myWorkspaces returns workspaces across all the user's orgs;\n // filter to the requested org before handing to downstream.\n if (result.data?.workspaces?.items) {\n result.data.workspaces.items = result.data.workspaces.items.filter(\n (w) => w.organizationId === organizationId,\n );\n }\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (isAuthGraphQLError(result.errors)) {\n throw new AuthError(errorMessage);\n }\n console.error(\"Failed to fetch workspaces:\", result.errors);\n throw new Error(errorMessage);\n }\n\n // `myWorkspaces` returns every workspace the caller can see across\n // all of their orgs/tenants. The store key is per-org (selecting\n // org X then opening the dropdown should only show X's workspaces),\n // so we filter down to the requested org here. The dropdown filter\n // in ContextSwitcher.tsx is a defence-in-depth — without this\n // narrowing the store would briefly hold the caller's other-org\n // workspaces and the auto-pick logic (lines below) might select\n // one of them as the default.\n const allWs: Workspace[] = result.data?.workspaces?.items ?? [];\n const ws = allWs.filter((w) => w.organizationId === organizationId);\n store.setWorkspaces(ws);\n return ws;\n } catch (err) {\n if (!currentSelection() || err instanceof AuthError) {\n return [];\n }\n const message =\n err instanceof Error ? err.message : \"Failed to fetch workspaces\";\n console.error(\"Failed to fetch workspaces:\", err);\n setError(message);\n return [];\n } finally {\n if (currentSelection()) store.setLoadingWorkspaces(false);\n fetchWorkspacesPromisesRef.current.delete(requestKey);\n }\n })();\n\n fetchWorkspacesPromisesRef.current.set(requestKey, fetchPromise);\n return fetchPromise;\n },\n [globalBaseUrl],\n );\n\n // ===========================================================================\n // Issue Workspace Token (Workspace Gateway - wspace-auth-svc)\n // ===========================================================================\n\n const issueWorkspaceToken = useCallback(\n async (\n organizationId: string,\n workspaceId?: string,\n projectId?: string | null,\n // Target workspace's tenant. Sent as x-tenant-id so the token service can\n // resolve the tenant for a super_admin switching into a workspace not\n // mirrored into the wspace layer (whose access token carries no tenant\n // claim). Regular members resolve their tenant via membership regardless.\n tenantId?: string,\n ): Promise<{\n success: boolean;\n token?: string;\n resolvedWorkspaceId?: string;\n error?: string;\n }> => {\n const store = useAuthStore.getState();\n const originalScope = [\n store.currentOrganizationId,\n store.currentWorkspaceId,\n store.currentProjectId,\n ];\n const workspaceWide = isWorkspaceWideSelection({\n organizationId,\n workspaceId: workspaceId ?? null,\n });\n const authToken = resolveAccessToken();\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n\n // Expired tokens flow through: graphqlFetch refreshes and retries once.\n if (!authToken) {\n return { success: false, error: \"No valid access token available\" };\n }\n\n setError(null);\n\n try {\n const result = await graphqlFetch<{\n issueWorkspaceToken: IssueWorkspaceTokenResult;\n }>({\n gateway: \"workspace\",\n query: ISSUE_WORKSPACE_TOKEN_MUTATION,\n variables: {\n input: {\n organizationId,\n ...(workspaceId && { workspaceId }),\n ...(projectId && { projectId }),\n },\n },\n authToken,\n baseUrl: workspaceBaseUrl,\n tenantId,\n });\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (!isAuthGraphQLError(result.errors)) {\n console.error(\"Failed to issue workspace token:\", result.errors);\n }\n return { success: false, error: errorMessage };\n }\n\n const tokenResult = result.data?.issueWorkspaceToken;\n\n if (tokenResult?.success && tokenResult.token) {\n const latest = useAuthStore.getState();\n if (\n resolveActiveProfileId() !== profileId ||\n useAuthStore.getState().user?.id !== userId ||\n resolveAccessToken() !== authToken ||\n originalScope.some(\n (value, index) =>\n value !==\n [\n latest.currentOrganizationId,\n latest.currentWorkspaceId,\n latest.currentProjectId,\n ][index],\n ) ||\n isWorkspaceWideSelection({\n organizationId,\n workspaceId: workspaceId ?? null,\n }) !== workspaceWide ||\n !isWorkspaceTokenValidForContext(tokenResult.token, {\n organizationId,\n workspaceId,\n projectId,\n workspaceWide,\n })\n ) {\n return {\n success: false,\n error: \"Workspace context changed during token issuance\",\n };\n }\n const resolvedWsId = tokenResult.workspaceId || workspaceId;\n if (resolvedWsId) {\n store.setCurrentWorkspaceId(resolvedWsId);\n }\n store.setWorkspaceToken(tokenResult.token);\n return {\n success: true,\n token: tokenResult.token,\n resolvedWorkspaceId: resolvedWsId || undefined,\n };\n }\n\n return {\n success: false,\n error:\n tokenResult?.error?.message ?? \"Failed to issue workspace token\",\n };\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : \"Failed to issue workspace token\";\n console.error(\"Failed to issue workspace token:\", err);\n setError(message);\n return { success: false, error: message };\n }\n },\n [workspaceBaseUrl],\n );\n\n // ===========================================================================\n // Fetch Projects (Workspace Gateway - requires workspace token)\n // ===========================================================================\n\n const fetchProjects = useCallback(async (): Promise<Project[]> => {\n const store = useAuthStore.getState();\n const authToken = resolveAccessToken();\n const wsToken = store.workspaceToken;\n const workspaceId = store.currentWorkspaceId;\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n const organizationId = store.currentOrganizationId;\n const projectId = store.currentProjectId;\n const wide = isWorkspaceWideSelection({ organizationId, workspaceId });\n const currentSelection = () => {\n const current = useAuthStore.getState();\n return (\n resolveActiveProfileId() === profileId &&\n current.user?.id === userId &&\n resolveAccessToken() === authToken &&\n current.workspaceToken === wsToken &&\n current.currentOrganizationId === organizationId &&\n current.currentWorkspaceId === workspaceId &&\n current.currentProjectId === projectId &&\n isWorkspaceWideSelection({ organizationId, workspaceId }) === wide\n );\n };\n\n // Expired tokens flow through: graphqlFetch refreshes and retries once.\n if (!authToken) {\n return [];\n }\n\n if (!wsToken) {\n return [];\n }\n\n if (!workspaceId) {\n return [];\n }\n\n store.setLoadingProjects(true);\n setError(null);\n\n try {\n const result = await graphqlFetch<ProjectsResponse>({\n gateway: \"workspace\",\n query: PROJECTS_QUERY,\n authToken,\n workspaceToken: wsToken,\n baseUrl: workspaceBaseUrl,\n });\n\n if (!currentSelection()) return [];\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (isAuthGraphQLError(result.errors)) {\n throw new AuthError(errorMessage);\n }\n console.error(\"Failed to fetch projects:\", result.errors);\n throw new Error(errorMessage);\n }\n\n const projs: Project[] = result.data?.projects?.items ?? [];\n store.setProjects(projs);\n return projs;\n } catch (err) {\n if (err instanceof AuthError) {\n return [];\n }\n const message =\n err instanceof Error ? err.message : \"Failed to fetch projects\";\n console.error(\"Failed to fetch projects:\", err);\n setError(message);\n return [];\n } finally {\n store.setLoadingProjects(false);\n }\n }, [workspaceBaseUrl]);\n\n // ===========================================================================\n // High-Level Selection Actions\n // ===========================================================================\n\n const selectOrganization = useCallback(\n async (org: Organization): Promise<Workspace[]> => {\n const store = useAuthStore.getState();\n\n // A deliberate switch answers any \"link not available\" notice.\n store.setContextLinkDenial(null);\n store.setCurrentWorkspaceId(null);\n store.setWorkspaceToken(null);\n store.setProjects([]);\n store.setCurrentProjectId(null);\n store.setCurrentOrganizationId(org.id);\n\n const ws = await fetchWorkspacesForOrg(org.id);\n return ws;\n },\n [fetchWorkspacesForOrg],\n );\n\n const selectWorkspace = useCallback(\n async (\n workspace: Workspace,\n ): Promise<{\n success: boolean;\n projects?: Project[];\n error?: string;\n }> => {\n const store = useAuthStore.getState();\n\n // A deliberate switch answers any \"link not available\" notice.\n store.setContextLinkDenial(null);\n store.setCurrentProjectId(null);\n store.setProjects([]);\n\n if (!workspace.organizationId) {\n console.error(\"Workspace missing organizationId:\", workspace);\n return {\n success: false,\n error: \"Workspace is missing organizationId\",\n };\n }\n\n const tokenResult = await issueWorkspaceToken(\n workspace.organizationId,\n workspace.id,\n undefined,\n workspace.tenantId,\n );\n\n if (!tokenResult.success) {\n return { success: false, error: tokenResult.error };\n }\n\n const selected = useAuthStore.getState();\n const profileId = resolveActiveProfileId();\n const userId = selected.user?.id;\n const accessToken = resolveAccessToken();\n const workspaceToken = selected.workspaceToken;\n const projectId = selected.currentProjectId;\n const wide = isWorkspaceWideSelection({\n organizationId: workspace.organizationId,\n workspaceId: workspace.id,\n });\n const currentSelection = () => {\n const current = useAuthStore.getState();\n return (\n resolveActiveProfileId() === profileId &&\n current.user?.id === userId &&\n resolveAccessToken() === accessToken &&\n current.workspaceToken === workspaceToken &&\n current.currentOrganizationId === workspace.organizationId &&\n current.currentWorkspaceId === workspace.id &&\n current.currentProjectId === projectId &&\n isWorkspaceWideSelection({\n organizationId: workspace.organizationId,\n workspaceId: workspace.id,\n }) === wide\n );\n };\n const projs = await fetchProjects();\n if (!currentSelection())\n return { success: false, error: \"Context changed during selection\" };\n\n if (\n projs.length > 0 &&\n !isWorkspaceWideSelection({\n organizationId: workspace.organizationId,\n workspaceId: workspace.id,\n })\n ) {\n store.setCurrentProjectId(projs[0]!.id);\n }\n\n return { success: true, projects: projs };\n },\n [issueWorkspaceToken, fetchProjects],\n );\n\n const selectProject = useCallback((project: Project): void => {\n const store = useAuthStore.getState();\n clearWorkspaceWideSelection({\n organizationId: store.currentOrganizationId,\n workspaceId: store.currentWorkspaceId,\n });\n store.setCurrentProjectId(project.id);\n if (\n store.workspaceToken &&\n !isWorkspaceTokenValidForContext(store.workspaceToken, {\n workspaceId: store.currentWorkspaceId,\n organizationId: store.currentOrganizationId,\n projectId: project.id,\n })\n ) {\n store.setWorkspaceToken(null);\n }\n }, []);\n\n const selectWorkspaceWide = useCallback((): void => {\n const store = useAuthStore.getState();\n const scope = {\n organizationId: activeOrganizationIdRef.current,\n workspaceId: activeWorkspaceIdRef.current,\n };\n if (\n !scope.organizationId ||\n !scope.workspaceId ||\n store.currentOrganizationId !== scope.organizationId ||\n store.currentWorkspaceId !== scope.workspaceId\n ) {\n throw new Error(\"Current workspace context required\");\n }\n if (!persistWorkspaceWideSelection(scope))\n throw new Error(\"Workspace-wide preference could not be saved\");\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n setActiveContext({ projectId: null });\n }, [setActiveContext]);\n\n // ===========================================================================\n // Cross-MFE refresh: keep the switcher lists current when another MFE mutates\n // org/workspace/project entities. microfe-workspaces emits these domain events\n // on the shared event bus (e.g. on project rename); without this the switcher\n // keeps showing the stale cached name until a full page reload.\n // ===========================================================================\n\n useEffect(() => {\n let cancelled = false;\n\n // Coalesce a refetch into a single in-flight run with one trailing re-run,\n // so a burst of events collapses to one request (+ one trailing) instead of\n // fanning out or being swallowed by a downstream per-key dedupe. The flag is\n // cleared on cleanup so nothing fires after the subscription is torn down.\n const makeCoalescedRefetch = (run: () => Promise<unknown>) => {\n let running = false;\n let queued = false;\n const trigger = () => {\n if (cancelled) return;\n if (running) {\n queued = true;\n return;\n }\n running = true;\n void run().finally(() => {\n running = false;\n if (!cancelled && queued) {\n queued = false;\n trigger();\n }\n });\n };\n return trigger;\n };\n\n const refreshProjects = makeCoalescedRefetch(() => fetchProjects());\n const refreshWorkspaces = makeCoalescedRefetch(() => {\n const orgId = useAuthStore.getState().currentOrganizationId;\n return orgId ? fetchWorkspacesForOrg(orgId) : Promise.resolve();\n });\n\n // Custom domain events (not in BurdenoffEventName) flow through `onAny`.\n const unsubscribe = eventBus.onAny((event) => {\n // Custom event names aren't in the BurdenoffEventName union; compare as string.\n const eventName: string = event.name;\n\n if (PROJECT_REFRESH_EVENTS.has(eventName)) {\n const eventWorkspaceId = readEventWorkspaceId(event.payload);\n const { currentWorkspaceId } = useAuthStore.getState();\n // Only refetch when the event targets the workspace currently in\n // context (events without a workspace id are treated as relevant).\n if (\n eventWorkspaceId &&\n currentWorkspaceId &&\n eventWorkspaceId !== currentWorkspaceId\n ) {\n return;\n }\n // Refetch only — a deleted project simply drops out of the list, and\n // `currentProject` resolves to undefined for it. Clearing the selection\n // here would race ActiveContext / the project sync effect (the real\n // source of truth), so we leave selection reconciliation to them.\n refreshProjects();\n return;\n }\n\n if (WORKSPACE_REFRESH_EVENTS.has(eventName)) {\n refreshWorkspaces();\n }\n });\n\n return () => {\n cancelled = true;\n unsubscribe();\n };\n }, [eventBus, fetchProjects, fetchWorkspacesForOrg]);\n\n // ===========================================================================\n // Get Current Items\n // ===========================================================================\n\n const currentOrganization = organizations.find(\n (org) => org.id === currentOrganizationId,\n );\n const currentWorkspace = workspaces.find(\n (ws) => ws.id === currentWorkspaceId,\n );\n const currentProject = projects.find((proj) => proj.id === currentProjectId);\n\n // ===========================================================================\n // Return Hook API\n // ===========================================================================\n\n return {\n organizations,\n workspaces,\n projects,\n\n currentOrganizationId,\n currentWorkspaceId,\n currentProjectId,\n\n currentOrganization,\n currentWorkspace,\n currentProject,\n\n workspaceToken,\n\n isLoadingOrganizations,\n isLoadingWorkspaces,\n isLoadingProjects,\n isLoading:\n isLoadingOrganizations || isLoadingWorkspaces || isLoadingProjects,\n\n error,\n\n fetchOrganizations,\n fetchWorkspacesForOrg,\n issueWorkspaceToken,\n fetchProjects,\n\n selectOrganization,\n selectWorkspace,\n selectProject,\n selectWorkspaceWide,\n };\n}\n\n/**\n * Hook to reactively ensure workspaces are synced with the current organization.\n *\n * This handles edge cases where the organization changes but workspaces\n * from a different org are still in the store (e.g., stale data, race conditions).\n *\n * Usage:\n * ```tsx\n * import { useWorkspaceSyncEffect } from '@burdenoff/microfe-auth';\n *\n * function MyComponent() {\n * useWorkspaceSyncEffect(); // Ensures workspaces stay in sync\n * // ...\n * }\n * ```\n */\nexport function useWorkspaceSyncEffect(\n options?: UseContextSwitcherDataOptions,\n) {\n const currentOrganizationId = useAuthStore(\n (state) => state.currentOrganizationId,\n );\n const workspaces = useAuthStore((state) => state.workspaces);\n const accessToken = useAuthStore((state) => state.accessToken);\n const isLoadingWorkspaces = useAuthStore(\n (state) => state.isLoadingWorkspaces,\n );\n const resolvedBaseUrls = resolveGatewayBaseUrls();\n const globalBaseUrl =\n options?.globalBaseUrl ?? resolvedBaseUrls.globalBaseUrl;\n const disableWorkspaceSync = options?.disableWorkspaceSync ?? false;\n\n useEffect(() => {\n if (disableWorkspaceSync) return;\n // Skip if no org selected, no token, or already loading\n if (!currentOrganizationId || !accessToken || isLoadingWorkspaces) return;\n\n // Check if any workspace matches the current org\n const hasMatchingWorkspaces = workspaces.some(\n (ws) => ws.organizationId === currentOrganizationId,\n );\n\n // If workspaces exist but none match current org, we need to refetch\n // This can happen in edge cases with stale data or race conditions\n if (workspaces.length > 0 && !hasMatchingWorkspaces) {\n const store = useAuthStore.getState();\n store.setLoadingWorkspaces(true);\n\n // Inline fetch to avoid circular dependency with useContextSwitcherData\n graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: accessToken,\n baseUrl: globalBaseUrl,\n })\n .then((result) => {\n // myWorkspaces returns ALL my workspaces; filter to current org\n // client-side (the old server-side filter no longer exists).\n if (result.data?.workspaces?.items) {\n result.data.workspaces.items = result.data.workspaces.items.filter(\n (w) => w.organizationId === currentOrganizationId,\n );\n }\n if (!result.errors?.length) {\n const allWs: Workspace[] = result.data?.workspaces?.items ?? [];\n // Same per-org narrowing as fetchWorkspacesForOrg — see the\n // comment there for why we don't just dump the full caller-\n // visible list into the store.\n const ws = allWs.filter(\n (w) => w.organizationId === currentOrganizationId,\n );\n store.setWorkspaces(ws);\n }\n })\n .catch(() => {\n // Silent fail - user can manually refresh or select org again\n })\n .finally(() => {\n store.setLoadingWorkspaces(false);\n });\n }\n }, [\n currentOrganizationId,\n workspaces,\n accessToken,\n isLoadingWorkspaces,\n globalBaseUrl,\n disableWorkspaceSync,\n ]);\n}\n\n/**\n * Hook to reactively ensure projects are loaded when a workspace is selected.\n *\n * This handles the case where a workspace is auto-selected (from URL/localStorage)\n * but projects haven't been fetched yet. It will:\n * 1. Issue a workspace token if not present\n * 2. Fetch projects for the selected workspace\n *\n * Usage:\n * ```tsx\n * import { useProjectSyncEffect } from '@burdenoff/microfe-auth';\n *\n * function MyComponent() {\n * useProjectSyncEffect(); // Ensures projects are loaded for selected workspace\n * // ...\n * }\n * ```\n */\nexport function useProjectSyncEffect(options?: UseContextSwitcherDataOptions) {\n const currentWorkspaceId = useAuthStore((state) => state.currentWorkspaceId);\n const currentOrganizationId = useAuthStore(\n (state) => state.currentOrganizationId,\n );\n const workspaces = useAuthStore((state) => state.workspaces);\n const projects = useAuthStore((state) => state.projects);\n const currentProjectId = useAuthStore((state) => state.currentProjectId);\n const workspaceToken = useAuthStore((state) => state.workspaceToken);\n const accessToken = useAuthStore((state) => state.accessToken);\n const resolvedBaseUrls = resolveGatewayBaseUrls();\n const workspaceBaseUrl =\n options?.workspaceBaseUrl ?? resolvedBaseUrls.workspaceBaseUrl;\n const disableProjectSync = options?.disableProjectSync ?? false;\n const skipProjectFetch = options?.skipProjectFetch ?? false;\n\n // Use refs to prevent the infinite loop caused by including loading/failure\n // state in the dependency array. Without these, the cycle is:\n // setLoadingProjects(true) -> dep change -> effect re-runs (guarded) ->\n // async completes -> setLoadingProjects(false) -> dep change ->\n // effect re-runs -> starts again -> INFINITE LOOP\n const syncInProgressRef = useRef(false);\n const [syncRevision, setSyncRevision] = useState(0);\n const emptyProjectsResultRef = useRef<{\n scopeKey: string;\n projects: Project[];\n } | null>(null);\n // Pending retry timer per workspace, so transient failures (e.g. RBAC lag\n // right after workspace creation) get an automatic second chance even if no\n // dependency change re-triggers the effect.\n const retryTimerRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(\n new Map(),\n );\n // Per-workspace failure tracker — caps retry attempts when project bootstrap\n // (token issue or list) fails. An empty authorized project list is a valid\n // workspace-only context, not a provisioning failure.\n //\n // We track both count AND lastFailureAt so we can honour a \"cool-down\" window:\n // after PROJECT_SYNC_FAILURE_COOLDOWN_MS has elapsed since the last failure,\n // the counter is treated as cleared so transient backend hiccups (e.g. RBAC\n // role propagation lag right after signup) don't permanently lock a workspace\n // out of the context dropdown — the original cause of marqueue §729.\n const projectSyncFailuresRef = useRef<\n Map<string, { count: number; lastFailureAt: number }>\n >(new Map());\n const PROJECT_SYNC_MAX_FAILURES = 8;\n const PROJECT_SYNC_FAILURE_COOLDOWN_MS = 30_000;\n useEffect(() => {\n if (disableProjectSync) return;\n // Skip if no workspace selected or no access token\n if (!currentWorkspaceId || !accessToken) return;\n\n // Skip if a sync is already in progress (ref-based guard avoids dep loop)\n if (syncInProgressRef.current) return;\n\n // Stop retrying once this workspace has failed too many times in a row.\n // Counter is reset on success, on workspace switch (different key), by\n // an explicit user retry path (e.g. selectWorkspace), or after the\n // cool-down window has elapsed since the last failure (so transient\n // backend issues don't permanently wedge the dropdown).\n const failureEntry = projectSyncFailuresRef.current.get(currentWorkspaceId);\n if (\n failureEntry &&\n failureEntry.count >= PROJECT_SYNC_MAX_FAILURES &&\n Date.now() - failureEntry.lastFailureAt < PROJECT_SYNC_FAILURE_COOLDOWN_MS\n ) {\n return;\n }\n if (\n failureEntry &&\n Date.now() - failureEntry.lastFailureAt >=\n PROJECT_SYNC_FAILURE_COOLDOWN_MS\n ) {\n // Cool-down elapsed — clear the counter so we get a fresh budget.\n projectSyncFailuresRef.current.delete(currentWorkspaceId);\n }\n\n // Resolve organizationId: prefer the workspace object (if loaded), fall\n // back to the persisted currentOrganizationId. The workspaces array is\n // NOT persisted by zustand, so on page reload it is empty until\n // fetchOrganizations() completes. Using the persisted org ID lets us\n // issue the workspace token immediately instead of waiting.\n const currentWorkspace = workspaces.find(\n (ws) => ws.id === currentWorkspaceId,\n );\n\n // Guard: transient (newOrg, oldWs) divergence during org switches — token issuance always fails.\n if (\n currentWorkspace &&\n currentOrganizationId &&\n currentWorkspace.organizationId !== currentOrganizationId\n ) {\n return;\n }\n\n const organizationId =\n currentWorkspace?.organizationId ?? currentOrganizationId;\n if (!organizationId) return;\n\n const store = useAuthStore.getState();\n if (\n currentProjectId &&\n getUrlParam(\"project\") === currentProjectId &&\n getUrlParam(\"workspace\") === currentWorkspaceId &&\n getUrlParam(\"org\") === organizationId\n ) {\n clearWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n });\n }\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n const workspaceWide = isWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n });\n if (workspaceWide && store.currentProjectId) {\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n return;\n }\n\n // If we don't have a workspace token or it's expired, issue one first\n const needsToken =\n !workspaceToken ||\n isTokenExpired(workspaceToken) ||\n !isWorkspaceTokenValidForContext(workspaceToken, {\n workspaceId: currentWorkspaceId,\n organizationId: currentOrganizationId,\n projectId: currentProjectId,\n workspaceWide,\n });\n\n // Skip if projects are already loaded for this workspace AND token is valid\n const scopeKey = JSON.stringify([\n profileId,\n userId,\n accessToken,\n organizationId,\n currentWorkspaceId,\n ]);\n const hasProjectsForWorkspace =\n projects.some((p) => p.workspaceId === currentWorkspaceId) ||\n (emptyProjectsResultRef.current?.scopeKey === scopeKey &&\n emptyProjectsResultRef.current.projects === projects);\n if (hasProjectsForWorkspace && !needsToken) {\n if (!store.currentProjectId && !workspaceWide) {\n const firstProject = projects.find(\n (project) => project.workspaceId === currentWorkspaceId,\n );\n if (firstProject) {\n store.setCurrentProjectId(firstProject.id);\n }\n }\n return;\n }\n\n const contextStillCurrent = () => {\n const latest = useAuthStore.getState();\n return (\n latest.accessToken === accessToken &&\n latest.user?.id === userId &&\n resolveActiveProfileId() === profileId &&\n latest.currentOrganizationId === organizationId &&\n latest.currentWorkspaceId === currentWorkspaceId &&\n latest.currentProjectId === currentProjectId &&\n isWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n }) === workspaceWide\n );\n };\n\n const syncProjects = async () => {\n syncInProgressRef.current = true;\n store.setLoadingProjects(true);\n let succeeded = false;\n\n try {\n let wsToken = workspaceToken;\n\n // Issue workspace token if needed\n if (needsToken) {\n const tokenResult = await graphqlFetch<{\n issueWorkspaceToken: IssueWorkspaceTokenResult;\n }>({\n gateway: \"workspace\",\n query: ISSUE_WORKSPACE_TOKEN_MUTATION,\n variables: {\n input: {\n organizationId,\n workspaceId: currentWorkspaceId,\n ...(currentProjectId ? { projectId: currentProjectId } : {}),\n },\n },\n authToken: accessToken,\n baseUrl: workspaceBaseUrl,\n // See issueWorkspaceToken: send the current workspace's tenant so a\n // super_admin (no tenant claim) can resolve it for an unmirrored ws.\n tenantId: workspaces.find((ws) => ws.id === currentWorkspaceId)\n ?.tenantId,\n });\n\n if (\n tokenResult.data?.issueWorkspaceToken?.success &&\n tokenResult.data.issueWorkspaceToken.token\n ) {\n wsToken = tokenResult.data.issueWorkspaceToken.token;\n if (\n !contextStillCurrent() ||\n !isWorkspaceTokenValidForContext(wsToken, {\n workspaceId: currentWorkspaceId,\n organizationId,\n projectId: currentProjectId,\n workspaceWide,\n })\n )\n return;\n store.setWorkspaceToken(wsToken);\n } else {\n return;\n }\n }\n\n // Fetch projects (skip if already loaded or token-only sync requested)\n if (wsToken && !hasProjectsForWorkspace && !skipProjectFetch) {\n const projectsResult = await graphqlFetch<ProjectsResponse>({\n gateway: \"workspace\",\n query: PROJECTS_QUERY,\n authToken: accessToken,\n workspaceToken: wsToken,\n baseUrl: workspaceBaseUrl,\n });\n\n if (!contextStillCurrent() || projectsResult.errors?.length) {\n return;\n }\n\n const projs: Project[] = projectsResult.data?.projects?.items ?? [];\n // myProjects lists accessible projects. An empty result does not\n // authorize creating one. Remember this result to avoid a render/\n // fetch loop; changing account or clearing the list invalidates it.\n emptyProjectsResultRef.current =\n projs.length === 0 ? { scopeKey, projects: projs } : null;\n store.setProjects(projs);\n\n // A restored/deep-linked project may have been removed or revoked.\n // Discard its token so the next sync establishes the current scope.\n if (\n currentProjectId &&\n !projs.some((project) => project.id === currentProjectId)\n ) {\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n }\n\n // Auto-select first project if none selected and projects exist\n if (\n projs.length > 0 &&\n !useAuthStore.getState().currentProjectId &&\n !isWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n })\n ) {\n store.setCurrentProjectId(projs[0].id);\n }\n }\n\n succeeded = true;\n } catch {\n // A later auth/context update should be able to retry.\n } finally {\n store.setLoadingProjects(false);\n syncInProgressRef.current = false;\n const scopeCurrent = contextStillCurrent();\n if (!scopeCurrent) setSyncRevision((revision) => revision + 1);\n const failures = projectSyncFailuresRef.current;\n if (scopeCurrent && succeeded) {\n failures.delete(currentWorkspaceId);\n const pending = retryTimerRef.current.get(currentWorkspaceId);\n if (pending) {\n clearTimeout(pending);\n retryTimerRef.current.delete(currentWorkspaceId);\n }\n } else if (scopeCurrent) {\n const existing = failures.get(currentWorkspaceId);\n const nextCount = (existing?.count ?? 0) + 1;\n failures.set(currentWorkspaceId, {\n count: nextCount,\n lastFailureAt: Date.now(),\n });\n // Schedule a backoff retry for transient backend lag (e.g. RBAC\n // role propagation right after signup → first projects() call hits\n // permission-denied). Without this, the effect would only re-run on\n // dep change and the user would be stuck staring at an empty\n // project dropdown until something else triggered a re-render.\n if (nextCount < PROJECT_SYNC_MAX_FAILURES) {\n const existingTimer = retryTimerRef.current.get(currentWorkspaceId);\n if (existingTimer) clearTimeout(existingTimer);\n const backoffMs = Math.min(1000 * 2 ** (nextCount - 1), 8000);\n const timer = setTimeout(() => {\n retryTimerRef.current.delete(currentWorkspaceId);\n // Only re-run if the workspace context is still the same and\n // a project hasn't been loaded for it in the meantime.\n const latest = useAuthStore.getState();\n if (\n contextStillCurrent() &&\n latest.currentWorkspaceId === currentWorkspaceId &&\n !latest.projects.some(\n (p) => p.workspaceId === currentWorkspaceId,\n ) &&\n !syncInProgressRef.current\n ) {\n // Bump a render by clearing then resetting workspaceToken\n // when valid, OR by re-issuing projects fetch directly. The\n // simplest robust trigger is to clear the in-progress guard\n // and call syncProjects() again with current closure values.\n void syncProjects();\n }\n }, backoffMs);\n retryTimerRef.current.set(currentWorkspaceId, timer);\n }\n }\n }\n };\n\n syncProjects();\n }, [\n syncRevision,\n currentWorkspaceId,\n currentOrganizationId,\n workspaces,\n projects,\n currentProjectId,\n workspaceToken,\n accessToken,\n workspaceBaseUrl,\n disableProjectSync,\n skipProjectFetch,\n ]);\n\n // Cleanup any pending retry timers on unmount so we don't leak stray\n // setTimeout callbacks running against stale closures.\n useEffect(() => {\n const timers = retryTimerRef.current;\n return () => {\n timers.forEach((timer) => clearTimeout(timer));\n timers.clear();\n };\n }, []);\n}\n// hmr-bust-1778064429\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useContextSwitcherData.js","names":[],"sources":["../../../src/auth/hooks/useContextSwitcherData.ts"],"sourcesContent":["import {\n isWorkspaceWideSelection,\n selectWorkspaceWide as persistWorkspaceWideSelection,\n clearWorkspaceWideSelection,\n} from \"@burdenoff/fe-libs/shared/providers/shell/WorkspaceWideSelection\";\nimport { resolveActiveProfileId } from \"@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager\";\nimport { getUrlParam } from \"@burdenoff/fe-libs/shared/providers/shell/ActiveContextStorage\";\nimport { useCallback, useState, useEffect, useRef } from \"react\";\nimport {\n graphqlFetch,\n getStoredAuthToken,\n} from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useActiveContext } from \"@burdenoff/fe-libs/shared/providers/shell/ActiveContextProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { Organization, Workspace, Project } from \"../types\";\nimport { resolveGatewayBaseUrls } from \"../utils/gatewayBaseUrls\";\nimport {\n decideOrganizationDeepLink,\n decideWorkspaceDeepLink,\n isCompleteListing,\n} from \"./contextDeepLink\";\n\n// =============================================================================\n// Hook Options\n// =============================================================================\n\nexport interface UseContextSwitcherDataOptions {\n /**\n * Optional Global API Gateway base URL override.\n * If not provided, uses GATEWAYS config from fe-libs.\n */\n globalBaseUrl?: string;\n /**\n * Optional Workspaces API Gateway base URL override.\n * If not provided, uses GATEWAYS config from fe-libs.\n */\n workspaceBaseUrl?: string;\n /** Disable initial organization/workspace bootstrap fetches */\n disableBootstrap?: boolean;\n /** Disable the workspace sync effect */\n disableWorkspaceSync?: boolean;\n /** Disable the project sync effect */\n disableProjectSync?: boolean;\n /** Only hydrate the workspace token; do not eagerly fetch projects */\n skipProjectFetch?: boolean;\n}\n\n// =============================================================================\n// GraphQL Response Types\n// =============================================================================\n\ninterface MyOrganizationsResponse {\n myOrganizations: {\n total: number;\n items: Organization[];\n };\n}\n\ninterface WorkspacesByOrgResponse {\n workspaces: {\n total: number;\n items: Workspace[];\n };\n}\n\ntype GraphQLFetchResult<T> = Awaited<ReturnType<typeof graphqlFetch<T>>>;\n\ninterface IssueWorkspaceTokenResult {\n success: boolean;\n token: string | null;\n expiresAt: string | null;\n organizationId: string | null;\n workspaceId: string | null;\n projectId: string | null;\n scopes: string[] | null;\n error: {\n code: string;\n message: string;\n } | null;\n}\n\ninterface ProjectsResponse {\n projects: {\n total: number;\n items: Project[];\n };\n}\n\n// =============================================================================\n// GraphQL Query/Mutation Strings\n// =============================================================================\n\nconst MY_ORGANIZATIONS_QUERY = `\n query MyOrganizations {\n myOrganizations {\n total\n items {\n id\n name\n slug\n tenantId\n description\n logo\n website\n status\n type\n createdAt\n updatedAt\n }\n }\n }\n`;\n\n// Both queries below historically used `workspaces(filter: $filter)` with\n// a `WorkspaceFilterInput` to fetch workspaces for a specific organization.\n// That Query field was removed/renamed in the global subgraph; calling it\n// now throws GRAPHQL_VALIDATION_FAILED (\"Cannot query field 'workspaces'\n// on type 'Query'\") which crashes the context switcher and cascades into a\n// \"Verifying workspace access...\" stuck-spinner on every Platform module\n// route (/billing, /support, /notifications, /devportal).\n//\n// The supported field is `myWorkspaces(limit, offset)` on the global\n// subgraph — self-scoped, returns the caller's workspaces across all\n// orgs. It does NOT accept a filter, so we fetch up to 100 of \"my\"\n// workspaces and let the existing downstream auto-pick logic filter by\n// organizationId client-side. The recovery path at line ~624 already\n// uses this exact aliased pattern; we're just promoting it to the\n// primary path.\nconst WORKSPACES_BY_ORG_QUERY = `\n query WorkspacesByOrg {\n workspaces: myWorkspaces(limit: 100) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n`;\n\n// A further page of `myWorkspaces`, read only when a deep-linked workspace is\n// not on the first page of a caller with more than 100 workspaces.\nconst MY_WORKSPACES_PAGE_QUERY = `\n query MyWorkspacesPage($limit: Int, $offset: Int) {\n workspaces: myWorkspaces(limit: $limit, offset: $offset) {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n type\n status\n region\n createdAt\n updatedAt\n }\n }\n }\n`;\n\n/**\n * `issueWorkspaceToken` error codes that mean the requested project can never\n * be used by this caller (they are not a member, or it no longer exists). A\n * retry with the same project cannot succeed, so the project selection has to\n * be dropped instead.\n */\nconst UNUSABLE_PROJECT_TOKEN_ERRORS: ReadonlySet<string> = new Set([\n \"NOT_PROJECT_MEMBER\",\n \"PROJECT_NOT_FOUND\",\n]);\n\nfunction isUnusableProjectTokenError(code: string | null | undefined): boolean {\n return !!code && UNUSABLE_PROJECT_TOKEN_ERRORS.has(code);\n}\n\nconst WORKSPACES_PAGE_SIZE = 100;\n// Bounds the read for a deep link: 20 pages = 2,000 workspaces.\nconst WORKSPACES_MAX_PAGES = 20;\n\nconst ISSUE_WORKSPACE_TOKEN_MUTATION = `\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n expiresAt\n organizationId\n workspaceId\n projectId\n scopes\n error {\n code\n message\n }\n }\n }\n`;\n\n// The workspace gateway exposes `myProjects` (public RBAC, caller-scoped to\n// the active workspace via x-workspace-id) and the privileged\n// `projects(filter)` (list:project). The header has no `list:project`\n// permission seeded for the typical operator, so the privileged form\n// returns FORBIDDEN even though the caller is a member of the workspace.\n// `myProjects` is the right field for \"give me the projects I can see in\n// this workspace\" — workspace scope is already established by the\n// workspace token + x-workspace-id header that we pass with the request.\nconst PROJECTS_QUERY = `\n query MyProjectsForContext {\n projects: myProjects {\n total\n items {\n id\n name\n key\n workspaceId\n status\n archived\n createdAt\n updatedAt\n }\n }\n }\n`;\n\n// =============================================================================\n// Auth Helpers\n// =============================================================================\n\n/**\n * Check whether a JWT is expired (or within bufferMs of expiry).\n * Returns true if the token is expired, close to expiry, or malformed.\n */\nfunction isTokenExpired(token: string, bufferMs = 30_000): boolean {\n try {\n const payload = JSON.parse(atob(token.split(\".\")[1]));\n if (!payload.exp) return false;\n return payload.exp * 1000 < Date.now() + bufferMs;\n } catch {\n return true;\n }\n}\n\nfunction getWorkspaceTokenPayload(token: string): {\n workspaceId?: string;\n organizationId?: string;\n projectId?: string;\n exp?: number;\n} | null {\n try {\n const payload = JSON.parse(atob(token.split(\".\")[1]));\n return {\n workspaceId:\n typeof payload.workspaceId === \"string\"\n ? payload.workspaceId\n : undefined,\n organizationId:\n typeof payload.organizationId === \"string\"\n ? payload.organizationId\n : undefined,\n projectId:\n typeof payload.projectId === \"string\" ? payload.projectId : undefined,\n exp: typeof payload.exp === \"number\" ? payload.exp : undefined,\n };\n } catch {\n return null;\n }\n}\n\nfunction isWorkspaceTokenValidForContext(\n token: string | null | undefined,\n expected: {\n workspaceId?: string | null;\n organizationId?: string | null;\n projectId?: string | null;\n workspaceWide?: boolean;\n },\n): token is string {\n if (!token || isTokenExpired(token)) {\n return false;\n }\n\n const payload = getWorkspaceTokenPayload(token);\n if (!payload) {\n return false;\n }\n\n if (expected.workspaceId && payload.workspaceId !== expected.workspaceId) {\n return false;\n }\n\n if (\n expected.organizationId &&\n payload.organizationId !== expected.organizationId\n ) {\n return false;\n }\n\n if (expected.workspaceWide && payload.projectId) return false;\n\n if (expected.projectId && payload.projectId !== expected.projectId) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Custom error class for auth-related failures (expired token, unauthorized, etc.).\n * Used to distinguish auth errors in outer catch blocks so they can be\n * suppressed from console.error — they are expected during token refresh cycles\n * and do not indicate application bugs.\n */\nclass AuthError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AuthError\";\n }\n}\n\n/**\n * Detect whether a set of GraphQL errors are authentication-related.\n */\nfunction isAuthGraphQLError(\n errors: Array<{ message?: string; extensions?: { code?: string } }>,\n): boolean {\n return errors.some(\n (e) =>\n e.extensions?.code === \"UNAUTHENTICATED\" ||\n e.message?.toLowerCase().includes(\"unauthorized\") ||\n e.message?.toLowerCase().includes(\"not authenticated\"),\n );\n}\n\n/**\n * Resolve the best available access token:\n * 1. Try the zustand store token\n * 2. If expired, fall back to profile storage (may have been refreshed)\n * 3. Return null if no valid token is available\n */\nfunction resolveAccessToken(): string | null {\n const storeToken = useAuthStore.getState().accessToken;\n\n // If store token is valid (or has no exp claim), use it\n if (storeToken && !isTokenExpired(storeToken)) {\n return storeToken;\n }\n\n // Store token is expired or missing — try profile storage (may be fresher)\n const profileToken = getStoredAuthToken();\n if (profileToken && !isTokenExpired(profileToken)) {\n return profileToken;\n }\n\n // Both expired or missing — if store token exists at all, return it as a last\n // resort (the gateway will reject it and the useEffect will re-trigger after\n // token refresh). Returning null would silently swallow the first load.\n return storeToken;\n}\n\n/**\n * The rest of the caller's workspaces after `first` (a first page of\n * `myWorkspaces`), bounded by WORKSPACES_MAX_PAGES. A failed page ends the read\n * with what was gathered, marked complete only if every page was read.\n */\nasync function readRemainingWorkspacePages(\n first: WorkspacesByOrgResponse[\"workspaces\"],\n token: string,\n globalBaseUrl: string,\n): Promise<WorkspacesByOrgResponse[\"workspaces\"]> {\n const items = [...first.items];\n const total = first.total;\n for (\n let page = 1;\n page < WORKSPACES_MAX_PAGES && items.length < total;\n page += 1\n ) {\n const next = await graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: MY_WORKSPACES_PAGE_QUERY,\n variables: { limit: WORKSPACES_PAGE_SIZE, offset: items.length },\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n const pageItems = next.errors?.length\n ? []\n : (next.data?.workspaces?.items ?? []);\n if (pageItems.length === 0) {\n // Unreadable or short: report what we have as a partial listing so no\n // denial is concluded from it.\n return { total: Math.max(total, items.length + 1), items };\n }\n items.push(...pageItems);\n }\n return { total, items };\n}\n\nfunction buildFallbackOrganizations(workspaces: Workspace[]): Organization[] {\n const uniqueOrgIds = Array.from(\n new Set(\n workspaces.flatMap((workspace) =>\n workspace.organizationId ? [workspace.organizationId] : [],\n ),\n ),\n );\n\n return uniqueOrgIds.map((organizationId, index) => ({\n id: organizationId,\n name:\n workspaces.find(\n (workspace) => workspace.organizationId === organizationId,\n )?.name || `Organization ${index + 1}`,\n slug: organizationId,\n tenantId:\n workspaces.find(\n (workspace) => workspace.organizationId === organizationId,\n )?.tenantId || \"\",\n }));\n}\n\n// =============================================================================\n// Hook Implementation\n// =============================================================================\n\n/**\n * Hook for managing the context switcher data (organizations, workspaces, projects)\n *\n * Data flow:\n * 1. fetchOrganizations() - Get all orgs user has access to (Global Gateway)\n * 2. selectOrganization(org) - Set current org & fetch workspaces for that org\n * 3. selectWorkspace(ws) - Issue workspace token & fetch projects\n * 4. selectProject(proj) - Set current project\n *\n * All GraphQL calls go through graphqlFetch from fe-libs which automatically\n * resolves gateway URLs and injects auth + locale headers.\n *\n * @param options - Optional configuration (gateway base URL overrides)\n */\n// Domain events emitted by microfe-workspaces that should refresh the switcher's\n// cached lists. Module-scoped so they aren't reallocated on every effect run.\nconst PROJECT_REFRESH_EVENTS = new Set<string>([\n \"project.created\",\n \"project.updated\",\n \"project.deleted\",\n \"project.archived\",\n \"project.unarchived\",\n]);\nconst WORKSPACE_REFRESH_EVENTS = new Set<string>([\n \"workspace.created\",\n \"workspace.updated\",\n \"workspace.deleted\",\n \"workspace.archived\",\n \"workspace.reactivated\",\n]);\n\n// Safely read a string `workspaceId` off a cross-MFE event payload of unknown\n// shape, rather than trusting a bare cast (the payload is produced by another\n// MFE and could drift). Returns undefined when absent or not a string.\nfunction readEventWorkspaceId(payload: unknown): string | undefined {\n if (payload && typeof payload === \"object\" && \"workspaceId\" in payload) {\n const value = (payload as { workspaceId?: unknown }).workspaceId;\n return typeof value === \"string\" ? value : undefined;\n }\n return undefined;\n}\n\nexport function useContextSwitcherData(\n options?: UseContextSwitcherDataOptions,\n) {\n const {\n organizationId: activeOrganizationId,\n workspaceId: activeWorkspaceId,\n projectId: activeProjectId,\n tenantId: activeTenantId,\n setContext: setActiveContext,\n } = useActiveContext();\n\n const eventBus = useEventBus();\n\n // Use selectors for reactive state\n const organizations = useAuthStore((state) => state.organizations);\n const currentOrganizationId = useAuthStore(\n (state) => state.currentOrganizationId,\n );\n const isLoadingOrganizations = useAuthStore(\n (state) => state.isLoadingOrganizations,\n );\n\n const workspaces = useAuthStore((state) => state.workspaces);\n const currentWorkspaceId = useAuthStore((state) => state.currentWorkspaceId);\n const workspaceToken = useAuthStore((state) => state.workspaceToken);\n const isLoadingWorkspaces = useAuthStore(\n (state) => state.isLoadingWorkspaces,\n );\n\n const projects = useAuthStore((state) => state.projects);\n const currentProjectId = useAuthStore((state) => state.currentProjectId);\n const isLoadingProjects = useAuthStore((state) => state.isLoadingProjects);\n\n // Restored workspaces can already be present in the profile store, so the\n // first-workspace bootstrap branch is not sufficient to hydrate tenant.\n // Resolve it from the selected accessible rows, never from a placeholder,\n // URL tenant value or a workspace belonging to another organization.\n useEffect(() => {\n const workspace = workspaces.find(\n (row) =>\n row.id === activeWorkspaceId &&\n row.organizationId === activeOrganizationId,\n );\n const organization = organizations.find(\n (row) => row.id === activeOrganizationId,\n );\n if (\n !workspace ||\n !organization ||\n !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(\n workspace.tenantId,\n ) ||\n workspace.tenantId !== organization.tenantId ||\n activeTenantId === workspace.tenantId\n ) {\n return;\n }\n // Updating only tenant preserves the selected project, including an\n // explicit workspace-wide (null-project) selection.\n setActiveContext({ tenantId: workspace.tenantId });\n }, [\n activeOrganizationId,\n activeWorkspaceId,\n activeTenantId,\n organizations,\n workspaces,\n setActiveContext,\n ]);\n\n // Local error state\n const [error, setError] = useState<string | null>(null);\n\n // Bootstrap-retry signal: incremented on every TRANSIENT bootstrap failure\n // (orgs fetch, stale-context recovery, or the inner workspace bootstrap).\n // A monotonically increasing nonce — rather than the `error` string — keys\n // the retry effect, because two consecutive failures with the identical\n // message would not re-trigger a string-keyed effect and the retry chain\n // would silently stop after one attempt.\n const [bootstrapFailureNonce, setBootstrapFailureNonce] = useState(0);\n const bootstrapRetryAttemptRef = useRef(0);\n\n // Deduplication refs to prevent duplicate GraphQL calls\n const fetchOrganizationsPromiseRef = useRef<Promise<Organization[]> | null>(\n null,\n );\n const fetchWorkspacesPromisesRef = useRef<Map<string, Promise<Workspace[]>>>(\n new Map(),\n );\n\n // Stable refs for latest context values — avoids stale closures in callbacks\n // without adding them to useCallback dep arrays (which would recreate callbacks\n // on every context change and trigger spurious fetch re-runs).\n const activeWorkspaceIdRef = useRef(activeWorkspaceId);\n const activeOrganizationIdRef = useRef(activeOrganizationId);\n useEffect(() => {\n activeWorkspaceIdRef.current = activeWorkspaceId;\n activeOrganizationIdRef.current = activeOrganizationId;\n });\n\n const resolvedBaseUrls = resolveGatewayBaseUrls();\n const globalBaseUrl =\n options?.globalBaseUrl ?? resolvedBaseUrls.globalBaseUrl;\n const workspaceBaseUrl =\n options?.workspaceBaseUrl ?? resolvedBaseUrls.workspaceBaseUrl;\n\n // Refs to break feedback loops with the Header's \"store → URL\" sync effect.\n // Without these the pair can ping-pong: this effect writes (URL → store),\n // Header writes (store → URL), Zustand re-fires our subscribers, and React\n // hits \"Maximum update depth exceeded\" — especially when a downstream call\n // (workspace token issuance) returns 504 and leaves the two halves in\n // disagreement. The refs record the last value we pushed for each axis;\n // identical pushes are skipped so the loop has a hard floor.\n const lastAppliedOrgIdRef = useRef<string | null>(null);\n const lastAppliedWorkspaceIdRef = useRef<string | null>(null);\n const lastAppliedProjectIdRef = useRef<string | null>(null);\n const lastClearedTokenContextRef = useRef<string | null>(null);\n\n useEffect(() => {\n const store = useAuthStore.getState();\n\n if (\n activeOrganizationId &&\n activeOrganizationId !== store.currentOrganizationId &&\n activeOrganizationId !== lastAppliedOrgIdRef.current\n ) {\n lastAppliedOrgIdRef.current = activeOrganizationId;\n store.setCurrentOrganizationId(activeOrganizationId);\n if (\n store.workspaces.length > 0 &&\n !store.workspaces.some(\n (workspace) => workspace.organizationId === activeOrganizationId,\n )\n ) {\n store.setWorkspaces([]);\n }\n }\n\n if (\n activeWorkspaceId &&\n activeWorkspaceId !== store.currentWorkspaceId &&\n activeWorkspaceId !== lastAppliedWorkspaceIdRef.current\n ) {\n lastAppliedWorkspaceIdRef.current = activeWorkspaceId;\n store.setCurrentWorkspaceId(activeWorkspaceId);\n if (\n store.projects.length > 0 &&\n !store.projects.some(\n (project) => project.workspaceId === activeWorkspaceId,\n )\n ) {\n store.setProjects([]);\n store.setCurrentProjectId(null);\n }\n }\n\n if (\n isWorkspaceWideSelection({\n organizationId: activeOrganizationId,\n workspaceId: activeWorkspaceId,\n }) &&\n !activeProjectId &&\n store.currentProjectId\n ) {\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n }\n\n if (\n activeProjectId &&\n activeProjectId !== store.currentProjectId &&\n activeProjectId !== lastAppliedProjectIdRef.current\n ) {\n lastAppliedProjectIdRef.current = activeProjectId;\n store.setCurrentProjectId(activeProjectId);\n }\n\n if (\n store.workspaceToken &&\n !isWorkspaceTokenValidForContext(store.workspaceToken, {\n workspaceId: activeWorkspaceId,\n organizationId: activeOrganizationId,\n projectId: activeProjectId,\n workspaceWide:\n !activeProjectId &&\n isWorkspaceWideSelection({\n organizationId: activeOrganizationId,\n workspaceId: activeWorkspaceId,\n }),\n })\n ) {\n // Clear-token-once-per-context guard: when the upstream gateway is\n // flaky (504s during workspace token issuance) the surrounding state\n // machine can oscillate between \"got token but invalid\" and \"clearing\n // token\". The downstream re-issue path then loops. Only clear once per\n // (org, ws, proj) tuple — a successful re-issue resets the ref via\n // setWorkspaceToken landing a new token for the same context.\n const ctxKey = `${activeOrganizationId ?? \"\"}|${activeWorkspaceId ?? \"\"}|${activeProjectId ?? \"\"}`;\n if (lastClearedTokenContextRef.current !== ctxKey) {\n lastClearedTokenContextRef.current = ctxKey;\n store.setWorkspaceToken(null);\n }\n }\n }, [activeOrganizationId, activeWorkspaceId, activeProjectId]);\n\n // ===========================================================================\n // Fetch Organizations (Global Gateway)\n // ===========================================================================\n\n const fetchOrganizations = useCallback(async (): Promise<Organization[]> => {\n // Deduplicate: if a fetch is already in progress, return the same promise\n if (fetchOrganizationsPromiseRef.current) {\n return fetchOrganizationsPromiseRef.current;\n }\n\n const store = useAuthStore.getState();\n const token = resolveAccessToken();\n\n if (!token) {\n // Not an error — user may not be authenticated yet (e.g., page load before login).\n // The useEffect in Header.tsx will re-trigger when accessToken becomes available.\n return [];\n }\n\n // NOTE: an EXPIRED token is deliberately NOT a bail-out. fe-libs\n // graphqlFetch now transparently refreshes on UNAUTHENTICATED and retries\n // the request once, and a definitive refresh failure triggers the shell's\n // forced logout (SESSION_EXPIRED_EVENT). The previous `isTokenExpired →\n // return []` short-circuit was the root cause of the persistently empty\n // context switcher: the expired token survives reloads in profile storage,\n // so every bootstrap silently produced [] — indistinguishable from \"you\n // have no organizations\" — until the user manually logged out.\n\n store.setLoadingOrganizations(true);\n setError(null);\n\n // Store the promise for deduplication before starting\n fetchOrganizationsPromiseRef.current = (async () => {\n try {\n const initialTargetOrgId =\n activeOrganizationIdRef.current ?? store.currentOrganizationId;\n // Deep links (contextDeepLink.ts): only ids that are in the URL right\n // now count as a link; restored ids fall back quietly as before.\n const requestedWorkspaceId = activeWorkspaceIdRef.current;\n const workspaceFromUrl =\n !!requestedWorkspaceId &&\n getUrlParam(\"workspace\") === requestedWorkspaceId;\n const orgFromUrl =\n !!initialTargetOrgId && getUrlParam(\"org\") === initialTargetOrgId;\n let result: GraphQLFetchResult<\n MyOrganizationsResponse & Partial<WorkspacesByOrgResponse>\n > =\n // The workspace list is read with the organizations whenever a\n // workspace is requested, because the workspace names its own\n // organization and may not be in the saved one.\n initialTargetOrgId || requestedWorkspaceId\n ? await (async () => {\n const [organizationsResult, workspacesResult] =\n await Promise.all([\n graphqlFetch<MyOrganizationsResponse>({\n gateway: \"global\",\n query: MY_ORGANIZATIONS_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n }),\n graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n }),\n ]);\n\n const data = organizationsResult.data?.myOrganizations\n ? {\n myOrganizations: organizationsResult.data.myOrganizations,\n ...(workspacesResult.data?.workspaces\n ? { workspaces: workspacesResult.data.workspaces }\n : {}),\n }\n : null;\n\n return {\n data,\n errors: [\n ...(organizationsResult.errors ?? []),\n ...(workspacesResult.errors ?? []),\n ],\n };\n })()\n : await graphqlFetch<MyOrganizationsResponse>({\n gateway: \"global\",\n query: MY_ORGANIZATIONS_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n\n // Self-heal stale-URL case: if the workspace side of the bootstrap\n // fails because the URL points at an inaccessible org/workspace, retry\n // with the organization-only query so the operator sees their actual\n // orgs and the URL params get corrected on next render.\n if (\n initialTargetOrgId &&\n result.errors?.length &&\n result.errors.some(\n (e) =>\n e?.extensions?.code === \"FORBIDDEN\" ||\n /not a member of this organization/i.test(e?.message ?? \"\") ||\n // Public gateway often drops the underlying FORBIDDEN error\n // and replaces it with the schema-level non-null violation\n // (\"Cannot return null for non-nullable field Query.myOrganizations\").\n // That happens when ANY field in the query throws — workspaces\n // here — and data:null cascades up. Treat that pattern as a\n // signal to retry without the workspaces filter.\n /Cannot return null for non-nullable field Query\\.myOrganizations/i.test(\n e?.message ?? \"\",\n ),\n )\n ) {\n // Fetch myOrganizations + ALL workspaces (no filter) so the\n // downstream auto-pick logic still has a workspace list to seed\n // context from. If we only refetched myOrganizations, the page\n // would render but every workspace-scoped query would fail with\n // \"Select a workspace…\".\n result = await graphqlFetch<\n MyOrganizationsResponse & Partial<WorkspacesByOrgResponse>\n >({\n gateway: \"global\",\n query: `\n query ContextSwitcherRecover {\n myOrganizations {\n total\n items { id name slug tenantId description logo website status type createdAt updatedAt }\n }\n workspaces: myWorkspaces {\n total\n items { id name slug tenantId organizationId type status region createdAt updatedAt }\n }\n }\n `,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n }\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (isAuthGraphQLError(result.errors)) {\n // Auth errors are expected during token refresh cycles — throw AuthError\n // so the outer catch can suppress console.error.\n throw new AuthError(errorMessage);\n }\n if (store.workspaces.length === 0) {\n console.error(\"Failed to fetch organizations:\", result.errors);\n }\n throw new Error(errorMessage);\n }\n\n let orgs: Organization[] = result.data?.myOrganizations?.items ?? [];\n\n // Stale-context recovery: bootstrap query was filtered by\n // initialTargetOrgId, so when the URL/persisted org doesn't belong to\n // this user the response is empty. Re-fetch the unfiltered list of\n // *all* orgs the user can access so the guard below can pick a real\n // default. Skip the recovery when no target was set (already\n // unfiltered) or when workspaces are already in the store (the\n // existing fallback path covers that case).\n if (\n initialTargetOrgId &&\n orgs.length === 0 &&\n store.workspaces.length === 0\n ) {\n const recovery = await graphqlFetch<MyOrganizationsResponse>({\n gateway: \"global\",\n query: MY_ORGANIZATIONS_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n if (!recovery.errors?.length) {\n orgs = recovery.data?.myOrganizations?.items ?? [];\n // discard the filtered workspaces payload — it was for the\n // ghost org and would only mislead the workspace selector below.\n result = {\n ...recovery,\n data: {\n myOrganizations: recovery.data?.myOrganizations ?? {\n items: [],\n total: 0,\n },\n },\n };\n } else {\n // The filtered bootstrap returned nothing AND the unfiltered\n // recovery failed — we genuinely do not know the user's org list.\n // Treat it as the failure it is (retryable via the bootstrap-retry\n // effect) instead of falling through to setOrganizations([]),\n // which presented a transient backend error as \"you have no\n // organizations\" until re-login.\n console.warn(\n \"[ContextSwitcher] Stale-context recovery failed:\",\n recovery.errors,\n );\n const recoveryMessage =\n recovery.errors?.[0]?.message ?? \"Failed to fetch organizations\";\n if (isAuthGraphQLError(recovery.errors ?? [])) {\n throw new AuthError(recoveryMessage);\n }\n throw new Error(recoveryMessage);\n }\n }\n\n const fallbackOrganizations =\n orgs.length === 0 && store.workspaces.length > 0\n ? buildFallbackOrganizations(store.workspaces)\n : [];\n const effectiveOrganizations =\n fallbackOrganizations.length > 0 ? fallbackOrganizations : orgs;\n\n store.setOrganizations(effectiveOrganizations);\n\n // Determine which organization to use for workspace fetching\n let targetOrgId = initialTargetOrgId;\n\n // The caller's workspaces across organizations, when this bootstrap\n // read them. A deep-linked workspace beyond the first page is looked\n // up in the remaining pages before anything is decided about it.\n let listedWorkspaces: WorkspacesByOrgResponse[\"workspaces\"] | null =\n result.data?.workspaces ?? null;\n if (\n workspaceFromUrl &&\n listedWorkspaces &&\n !listedWorkspaces.items.some((w) => w.id === requestedWorkspaceId) &&\n !isCompleteListing(listedWorkspaces)\n ) {\n listedWorkspaces = await readRemainingWorkspacePages(\n listedWorkspaces,\n token,\n globalBaseUrl,\n );\n }\n\n // A deep-linked workspace in another accessible organization switches\n // the organization to it, instead of being replaced by the saved\n // organization's first workspace (vc-fg-parity round 3). One the\n // caller cannot open is recorded so the shell can say so.\n const workspaceLink = decideWorkspaceDeepLink({\n requestedWorkspaceId,\n fromUrl: workspaceFromUrl,\n targetOrgId,\n organizations: orgs,\n workspaces: listedWorkspaces?.items ?? null,\n workspacesComplete: isCompleteListing(listedWorkspaces),\n });\n if (workspaceLink.kind === \"switch\") {\n const linked = workspaceLink.workspace;\n targetOrgId = linked.organizationId;\n setActiveContext({\n organizationId: linked.organizationId,\n workspaceId: linked.id,\n tenantId: linked.tenantId || null,\n projectId: getUrlParam(\"project\"),\n });\n store.setWorkspaces([]);\n store.setProjects([]);\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n store.setCurrentWorkspaceId(linked.id);\n } else {\n const denial =\n workspaceLink.kind === \"denied\"\n ? workspaceLink.denial\n : decideOrganizationDeepLink({\n requestedOrgId: initialTargetOrgId,\n fromUrl: orgFromUrl,\n organizations: orgs,\n });\n if (denial) store.setContextLinkDenial(denial);\n }\n\n // Stale-context guard: if the persisted/URL org isn't in the user's\n // accessible orgs, fall back to the first available one and clear the\n // stale workspace/project so they get re-derived. Without this, URL\n // params like ?org=<ghost-id> from a prior session leak into the new\n // user's request headers and cause \"Authentication required\" /\n // \"not a member\" errors at every subgraph.\n //\n // Check against `orgs` (freshly returned from the API in this same\n // closure) rather than `effectiveOrganizations`, which can fall back\n // to ids derived from `store.workspaces` — that fallback path could\n // contain stale ghost orgIds and mask the guard.\n const targetOrgIsAccessible =\n targetOrgId && orgs.some((org) => org.id === targetOrgId);\n if (targetOrgId && !targetOrgIsAccessible && orgs.length > 0) {\n targetOrgId = orgs[0].id;\n setActiveContext({\n organizationId: targetOrgId,\n workspaceId: null,\n tenantId: null,\n projectId: null,\n });\n store.setWorkspaces([]);\n store.setProjects([]);\n store.setCurrentWorkspaceId(null);\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n }\n\n if (targetOrgId && targetOrgId !== store.currentOrganizationId) {\n store.setCurrentOrganizationId(targetOrgId);\n }\n\n // Auto-select first organization if none is selected\n if (effectiveOrganizations.length > 0 && !targetOrgId) {\n store.setCurrentOrganizationId(effectiveOrganizations[0].id);\n targetOrgId = effectiveOrganizations[0].id;\n }\n\n // Fetch workspaces for the selected/auto-selected org if:\n // 1. We have a target org (either auto-selected or persisted from localStorage)\n // 2. AND workspaces array is empty (not persisted, or cleared)\n // This handles both initial login AND page refresh scenarios.\n // Read from useAuthStore.getState() rather than the captured `store`\n // snapshot so we see the post-guard cleared value when the org guard\n // above fired (the local `store.workspaces` ref is stale after\n // setWorkspaces([])).\n if (targetOrgId && useAuthStore.getState().workspaces.length === 0) {\n store.setLoadingWorkspaces(true);\n try {\n // myWorkspaces returns ALL workspaces the user is a member of\n // across every org. Filter to the target org client-side here.\n // The list read above covers every organization, so it serves the\n // target organization even when the guard or a deep link changed it.\n const bootstrappedWorkspaces = listedWorkspaces\n ? listedWorkspaces.items.filter(\n (w) => w.organizationId === targetOrgId,\n )\n : undefined;\n const ws = bootstrappedWorkspaces\n ? (bootstrappedWorkspaces as Workspace[])\n : await graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n }).then((wsResult) =>\n wsResult.errors?.length\n ? null\n : (wsResult.data?.workspaces?.items ?? []).filter(\n (w) => w.organizationId === targetOrgId,\n ),\n );\n\n if (!ws) {\n // Workspace bootstrap failed (errored result) while orgs\n // succeeded — retryable, otherwise the org renders with no\n // workspaces until manual interaction.\n setBootstrapFailureNonce((n) => n + 1);\n }\n if (ws) {\n store.setWorkspaces(ws);\n const persistedWorkspace = activeWorkspaceIdRef.current\n ? ws.find(\n (workspace) =>\n workspace.id === activeWorkspaceIdRef.current,\n )\n : null;\n\n if (persistedWorkspace) {\n store.setCurrentWorkspaceId(persistedWorkspace.id);\n } else if (ws.length > 0) {\n const nextActiveContext = {\n organizationId: targetOrgId,\n workspaceId: ws[0].id,\n tenantId: ws[0].tenantId,\n projectId: null,\n };\n\n if (activeWorkspaceIdRef.current) {\n // Stale-context guard: URL/persisted workspace ID is not in\n // the user's accessible workspaces for the active org. Fall\n // back to the first workspace and clear the stale URL param\n // so subsequent requests don't carry the ghost ID.\n store.setCurrentWorkspaceId(ws[0].id);\n store.setWorkspaceToken(null);\n setActiveContext(nextActiveContext);\n } else if (!store.currentWorkspaceId) {\n // Auto-select first workspace only if none is currently selected\n // (preserves selection from completeAuthFlow or persistence if applicable)\n store.setCurrentWorkspaceId(ws[0].id);\n setActiveContext(nextActiveContext);\n }\n }\n }\n } catch {\n // Non-critical for THIS call (user can still select a workspace\n // manually) but worth a bounded background retry.\n setBootstrapFailureNonce((n) => n + 1);\n } finally {\n store.setLoadingWorkspaces(false);\n }\n }\n\n // Full bootstrap succeeded — re-arm the retry budget for the next\n // genuine failure.\n bootstrapRetryAttemptRef.current = 0;\n return effectiveOrganizations;\n } catch (err) {\n // Auth errors are expected during token refresh / page load cycles.\n // Suppress from console.error — the useEffect will re-trigger when the\n // auth provider refreshes the token. (A DEFINITIVELY dead session\n // never reaches here as an AuthError loop: graphqlFetch dispatches\n // SESSION_EXPIRED_EVENT and the shell force-logs-out.)\n if (err instanceof AuthError) {\n return [];\n }\n const message =\n err instanceof Error ? err.message : \"Failed to fetch organizations\";\n if (store.workspaces.length === 0) {\n console.error(\"Failed to fetch organizations:\", err);\n }\n setError(message);\n setBootstrapFailureNonce((n) => n + 1);\n\n if (store.workspaces.length > 0) {\n const fallbackOrganizations = buildFallbackOrganizations(\n store.workspaces,\n );\n if (fallbackOrganizations.length > 0) {\n store.setOrganizations(fallbackOrganizations);\n if (!store.currentOrganizationId) {\n store.setCurrentOrganizationId(fallbackOrganizations[0].id);\n }\n return fallbackOrganizations;\n }\n }\n\n return [];\n } finally {\n store.setLoadingOrganizations(false);\n fetchOrganizationsPromiseRef.current = null;\n }\n })();\n\n return fetchOrganizationsPromiseRef.current;\n }, [globalBaseUrl, setActiveContext]);\n\n // ===========================================================================\n // Bootstrap retry (transient-failure self-heal)\n // ===========================================================================\n //\n // A transient backend failure during bootstrap (cold service, gateway blip,\n // Valkey/pg hiccup) used to leave the switcher empty for the whole session —\n // the shell's Header effect only re-fires on accessToken change, so nothing\n // ever retried. Retry with exponential backoff, bounded so a genuinely down\n // backend does not hammer: 1.5s, 3s, 6s, 12s, 12s (max 5 attempts, re-armed\n // by any subsequent successful bootstrap).\n useEffect(() => {\n if (bootstrapFailureNonce === 0) return undefined;\n if (bootstrapRetryAttemptRef.current >= 5) return undefined;\n if (!resolveAccessToken()) return undefined;\n bootstrapRetryAttemptRef.current += 1;\n const delayMs = Math.min(\n 1500 * 2 ** (bootstrapRetryAttemptRef.current - 1),\n 12000,\n );\n const timer = window.setTimeout(() => {\n void fetchOrganizations();\n }, delayMs);\n return () => {\n window.clearTimeout(timer);\n };\n }, [bootstrapFailureNonce, fetchOrganizations]);\n\n // Connectivity recovery: if the browser reports we are back online and the\n // switcher is still empty, re-run the bootstrap immediately with a fresh\n // retry budget.\n useEffect(() => {\n const onOnline = () => {\n if (\n useAuthStore.getState().organizations.length === 0 &&\n resolveAccessToken()\n ) {\n bootstrapRetryAttemptRef.current = 0;\n void fetchOrganizations();\n }\n };\n window.addEventListener(\"online\", onOnline);\n return () => {\n window.removeEventListener(\"online\", onOnline);\n };\n }, [fetchOrganizations]);\n\n // ===========================================================================\n // Fetch Workspaces for Organization (Global Gateway)\n // ===========================================================================\n\n const fetchWorkspacesForOrg = useCallback(\n async (organizationId: string): Promise<Workspace[]> => {\n const store = useAuthStore.getState();\n const token = resolveAccessToken();\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n // Account-scoped in-memory deduplication; this key is never persisted or logged.\n const requestKey = JSON.stringify([\n profileId,\n userId,\n token,\n organizationId,\n ]);\n const existingPromise =\n fetchWorkspacesPromisesRef.current.get(requestKey);\n if (existingPromise) return existingPromise;\n const currentSelection = () => {\n const current = useAuthStore.getState();\n return (\n resolveActiveProfileId() === profileId &&\n current.user?.id === userId &&\n resolveAccessToken() === token &&\n current.currentOrganizationId === organizationId\n );\n };\n\n // Only bail when there is NO token at all. An expired one is handled by\n // graphqlFetch's refresh-and-retry (or ends in the shell's forced\n // logout) — silently returning [] here masked expiry as an empty list.\n if (!token) {\n return [];\n }\n\n store.setLoadingWorkspaces(true);\n setError(null);\n\n // Store the promise for deduplication before starting\n const fetchPromise = (async () => {\n try {\n const result = await graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n if (!currentSelection()) return [];\n // myWorkspaces returns workspaces across all the user's orgs;\n // filter to the requested org before handing to downstream.\n if (result.data?.workspaces?.items) {\n result.data.workspaces.items = result.data.workspaces.items.filter(\n (w) => w.organizationId === organizationId,\n );\n }\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (isAuthGraphQLError(result.errors)) {\n throw new AuthError(errorMessage);\n }\n console.error(\"Failed to fetch workspaces:\", result.errors);\n throw new Error(errorMessage);\n }\n\n // `myWorkspaces` returns every workspace the caller can see across\n // all of their orgs/tenants. The store key is per-org (selecting\n // org X then opening the dropdown should only show X's workspaces),\n // so we filter down to the requested org here. The dropdown filter\n // in ContextSwitcher.tsx is a defence-in-depth — without this\n // narrowing the store would briefly hold the caller's other-org\n // workspaces and the auto-pick logic (lines below) might select\n // one of them as the default.\n const allWs: Workspace[] = result.data?.workspaces?.items ?? [];\n const ws = allWs.filter((w) => w.organizationId === organizationId);\n store.setWorkspaces(ws);\n return ws;\n } catch (err) {\n if (!currentSelection() || err instanceof AuthError) {\n return [];\n }\n const message =\n err instanceof Error ? err.message : \"Failed to fetch workspaces\";\n console.error(\"Failed to fetch workspaces:\", err);\n setError(message);\n return [];\n } finally {\n if (currentSelection()) store.setLoadingWorkspaces(false);\n fetchWorkspacesPromisesRef.current.delete(requestKey);\n }\n })();\n\n fetchWorkspacesPromisesRef.current.set(requestKey, fetchPromise);\n return fetchPromise;\n },\n [globalBaseUrl],\n );\n\n // ===========================================================================\n // Issue Workspace Token (Workspace Gateway - wspace-auth-svc)\n // ===========================================================================\n\n const issueWorkspaceToken = useCallback(\n async (\n organizationId: string,\n workspaceId?: string,\n projectId?: string | null,\n // Target workspace's tenant. Sent as x-tenant-id so the token service can\n // resolve the tenant for a super_admin switching into a workspace not\n // mirrored into the wspace layer (whose access token carries no tenant\n // claim). Regular members resolve their tenant via membership regardless.\n tenantId?: string,\n ): Promise<{\n success: boolean;\n token?: string;\n resolvedWorkspaceId?: string;\n error?: string;\n }> => {\n const store = useAuthStore.getState();\n const originalScope = [\n store.currentOrganizationId,\n store.currentWorkspaceId,\n store.currentProjectId,\n ];\n const workspaceWide = isWorkspaceWideSelection({\n organizationId,\n workspaceId: workspaceId ?? null,\n });\n const authToken = resolveAccessToken();\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n\n // Expired tokens flow through: graphqlFetch refreshes and retries once.\n if (!authToken) {\n return { success: false, error: \"No valid access token available\" };\n }\n\n setError(null);\n\n try {\n const result = await graphqlFetch<{\n issueWorkspaceToken: IssueWorkspaceTokenResult;\n }>({\n gateway: \"workspace\",\n query: ISSUE_WORKSPACE_TOKEN_MUTATION,\n variables: {\n input: {\n organizationId,\n ...(workspaceId && { workspaceId }),\n ...(projectId && { projectId }),\n },\n },\n authToken,\n baseUrl: workspaceBaseUrl,\n tenantId,\n });\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (!isAuthGraphQLError(result.errors)) {\n console.error(\"Failed to issue workspace token:\", result.errors);\n }\n return { success: false, error: errorMessage };\n }\n\n const tokenResult = result.data?.issueWorkspaceToken;\n\n if (tokenResult?.success && tokenResult.token) {\n const latest = useAuthStore.getState();\n if (\n resolveActiveProfileId() !== profileId ||\n useAuthStore.getState().user?.id !== userId ||\n resolveAccessToken() !== authToken ||\n originalScope.some(\n (value, index) =>\n value !==\n [\n latest.currentOrganizationId,\n latest.currentWorkspaceId,\n latest.currentProjectId,\n ][index],\n ) ||\n isWorkspaceWideSelection({\n organizationId,\n workspaceId: workspaceId ?? null,\n }) !== workspaceWide ||\n !isWorkspaceTokenValidForContext(tokenResult.token, {\n organizationId,\n workspaceId,\n projectId,\n workspaceWide,\n })\n ) {\n return {\n success: false,\n error: \"Workspace context changed during token issuance\",\n };\n }\n const resolvedWsId = tokenResult.workspaceId || workspaceId;\n if (resolvedWsId) {\n store.setCurrentWorkspaceId(resolvedWsId);\n }\n store.setWorkspaceToken(tokenResult.token);\n return {\n success: true,\n token: tokenResult.token,\n resolvedWorkspaceId: resolvedWsId || undefined,\n };\n }\n\n return {\n success: false,\n error:\n tokenResult?.error?.message ?? \"Failed to issue workspace token\",\n };\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : \"Failed to issue workspace token\";\n console.error(\"Failed to issue workspace token:\", err);\n setError(message);\n return { success: false, error: message };\n }\n },\n [workspaceBaseUrl],\n );\n\n // ===========================================================================\n // Fetch Projects (Workspace Gateway - requires workspace token)\n // ===========================================================================\n\n const fetchProjects = useCallback(async (): Promise<Project[]> => {\n const store = useAuthStore.getState();\n const authToken = resolveAccessToken();\n const wsToken = store.workspaceToken;\n const workspaceId = store.currentWorkspaceId;\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n const organizationId = store.currentOrganizationId;\n const projectId = store.currentProjectId;\n const wide = isWorkspaceWideSelection({ organizationId, workspaceId });\n const currentSelection = () => {\n const current = useAuthStore.getState();\n return (\n resolveActiveProfileId() === profileId &&\n current.user?.id === userId &&\n resolveAccessToken() === authToken &&\n current.workspaceToken === wsToken &&\n current.currentOrganizationId === organizationId &&\n current.currentWorkspaceId === workspaceId &&\n current.currentProjectId === projectId &&\n isWorkspaceWideSelection({ organizationId, workspaceId }) === wide\n );\n };\n\n // Expired tokens flow through: graphqlFetch refreshes and retries once.\n if (!authToken) {\n return [];\n }\n\n if (!wsToken) {\n return [];\n }\n\n if (!workspaceId) {\n return [];\n }\n\n store.setLoadingProjects(true);\n setError(null);\n\n try {\n const result = await graphqlFetch<ProjectsResponse>({\n gateway: \"workspace\",\n query: PROJECTS_QUERY,\n authToken,\n workspaceToken: wsToken,\n baseUrl: workspaceBaseUrl,\n });\n\n if (!currentSelection()) return [];\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n if (isAuthGraphQLError(result.errors)) {\n throw new AuthError(errorMessage);\n }\n console.error(\"Failed to fetch projects:\", result.errors);\n throw new Error(errorMessage);\n }\n\n const projs: Project[] = result.data?.projects?.items ?? [];\n store.setProjects(projs);\n return projs;\n } catch (err) {\n if (err instanceof AuthError) {\n return [];\n }\n const message =\n err instanceof Error ? err.message : \"Failed to fetch projects\";\n console.error(\"Failed to fetch projects:\", err);\n setError(message);\n return [];\n } finally {\n store.setLoadingProjects(false);\n }\n }, [workspaceBaseUrl]);\n\n // ===========================================================================\n // High-Level Selection Actions\n // ===========================================================================\n\n const selectOrganization = useCallback(\n async (org: Organization): Promise<Workspace[]> => {\n const store = useAuthStore.getState();\n\n // A deliberate switch answers any \"link not available\" notice.\n store.setContextLinkDenial(null);\n store.setCurrentWorkspaceId(null);\n store.setWorkspaceToken(null);\n store.setProjects([]);\n store.setCurrentProjectId(null);\n store.setCurrentOrganizationId(org.id);\n\n const ws = await fetchWorkspacesForOrg(org.id);\n return ws;\n },\n [fetchWorkspacesForOrg],\n );\n\n const selectWorkspace = useCallback(\n async (\n workspace: Workspace,\n ): Promise<{\n success: boolean;\n projects?: Project[];\n error?: string;\n }> => {\n const store = useAuthStore.getState();\n\n // A deliberate switch answers any \"link not available\" notice.\n store.setContextLinkDenial(null);\n store.setCurrentProjectId(null);\n store.setProjects([]);\n\n if (!workspace.organizationId) {\n console.error(\"Workspace missing organizationId:\", workspace);\n return {\n success: false,\n error: \"Workspace is missing organizationId\",\n };\n }\n\n const tokenResult = await issueWorkspaceToken(\n workspace.organizationId,\n workspace.id,\n undefined,\n workspace.tenantId,\n );\n\n if (!tokenResult.success) {\n return { success: false, error: tokenResult.error };\n }\n\n const selected = useAuthStore.getState();\n const profileId = resolveActiveProfileId();\n const userId = selected.user?.id;\n const accessToken = resolveAccessToken();\n const workspaceToken = selected.workspaceToken;\n const projectId = selected.currentProjectId;\n const wide = isWorkspaceWideSelection({\n organizationId: workspace.organizationId,\n workspaceId: workspace.id,\n });\n const currentSelection = () => {\n const current = useAuthStore.getState();\n return (\n resolveActiveProfileId() === profileId &&\n current.user?.id === userId &&\n resolveAccessToken() === accessToken &&\n current.workspaceToken === workspaceToken &&\n current.currentOrganizationId === workspace.organizationId &&\n current.currentWorkspaceId === workspace.id &&\n current.currentProjectId === projectId &&\n isWorkspaceWideSelection({\n organizationId: workspace.organizationId,\n workspaceId: workspace.id,\n }) === wide\n );\n };\n const projs = await fetchProjects();\n if (!currentSelection())\n return { success: false, error: \"Context changed during selection\" };\n\n if (\n projs.length > 0 &&\n !isWorkspaceWideSelection({\n organizationId: workspace.organizationId,\n workspaceId: workspace.id,\n })\n ) {\n store.setCurrentProjectId(projs[0]!.id);\n }\n\n return { success: true, projects: projs };\n },\n [issueWorkspaceToken, fetchProjects],\n );\n\n const selectProject = useCallback((project: Project): void => {\n const store = useAuthStore.getState();\n clearWorkspaceWideSelection({\n organizationId: store.currentOrganizationId,\n workspaceId: store.currentWorkspaceId,\n });\n store.setCurrentProjectId(project.id);\n if (\n store.workspaceToken &&\n !isWorkspaceTokenValidForContext(store.workspaceToken, {\n workspaceId: store.currentWorkspaceId,\n organizationId: store.currentOrganizationId,\n projectId: project.id,\n })\n ) {\n store.setWorkspaceToken(null);\n }\n }, []);\n\n const selectWorkspaceWide = useCallback((): void => {\n const store = useAuthStore.getState();\n const scope = {\n organizationId: activeOrganizationIdRef.current,\n workspaceId: activeWorkspaceIdRef.current,\n };\n if (\n !scope.organizationId ||\n !scope.workspaceId ||\n store.currentOrganizationId !== scope.organizationId ||\n store.currentWorkspaceId !== scope.workspaceId\n ) {\n throw new Error(\"Current workspace context required\");\n }\n if (!persistWorkspaceWideSelection(scope))\n throw new Error(\"Workspace-wide preference could not be saved\");\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n setActiveContext({ projectId: null });\n }, [setActiveContext]);\n\n // ===========================================================================\n // Cross-MFE refresh: keep the switcher lists current when another MFE mutates\n // org/workspace/project entities. microfe-workspaces emits these domain events\n // on the shared event bus (e.g. on project rename); without this the switcher\n // keeps showing the stale cached name until a full page reload.\n // ===========================================================================\n\n useEffect(() => {\n let cancelled = false;\n\n // Coalesce a refetch into a single in-flight run with one trailing re-run,\n // so a burst of events collapses to one request (+ one trailing) instead of\n // fanning out or being swallowed by a downstream per-key dedupe. The flag is\n // cleared on cleanup so nothing fires after the subscription is torn down.\n const makeCoalescedRefetch = (run: () => Promise<unknown>) => {\n let running = false;\n let queued = false;\n const trigger = () => {\n if (cancelled) return;\n if (running) {\n queued = true;\n return;\n }\n running = true;\n void run().finally(() => {\n running = false;\n if (!cancelled && queued) {\n queued = false;\n trigger();\n }\n });\n };\n return trigger;\n };\n\n const refreshProjects = makeCoalescedRefetch(() => fetchProjects());\n const refreshWorkspaces = makeCoalescedRefetch(() => {\n const orgId = useAuthStore.getState().currentOrganizationId;\n return orgId ? fetchWorkspacesForOrg(orgId) : Promise.resolve();\n });\n\n // Custom domain events (not in BurdenoffEventName) flow through `onAny`.\n const unsubscribe = eventBus.onAny((event) => {\n // Custom event names aren't in the BurdenoffEventName union; compare as string.\n const eventName: string = event.name;\n\n if (PROJECT_REFRESH_EVENTS.has(eventName)) {\n const eventWorkspaceId = readEventWorkspaceId(event.payload);\n const { currentWorkspaceId } = useAuthStore.getState();\n // Only refetch when the event targets the workspace currently in\n // context (events without a workspace id are treated as relevant).\n if (\n eventWorkspaceId &&\n currentWorkspaceId &&\n eventWorkspaceId !== currentWorkspaceId\n ) {\n return;\n }\n // Refetch only — a deleted project simply drops out of the list, and\n // `currentProject` resolves to undefined for it. Clearing the selection\n // here would race ActiveContext / the project sync effect (the real\n // source of truth), so we leave selection reconciliation to them.\n refreshProjects();\n return;\n }\n\n if (WORKSPACE_REFRESH_EVENTS.has(eventName)) {\n refreshWorkspaces();\n }\n });\n\n return () => {\n cancelled = true;\n unsubscribe();\n };\n }, [eventBus, fetchProjects, fetchWorkspacesForOrg]);\n\n // ===========================================================================\n // Get Current Items\n // ===========================================================================\n\n const currentOrganization = organizations.find(\n (org) => org.id === currentOrganizationId,\n );\n const currentWorkspace = workspaces.find(\n (ws) => ws.id === currentWorkspaceId,\n );\n const currentProject = projects.find((proj) => proj.id === currentProjectId);\n\n // ===========================================================================\n // Return Hook API\n // ===========================================================================\n\n return {\n organizations,\n workspaces,\n projects,\n\n currentOrganizationId,\n currentWorkspaceId,\n currentProjectId,\n\n currentOrganization,\n currentWorkspace,\n currentProject,\n\n workspaceToken,\n\n isLoadingOrganizations,\n isLoadingWorkspaces,\n isLoadingProjects,\n isLoading:\n isLoadingOrganizations || isLoadingWorkspaces || isLoadingProjects,\n\n error,\n\n fetchOrganizations,\n fetchWorkspacesForOrg,\n issueWorkspaceToken,\n fetchProjects,\n\n selectOrganization,\n selectWorkspace,\n selectProject,\n selectWorkspaceWide,\n };\n}\n\n/**\n * Hook to reactively ensure workspaces are synced with the current organization.\n *\n * This handles edge cases where the organization changes but workspaces\n * from a different org are still in the store (e.g., stale data, race conditions).\n *\n * Usage:\n * ```tsx\n * import { useWorkspaceSyncEffect } from '@burdenoff/microfe-auth';\n *\n * function MyComponent() {\n * useWorkspaceSyncEffect(); // Ensures workspaces stay in sync\n * // ...\n * }\n * ```\n */\nexport function useWorkspaceSyncEffect(\n options?: UseContextSwitcherDataOptions,\n) {\n const currentOrganizationId = useAuthStore(\n (state) => state.currentOrganizationId,\n );\n const workspaces = useAuthStore((state) => state.workspaces);\n const accessToken = useAuthStore((state) => state.accessToken);\n const isLoadingWorkspaces = useAuthStore(\n (state) => state.isLoadingWorkspaces,\n );\n const resolvedBaseUrls = resolveGatewayBaseUrls();\n const globalBaseUrl =\n options?.globalBaseUrl ?? resolvedBaseUrls.globalBaseUrl;\n const disableWorkspaceSync = options?.disableWorkspaceSync ?? false;\n\n useEffect(() => {\n if (disableWorkspaceSync) return;\n // Skip if no org selected, no token, or already loading\n if (!currentOrganizationId || !accessToken || isLoadingWorkspaces) return;\n\n // Check if any workspace matches the current org\n const hasMatchingWorkspaces = workspaces.some(\n (ws) => ws.organizationId === currentOrganizationId,\n );\n\n // If workspaces exist but none match current org, we need to refetch\n // This can happen in edge cases with stale data or race conditions\n if (workspaces.length > 0 && !hasMatchingWorkspaces) {\n const store = useAuthStore.getState();\n store.setLoadingWorkspaces(true);\n\n // Inline fetch to avoid circular dependency with useContextSwitcherData\n graphqlFetch<WorkspacesByOrgResponse>({\n gateway: \"global\",\n query: WORKSPACES_BY_ORG_QUERY,\n authToken: accessToken,\n baseUrl: globalBaseUrl,\n })\n .then((result) => {\n // myWorkspaces returns ALL my workspaces; filter to current org\n // client-side (the old server-side filter no longer exists).\n if (result.data?.workspaces?.items) {\n result.data.workspaces.items = result.data.workspaces.items.filter(\n (w) => w.organizationId === currentOrganizationId,\n );\n }\n if (!result.errors?.length) {\n const allWs: Workspace[] = result.data?.workspaces?.items ?? [];\n // Same per-org narrowing as fetchWorkspacesForOrg — see the\n // comment there for why we don't just dump the full caller-\n // visible list into the store.\n const ws = allWs.filter(\n (w) => w.organizationId === currentOrganizationId,\n );\n store.setWorkspaces(ws);\n }\n })\n .catch(() => {\n // Silent fail - user can manually refresh or select org again\n })\n .finally(() => {\n store.setLoadingWorkspaces(false);\n });\n }\n }, [\n currentOrganizationId,\n workspaces,\n accessToken,\n isLoadingWorkspaces,\n globalBaseUrl,\n disableWorkspaceSync,\n ]);\n}\n\n/**\n * Hook to reactively ensure projects are loaded when a workspace is selected.\n *\n * This handles the case where a workspace is auto-selected (from URL/localStorage)\n * but projects haven't been fetched yet. It will:\n * 1. Issue a workspace token if not present\n * 2. Fetch projects for the selected workspace\n *\n * Usage:\n * ```tsx\n * import { useProjectSyncEffect } from '@burdenoff/microfe-auth';\n *\n * function MyComponent() {\n * useProjectSyncEffect(); // Ensures projects are loaded for selected workspace\n * // ...\n * }\n * ```\n */\nexport function useProjectSyncEffect(options?: UseContextSwitcherDataOptions) {\n const currentWorkspaceId = useAuthStore((state) => state.currentWorkspaceId);\n const currentOrganizationId = useAuthStore(\n (state) => state.currentOrganizationId,\n );\n const workspaces = useAuthStore((state) => state.workspaces);\n const projects = useAuthStore((state) => state.projects);\n const currentProjectId = useAuthStore((state) => state.currentProjectId);\n const workspaceToken = useAuthStore((state) => state.workspaceToken);\n const accessToken = useAuthStore((state) => state.accessToken);\n const { setContext: setActiveContext } = useActiveContext();\n const resolvedBaseUrls = resolveGatewayBaseUrls();\n const workspaceBaseUrl =\n options?.workspaceBaseUrl ?? resolvedBaseUrls.workspaceBaseUrl;\n const disableProjectSync = options?.disableProjectSync ?? false;\n const skipProjectFetch = options?.skipProjectFetch ?? false;\n\n // Use refs to prevent the infinite loop caused by including loading/failure\n // state in the dependency array. Without these, the cycle is:\n // setLoadingProjects(true) -> dep change -> effect re-runs (guarded) ->\n // async completes -> setLoadingProjects(false) -> dep change ->\n // effect re-runs -> starts again -> INFINITE LOOP\n const syncInProgressRef = useRef(false);\n const [syncRevision, setSyncRevision] = useState(0);\n const emptyProjectsResultRef = useRef<{\n scopeKey: string;\n projects: Project[];\n } | null>(null);\n // Pending retry timer per workspace, so transient failures (e.g. RBAC lag\n // right after workspace creation) get an automatic second chance even if no\n // dependency change re-triggers the effect.\n const retryTimerRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(\n new Map(),\n );\n // Per-workspace failure tracker — caps retry attempts when project bootstrap\n // (token issue or list) fails. An empty authorized project list is a valid\n // workspace-only context, not a provisioning failure.\n //\n // We track both count AND lastFailureAt so we can honour a \"cool-down\" window:\n // after PROJECT_SYNC_FAILURE_COOLDOWN_MS has elapsed since the last failure,\n // the counter is treated as cleared so transient backend hiccups (e.g. RBAC\n // role propagation lag right after signup) don't permanently lock a workspace\n // out of the context dropdown — the original cause of marqueue §729.\n const projectSyncFailuresRef = useRef<\n Map<string, { count: number; lastFailureAt: number }>\n >(new Map());\n const PROJECT_SYNC_MAX_FAILURES = 8;\n const PROJECT_SYNC_FAILURE_COOLDOWN_MS = 30_000;\n useEffect(() => {\n if (disableProjectSync) return;\n // Skip if no workspace selected or no access token\n if (!currentWorkspaceId || !accessToken) return;\n\n // Skip if a sync is already in progress (ref-based guard avoids dep loop)\n if (syncInProgressRef.current) return;\n\n // Stop retrying once this workspace has failed too many times in a row.\n // Counter is reset on success, on workspace switch (different key), by\n // an explicit user retry path (e.g. selectWorkspace), or after the\n // cool-down window has elapsed since the last failure (so transient\n // backend issues don't permanently wedge the dropdown).\n const failureEntry = projectSyncFailuresRef.current.get(currentWorkspaceId);\n if (\n failureEntry &&\n failureEntry.count >= PROJECT_SYNC_MAX_FAILURES &&\n Date.now() - failureEntry.lastFailureAt < PROJECT_SYNC_FAILURE_COOLDOWN_MS\n ) {\n return;\n }\n if (\n failureEntry &&\n Date.now() - failureEntry.lastFailureAt >=\n PROJECT_SYNC_FAILURE_COOLDOWN_MS\n ) {\n // Cool-down elapsed — clear the counter so we get a fresh budget.\n projectSyncFailuresRef.current.delete(currentWorkspaceId);\n }\n\n // Resolve organizationId: prefer the workspace object (if loaded), fall\n // back to the persisted currentOrganizationId. The workspaces array is\n // NOT persisted by zustand, so on page reload it is empty until\n // fetchOrganizations() completes. Using the persisted org ID lets us\n // issue the workspace token immediately instead of waiting.\n const currentWorkspace = workspaces.find(\n (ws) => ws.id === currentWorkspaceId,\n );\n\n // Guard: transient (newOrg, oldWs) divergence during org switches — token issuance always fails.\n if (\n currentWorkspace &&\n currentOrganizationId &&\n currentWorkspace.organizationId !== currentOrganizationId\n ) {\n return;\n }\n\n const organizationId =\n currentWorkspace?.organizationId ?? currentOrganizationId;\n if (!organizationId) return;\n\n const store = useAuthStore.getState();\n if (\n currentProjectId &&\n getUrlParam(\"project\") === currentProjectId &&\n getUrlParam(\"workspace\") === currentWorkspaceId &&\n getUrlParam(\"org\") === organizationId\n ) {\n clearWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n });\n }\n const profileId = resolveActiveProfileId();\n const userId = store.user?.id;\n const workspaceWide = isWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n });\n if (workspaceWide && store.currentProjectId) {\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n return;\n }\n\n // If we don't have a workspace token or it's expired, issue one first\n const needsToken =\n !workspaceToken ||\n isTokenExpired(workspaceToken) ||\n !isWorkspaceTokenValidForContext(workspaceToken, {\n workspaceId: currentWorkspaceId,\n organizationId: currentOrganizationId,\n projectId: currentProjectId,\n workspaceWide,\n });\n\n // Skip if projects are already loaded for this workspace AND token is valid\n const scopeKey = JSON.stringify([\n profileId,\n userId,\n accessToken,\n organizationId,\n currentWorkspaceId,\n ]);\n const hasProjectsForWorkspace =\n projects.some((p) => p.workspaceId === currentWorkspaceId) ||\n (emptyProjectsResultRef.current?.scopeKey === scopeKey &&\n emptyProjectsResultRef.current.projects === projects);\n if (hasProjectsForWorkspace && !needsToken) {\n if (!store.currentProjectId && !workspaceWide) {\n const firstProject = projects.find(\n (project) => project.workspaceId === currentWorkspaceId,\n );\n if (firstProject) {\n store.setCurrentProjectId(firstProject.id);\n }\n }\n return;\n }\n\n const contextStillCurrent = () => {\n const latest = useAuthStore.getState();\n return (\n latest.accessToken === accessToken &&\n latest.user?.id === userId &&\n resolveActiveProfileId() === profileId &&\n latest.currentOrganizationId === organizationId &&\n latest.currentWorkspaceId === currentWorkspaceId &&\n latest.currentProjectId === currentProjectId &&\n isWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n }) === workspaceWide\n );\n };\n\n const syncProjects = async () => {\n syncInProgressRef.current = true;\n store.setLoadingProjects(true);\n let succeeded = false;\n\n try {\n let wsToken = workspaceToken;\n\n // Issue workspace token if needed\n if (needsToken) {\n const tokenResult = await graphqlFetch<{\n issueWorkspaceToken: IssueWorkspaceTokenResult;\n }>({\n gateway: \"workspace\",\n query: ISSUE_WORKSPACE_TOKEN_MUTATION,\n variables: {\n input: {\n organizationId,\n workspaceId: currentWorkspaceId,\n ...(currentProjectId ? { projectId: currentProjectId } : {}),\n },\n },\n authToken: accessToken,\n baseUrl: workspaceBaseUrl,\n // See issueWorkspaceToken: send the current workspace's tenant so a\n // super_admin (no tenant claim) can resolve it for an unmirrored ws.\n tenantId: workspaces.find((ws) => ws.id === currentWorkspaceId)\n ?.tenantId,\n });\n\n if (\n tokenResult.data?.issueWorkspaceToken?.success &&\n tokenResult.data.issueWorkspaceToken.token\n ) {\n wsToken = tokenResult.data.issueWorkspaceToken.token;\n if (\n !contextStillCurrent() ||\n !isWorkspaceTokenValidForContext(wsToken, {\n workspaceId: currentWorkspaceId,\n organizationId,\n projectId: currentProjectId,\n workspaceWide,\n })\n )\n return;\n store.setWorkspaceToken(wsToken);\n } else {\n // A restored project the caller is no longer a member of (or that\n // was deleted) can never yield a token, and without a token the\n // project list below is never re-read to discard it. Drop the\n // project everywhere it is held so the next run issues a\n // workspace-scoped token; leaving it in place locks the user out\n // of every workspace-token page (BOFF-10431).\n if (\n currentProjectId &&\n contextStillCurrent() &&\n isUnusableProjectTokenError(\n tokenResult.data?.issueWorkspaceToken?.error?.code,\n )\n ) {\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n setActiveContext({ projectId: null });\n }\n return;\n }\n }\n\n // Fetch projects (skip if already loaded or token-only sync requested)\n if (wsToken && !hasProjectsForWorkspace && !skipProjectFetch) {\n const projectsResult = await graphqlFetch<ProjectsResponse>({\n gateway: \"workspace\",\n query: PROJECTS_QUERY,\n authToken: accessToken,\n workspaceToken: wsToken,\n baseUrl: workspaceBaseUrl,\n });\n\n if (!contextStillCurrent() || projectsResult.errors?.length) {\n return;\n }\n\n const projs: Project[] = projectsResult.data?.projects?.items ?? [];\n // myProjects lists accessible projects. An empty result does not\n // authorize creating one. Remember this result to avoid a render/\n // fetch loop; changing account or clearing the list invalidates it.\n emptyProjectsResultRef.current =\n projs.length === 0 ? { scopeKey, projects: projs } : null;\n store.setProjects(projs);\n\n // A restored/deep-linked project may have been removed or revoked.\n // Discard its token so the next sync establishes the current scope.\n if (\n currentProjectId &&\n !projs.some((project) => project.id === currentProjectId)\n ) {\n store.setCurrentProjectId(null);\n store.setWorkspaceToken(null);\n }\n\n // Auto-select first project if none selected and projects exist\n if (\n projs.length > 0 &&\n !useAuthStore.getState().currentProjectId &&\n !isWorkspaceWideSelection({\n organizationId,\n workspaceId: currentWorkspaceId,\n })\n ) {\n store.setCurrentProjectId(projs[0].id);\n }\n }\n\n succeeded = true;\n } catch {\n // A later auth/context update should be able to retry.\n } finally {\n store.setLoadingProjects(false);\n syncInProgressRef.current = false;\n const scopeCurrent = contextStillCurrent();\n if (!scopeCurrent) setSyncRevision((revision) => revision + 1);\n const failures = projectSyncFailuresRef.current;\n if (scopeCurrent && succeeded) {\n failures.delete(currentWorkspaceId);\n const pending = retryTimerRef.current.get(currentWorkspaceId);\n if (pending) {\n clearTimeout(pending);\n retryTimerRef.current.delete(currentWorkspaceId);\n }\n } else if (scopeCurrent) {\n const existing = failures.get(currentWorkspaceId);\n const nextCount = (existing?.count ?? 0) + 1;\n failures.set(currentWorkspaceId, {\n count: nextCount,\n lastFailureAt: Date.now(),\n });\n // Schedule a backoff retry for transient backend lag (e.g. RBAC\n // role propagation right after signup → first projects() call hits\n // permission-denied). Without this, the effect would only re-run on\n // dep change and the user would be stuck staring at an empty\n // project dropdown until something else triggered a re-render.\n if (nextCount < PROJECT_SYNC_MAX_FAILURES) {\n const existingTimer = retryTimerRef.current.get(currentWorkspaceId);\n if (existingTimer) clearTimeout(existingTimer);\n const backoffMs = Math.min(1000 * 2 ** (nextCount - 1), 8000);\n const timer = setTimeout(() => {\n retryTimerRef.current.delete(currentWorkspaceId);\n // Only re-run if the workspace context is still the same and\n // a project hasn't been loaded for it in the meantime.\n const latest = useAuthStore.getState();\n if (\n contextStillCurrent() &&\n latest.currentWorkspaceId === currentWorkspaceId &&\n !latest.projects.some(\n (p) => p.workspaceId === currentWorkspaceId,\n ) &&\n !syncInProgressRef.current\n ) {\n // Bump a render by clearing then resetting workspaceToken\n // when valid, OR by re-issuing projects fetch directly. The\n // simplest robust trigger is to clear the in-progress guard\n // and call syncProjects() again with current closure values.\n void syncProjects();\n }\n }, backoffMs);\n retryTimerRef.current.set(currentWorkspaceId, timer);\n }\n }\n }\n };\n\n syncProjects();\n }, [\n syncRevision,\n currentWorkspaceId,\n currentOrganizationId,\n workspaces,\n projects,\n currentProjectId,\n workspaceToken,\n accessToken,\n workspaceBaseUrl,\n disableProjectSync,\n skipProjectFetch,\n setActiveContext,\n ]);\n\n // Cleanup any pending retry timers on unmount so we don't leak stray\n // setTimeout callbacks running against stale closures.\n useEffect(() => {\n const timers = retryTimerRef.current;\n return () => {\n timers.forEach((timer) => clearTimeout(timer));\n timers.clear();\n };\n }, []);\n}\n// hmr-bust-1778064429\n"],"mappings":"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"}
|