@object-ui/auth 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +600 -0
  2. package/README.md +245 -15
  3. package/dist/ActiveOrganizationStorage.d.ts +199 -0
  4. package/dist/ActiveOrganizationStorage.d.ts.map +1 -0
  5. package/dist/ActiveOrganizationStorage.js +584 -0
  6. package/dist/AuthContext.d.ts +23 -3
  7. package/dist/AuthContext.d.ts.map +1 -1
  8. package/dist/AuthGuard.d.ts +8 -1
  9. package/dist/AuthGuard.d.ts.map +1 -1
  10. package/dist/AuthGuard.js +16 -3
  11. package/dist/AuthProvider.d.ts +1 -1
  12. package/dist/AuthProvider.d.ts.map +1 -1
  13. package/dist/AuthProvider.js +274 -9
  14. package/dist/ForgotPasswordForm.d.ts +1 -1
  15. package/dist/ForgotPasswordForm.d.ts.map +1 -1
  16. package/dist/ForgotPasswordForm.js +5 -5
  17. package/dist/LoginForm.d.ts +11 -2
  18. package/dist/LoginForm.d.ts.map +1 -1
  19. package/dist/LoginForm.js +5 -6
  20. package/dist/PreviewBanner.js +1 -1
  21. package/dist/RegisterForm.d.ts +22 -3
  22. package/dist/RegisterForm.d.ts.map +1 -1
  23. package/dist/RegisterForm.js +6 -7
  24. package/dist/SocialSignInButtons.d.ts +9 -2
  25. package/dist/SocialSignInButtons.d.ts.map +1 -1
  26. package/dist/SocialSignInButtons.js +5 -5
  27. package/dist/UserMenu.d.ts +1 -1
  28. package/dist/UserMenu.js +3 -3
  29. package/dist/createAuthClient.d.ts +1 -1
  30. package/dist/createAuthClient.d.ts.map +1 -1
  31. package/dist/createAuthClient.js +42 -4
  32. package/dist/createAuthenticatedFetch.d.ts +9 -9
  33. package/dist/createAuthenticatedFetch.d.ts.map +1 -1
  34. package/dist/createAuthenticatedFetch.js +50 -38
  35. package/dist/index.d.ts +21 -19
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +34 -17
  38. package/dist/invitation-status.d.ts +59 -0
  39. package/dist/invitation-status.d.ts.map +1 -0
  40. package/dist/invitation-status.js +32 -0
  41. package/dist/types.d.ts +25 -5
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/useAuth.d.ts +1 -1
  44. package/dist/useAuth.d.ts.map +1 -1
  45. package/dist/useAuth.js +5 -1
  46. package/dist/useWorkspaceAdminStatus.d.ts +134 -0
  47. package/dist/useWorkspaceAdminStatus.d.ts.map +1 -0
  48. package/dist/useWorkspaceAdminStatus.js +208 -0
  49. package/package.json +6 -7
  50. package/dist/useIsWorkspaceAdmin.d.ts +0 -21
  51. package/dist/useIsWorkspaceAdmin.d.ts.map +0 -1
  52. package/dist/useIsWorkspaceAdmin.js +0 -47
@@ -0,0 +1,584 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Per-user client state: WHOSE state this browser currently holds
10
+ * ({@link SessionUserScope}), and the active organization id scoped to that
11
+ * user ({@link ActiveOrganizationStorage}).
12
+ *
13
+ * # objectui#5664 — why this file exists
14
+ *
15
+ * `auth-active-organization-id` was a single un-namespaced `localStorage` key,
16
+ * while its siblings in `@object-ui/app-shell` were already user-scoped
17
+ * (`objectui-recent-items:u:<id>`, `objectui-favorites:u:<id>`,
18
+ * `flow-palette-recents:u:<id>`). On a browser handed from one account to
19
+ * another the new session read the PREVIOUS user's organization id: the header
20
+ * workspace chip rendered the previous user's workspace, and — the consequence
21
+ * that is not cosmetic — the polluted org context suppressed
22
+ * `RequireOrganization`'s routing into the guided "Create your workspace"
23
+ * first-run flow. A brand-new user on a shared browser silently never got the
24
+ * new-user flow.
25
+ *
26
+ * The server side was verified clean while the card was written: with the
27
+ * stale id, `get-full-organization` and `set-active` both answer
28
+ * `403 USER_IS_NOT_A_MEMBER` and `sys_environment` lists zero rows. Nothing
29
+ * about row visibility rode on this; the damage was entirely in what the
30
+ * client believed about itself.
31
+ *
32
+ * # The three parts, and which one closes the class
33
+ *
34
+ * 1. The key is namespaced per user (below).
35
+ * 2. `set()` can no longer write the un-namespaced key at all — see
36
+ * {@link scopedActiveOrgKey}. Namespacing that keeps a bare-key fallback
37
+ * re-opens the defect the first time a write happens before the user id is
38
+ * known.
39
+ * 3. {@link SessionUserScope.adopt} drops the previous user's UI state
40
+ * WHOLESALE when the session user changes. That is the load-bearing part:
41
+ * (1) and (2) fix one key, while (3) is what keeps the NEXT un-namespaced
42
+ * key — one nobody has written yet — from re-opening the same class. It is
43
+ * an allowlist sweep for exactly that reason: a denylist of the keys we
44
+ * know about today would not cover a key added tomorrow.
45
+ *
46
+ * # Resolving the scope with no `await`
47
+ *
48
+ * The user id is NOT known from React state at the moment this storage is
49
+ * first read. `createAuthenticatedFetch` reads it on every request including
50
+ * the very first `get-session`, and `MetadataProvider` reads it synchronously
51
+ * at mount to scope its seed cache — both before `AuthProvider`'s async
52
+ * session chain has resolved anything. So the scope is resolved from
53
+ * {@link SESSION_USER_STORAGE_KEY}, an ordinary `localStorage` pointer the
54
+ * PREVIOUS page-load wrote: one synchronous read, no await, and correct for
55
+ * every boot of a browser that has held a session before.
56
+ *
57
+ * The window where that pointer is stale — a boot whose cookie session belongs
58
+ * to someone other than the pointer — is bounded by `adopt()` firing as soon
59
+ * as `get-session` answers, and it is the same window the `X-Tenant-ID` edge
60
+ * contract already documents as "the header may be absent or not yet
61
+ * authoritative" (objectui#5279, this package's README). Requests that leave
62
+ * inside it are scoped by the SESSION server-side, not by the header.
63
+ */
64
+ /**
65
+ * Pointer to the user whose client state this browser holds. Deliberately NOT
66
+ * user-scoped itself — it is the thing that makes user scoping resolvable, and
67
+ * it is overwritten rather than accumulated, so there is never more than one.
68
+ */
69
+ const SESSION_USER_STORAGE_KEY = 'auth-session-user-id';
70
+ /** Base name the per-user active-org key is derived from. */
71
+ const ACTIVE_ORG_BASE_KEY = 'auth-active-organization-id';
72
+ /**
73
+ * The pre-#5664 spelling — the same string, un-suffixed. Every browser that
74
+ * has run an older build still has a value under it, and that value is
75
+ * UNATTRIBUTABLE: nothing recorded whose org id it is. It is therefore deleted
76
+ * rather than migrated. Migrating it is precisely the defect (on a handed-over
77
+ * browser it would hand the previous user's org id to the new one), and
78
+ * keeping it would leave a live bare key for a future reader to find.
79
+ *
80
+ * Dropping it costs a signed-in user nothing durable: `AuthProvider`'s
81
+ * `refreshOrganizations` asks the SERVER for the active organization whenever
82
+ * the list is non-empty and no active org is held, including the ADR-0081
83
+ * single-membership repair. The server is the authority for this value; the
84
+ * client copy is a routing-header cache. One boot re-supplies it.
85
+ */
86
+ const LEGACY_ACTIVE_ORG_KEY = ACTIVE_ORG_BASE_KEY;
87
+ /**
88
+ * Keys that survive a change of session user, because they describe the DEVICE
89
+ * or the INCOMING session rather than the outgoing user's state.
90
+ *
91
+ * `auth-session-token` is spelled out rather than imported from
92
+ * `createAuthClient.ts` (where it is a module-private constant). The two ends
93
+ * are held together by BEHAVIOUR instead of by a shared symbol — the pin in
94
+ * `__tests__/sessionUserChangePurge-5664.test.tsx` fills it through the real
95
+ * `TokenStorage` and reads it back through `TokenStorage` after a purge, so a
96
+ * spelling that drifts on either side fails there rather than silently signing
97
+ * the incoming user out.
98
+ *
99
+ * `vite-ui-theme` is `@object-ui/app-shell`'s `ThemeProvider` default. An app
100
+ * that passes a custom `storageKey` loses its theme once on a user change and
101
+ * re-picks it on the next toggle — cosmetic and self-healing, which is the
102
+ * right side of an allowlist to err on.
103
+ */
104
+ const DEVICE_SCOPED_KEYS = new Set([
105
+ 'auth-session-token',
106
+ SESSION_USER_STORAGE_KEY,
107
+ 'vite-ui-theme',
108
+ ]);
109
+ /**
110
+ * `localStorage` / `sessionStorage`, or `undefined` where they are absent or
111
+ * blocked. Accessing the global itself can throw (partitioned iframes, some
112
+ * privacy modes), so the guard has to be a try/catch and not a `typeof` test
113
+ * alone.
114
+ */
115
+ function safeStore(kind) {
116
+ try {
117
+ if (kind === 'local') {
118
+ return typeof localStorage !== 'undefined' ? localStorage : undefined;
119
+ }
120
+ return typeof sessionStorage !== 'undefined' ? sessionStorage : undefined;
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ }
126
+ function readPersisted(key) {
127
+ try {
128
+ return safeStore('local')?.getItem(key) ?? null;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ function writePersisted(key, value) {
135
+ try {
136
+ safeStore('local')?.setItem(key, value);
137
+ }
138
+ catch {
139
+ /* SSR / quota / private browsing — the caller keeps a memory copy */
140
+ }
141
+ }
142
+ /**
143
+ * Remove `key`, and report whether it is gone — `true` when a read-back can no
144
+ * longer produce a value for it.
145
+ *
146
+ * ## Why the verdict is a READ-BACK and not "did `removeItem` throw"
147
+ * (objectui#5731)
148
+ *
149
+ * `get()` can only answer with what `getItem` hands back, so "did this removal
150
+ * stick" is exactly the question "is the key still readable". Deciding it from
151
+ * the throw instead would be both too narrow and too wide:
152
+ *
153
+ * - TOO NARROW. A wrapped or proxied `localStorage` — an extension, a
154
+ * polyfill — whose `removeItem` is a silent no-op never throws, and leaves
155
+ * exactly the residue this guards against. objectui#5731 named the throwing
156
+ * variant; the no-op variant is the same defect, and a read-back catches
157
+ * both without having to enumerate them.
158
+ * - TOO WIDE. In SSR, and in a partitioned iframe where every operation
159
+ * throws, there is nothing readable to resurrect and the paths are already
160
+ * safe. A throw-based verdict would report those two states as failures,
161
+ * which is how a report earns a reputation for crying wolf.
162
+ *
163
+ * The throw is therefore still swallowed here: it is not the verdict.
164
+ */
165
+ function removePersisted(key) {
166
+ try {
167
+ safeStore('local')?.removeItem(key);
168
+ }
169
+ catch {
170
+ /* not the verdict — the read-back below is */
171
+ }
172
+ return readPersisted(key) === null;
173
+ }
174
+ /**
175
+ * Delete every entry in `store` that is not device-scoped.
176
+ *
177
+ * Snapshot the keys first (`Object.keys`) — removing entries during a live
178
+ * index walk shifts the ones behind it and skips half of them. Same idiom as
179
+ * `AuthProvider`'s sign-out purge loop.
180
+ *
181
+ * ## The `try` guards the SNAPSHOT, not the walk (objectui#5763)
182
+ *
183
+ * `Object.keys(store)` can itself throw — partitioned iframes, some privacy
184
+ * modes — and that is the reason a guard exists here at all, so it stays.
185
+ * What it must NOT do is wrap the loop: a `removeItem` that throws on key `n`
186
+ * would abort the walk, leaving keys `n+1..end` unswept, with the failure
187
+ * swallowed so the caller believes the purge completed. This is an ALLOWLIST
188
+ * sweep (see the module doc, part 3) precisely so the next un-namespaced key
189
+ * cannot re-open the cross-user pollution class — a partial sweep is a
190
+ * partial allowlist. So each `removeItem` gets its own `try`: one
191
+ * uncooperative key costs exactly that key.
192
+ *
193
+ * ## Reported, not quarantined — unlike {@link ActiveOrganizationStorage.clear}
194
+ *
195
+ * `clear()` (objectui#5731) judges a removal by READ-BACK and quarantines a
196
+ * key that fails, because it owns every future read of that one key through
197
+ * {@link ActiveOrganizationStorage.get} — the quarantine is what keeps a
198
+ * failed `clear()` from handing the value straight back. `sweepStore` walks
199
+ * keys it does not own reads for (another package's recents cache, a
200
+ * metadata seed) — there is no `get()` here to guard, so there is nothing to
201
+ * quarantine, and adding a read-back verdict for keys this function does not
202
+ * otherwise touch would be exactly the "general storage-error-handling
203
+ * refactor" the card scopes this fix away from. What IS mirrored is the
204
+ * reporting channel: a key whose `removeItem` throws is named in a
205
+ * `console.warn`, same as `clear()`, so a partial sweep is discoverable
206
+ * instead of silent — the caller (`SessionUserScope.adopt`, on the sign-in
207
+ * path) still cannot act on it and must not throw either.
208
+ */
209
+ function sweepStore(store) {
210
+ if (!store)
211
+ return;
212
+ let keys;
213
+ try {
214
+ keys = Object.keys(store);
215
+ }
216
+ catch {
217
+ return; /* storage unavailable */
218
+ }
219
+ const unswept = [];
220
+ for (const key of keys) {
221
+ if (DEVICE_SCOPED_KEYS.has(key))
222
+ continue;
223
+ try {
224
+ store.removeItem(key);
225
+ }
226
+ catch {
227
+ unswept.push(key);
228
+ }
229
+ }
230
+ if (unswept.length > 0) {
231
+ console.warn(`[purgePreviousUserClientState] could not remove ${unswept.length} key(s) from storage: ` +
232
+ `${unswept.join(', ')}. The previous user's state may still be readable under these keys.`);
233
+ }
234
+ }
235
+ /**
236
+ * Drop the outgoing user's client state wholesale — part 3 of the fix.
237
+ *
238
+ * Order is load-bearing and mirrors `clear()`'s: the in-memory active org goes
239
+ * FIRST, because `ActiveOrganizationStorage.get()` falls through to
240
+ * `_memoryValue` whenever the persisted read is null, which is exactly the
241
+ * state this function leaves behind (objectui#5703). Sweeping storage first
242
+ * would leave the outgoing user's org id live in memory for the rest of the
243
+ * page-load — the SPA sign-out-then-sign-in path never reloads.
244
+ *
245
+ * Both stores are swept. `sessionStorage` is per-TAB, not per-session, so the
246
+ * previous user's `objectui:metadata:*` seed (their PERMISSION-FILTERED app
247
+ * list) and their `objectui-ctx-*` scope picks survive into whoever signs in
248
+ * next in that tab unless something removes them.
249
+ */
250
+ export function purgePreviousUserClientState() {
251
+ ActiveOrganizationStorage.clear();
252
+ sweepStore(safeStore('local'));
253
+ sweepStore(safeStore('session'));
254
+ }
255
+ /**
256
+ * Listeners told that {@link SessionUserScope.adopt} dropped a previous
257
+ * owner's state (objectui#10193).
258
+ */
259
+ const ownerChangeListeners = new Set();
260
+ /**
261
+ * How many times THIS page-load has dropped a previous owner's state.
262
+ *
263
+ * In memory on purpose, and never persisted: the question it answers is "has a
264
+ * purge happened since this code started running", and a storage copy would
265
+ * answer it for a page-load that no longer exists. ⛔ It is not a storage key,
266
+ * and the ruling on objectui#10193 refuses one.
267
+ */
268
+ let ownerChangeCount = 0;
269
+ /**
270
+ * The number of owner changes {@link SessionUserScope.adopt} has performed in
271
+ * this page-load — `0` until a DIFFERENT user than the one the browser
272
+ * previously held is adopted.
273
+ *
274
+ * ## Why a count and not only an event (objectui#10193)
275
+ *
276
+ * The purge sweeps the previous owner's STORAGE, but state that was already
277
+ * derived from that storage before the purge — the UI language the boot
278
+ * resolved out of the previous owner's language slots, above all — lives in
279
+ * memory and is untouched by it. Whoever owns such state has to re-derive it,
280
+ * and the component that does may mount AFTER the purge happened (the shell
281
+ * mounts behind the session gate, the purge fires as the session resolves). A
282
+ * bare event would be missed by it; a count lets a late reader compare against
283
+ * what it has already handled. Shaped for React's `useSyncExternalStore`
284
+ * together with {@link subscribeSessionOwnerChange}.
285
+ */
286
+ export function getSessionOwnerChangeCount() {
287
+ return ownerChangeCount;
288
+ }
289
+ /**
290
+ * Be told each time {@link SessionUserScope.adopt} drops a previous owner's
291
+ * state. Returns the unsubscribe function. A listener that throws is reported
292
+ * and skipped, never allowed to break the sign-in path that called `adopt`.
293
+ */
294
+ export function subscribeSessionOwnerChange(listener) {
295
+ ownerChangeListeners.add(listener);
296
+ return () => {
297
+ ownerChangeListeners.delete(listener);
298
+ };
299
+ }
300
+ function notifyOwnerChange() {
301
+ ownerChangeCount += 1;
302
+ for (const listener of [...ownerChangeListeners]) {
303
+ try {
304
+ listener();
305
+ }
306
+ catch (err) {
307
+ console.warn('[SessionUserScope] an owner-change listener threw:', err);
308
+ }
309
+ }
310
+ }
311
+ /**
312
+ * Which user's client state this browser holds, and the transition that drops
313
+ * the previous user's state.
314
+ */
315
+ export const SessionUserScope = {
316
+ /**
317
+ * Memory copy of the pointer. Read FIRST by {@link current}: once this
318
+ * page-load has adopted a user, that answer is authoritative for this tab
319
+ * even if another tab has since written a different id, and it is the only
320
+ * copy in a browser whose `localStorage` rejects writes.
321
+ */
322
+ _userId: null,
323
+ /** The user whose state this browser holds, or `null` if it has never held one. */
324
+ current() {
325
+ if (this._userId !== null)
326
+ return this._userId;
327
+ return readPersisted(SESSION_USER_STORAGE_KEY);
328
+ },
329
+ /**
330
+ * Record `userId` as the owner of this browser's client state, dropping the
331
+ * previous owner's state wholesale when it changes.
332
+ *
333
+ * Returns `true` when it dropped a previous owner's state, `false`
334
+ * otherwise (same user, first-ever owner, empty id). A drop is also reported
335
+ * to {@link subscribeSessionOwnerChange} listeners and counted by
336
+ * {@link getSessionOwnerChangeCount} (objectui#10193): the purge clears
337
+ * storage, and anything already derived from that storage in memory is the
338
+ * listener's to re-derive.
339
+ *
340
+ * Idempotent: re-adopting the same user purges nothing. Called from
341
+ * `AuthProvider` for real sessions only — the synthetic `preview-user` /
342
+ * `guest` identities never reach it, so a preview mount cannot purge a real
343
+ * user's state.
344
+ *
345
+ * Because the decision reads the PERSISTED pointer, anything that deletes
346
+ * that pointer without deleting the state it describes would silently retire
347
+ * this invariant. `apps/console/src/lib/auth-preflight.ts` is the one place
348
+ * that sweeps auth keys before render, and it says so in its own comment:
349
+ * it removes every active-org spelling and leaves `auth-session-user-id`
350
+ * alone on purpose.
351
+ */
352
+ adopt(userId) {
353
+ if (!userId)
354
+ return false;
355
+ // Already this page-load's owner. Short-circuits before the storage read
356
+ // below, which matters in a browser that REJECTS WRITES: there the pointer
357
+ // write further down silently fails, so every later call would re-read a
358
+ // stale persisted owner and purge again.
359
+ if (this._userId === userId)
360
+ return false;
361
+ // WHOSE RESIDUE IS IN THIS STORE — read from storage, deliberately not
362
+ // from {@link current}. The two questions are different and take different
363
+ // authorities. `current()` answers "which key do I use right now", and
364
+ // memory is this tab's truth for that. This answers "is there another
365
+ // user's state PERSISTED here", and the persisted pointer is the only
366
+ // witness to that: a browser that never persisted anything has no residue
367
+ // to drop, and purging on the strength of an in-memory id would delete
368
+ // state that was written for the arriving user.
369
+ const persistedPrevious = readPersisted(SESSION_USER_STORAGE_KEY);
370
+ const ownerChanged = persistedPrevious !== null && persistedPrevious !== userId;
371
+ if (ownerChanged) {
372
+ purgePreviousUserClientState();
373
+ }
374
+ this._userId = userId;
375
+ writePersisted(SESSION_USER_STORAGE_KEY, userId);
376
+ // Unattributable legacy state, cleaned up on any adopt rather than only on
377
+ // a change — see LEGACY_ACTIVE_ORG_KEY.
378
+ removePersisted(LEGACY_ACTIVE_ORG_KEY);
379
+ // Last, so a listener that reads `current()` or the store sees the
380
+ // arriving owner and the swept storage, never a half-adopted state.
381
+ if (ownerChanged)
382
+ notifyOwnerChange();
383
+ return ownerChanged;
384
+ },
385
+ /**
386
+ * Forget the in-memory pointer. Test seam only — production has no reason to
387
+ * un-know who is signed in, and sign-out deliberately KEEPS the persisted
388
+ * pointer so that the next sign-in by a DIFFERENT user still sees a previous
389
+ * owner to purge.
390
+ */
391
+ _resetForTests() {
392
+ this._userId = null;
393
+ },
394
+ };
395
+ /**
396
+ * The `localStorage` key holding the active org id for the current user, or
397
+ * `null` when no user is known yet.
398
+ *
399
+ * `null` — not the bare base key — is the whole point. `@object-ui/app-shell`'s
400
+ * `scopedKey(base, userId)` falls back to the un-namespaced key for
401
+ * "no user id yet", and that is correct THERE: its callers persist recents and
402
+ * favourites, where a signed-out bucket leaking is cosmetic. Here the bare key
403
+ * IS the defect, so "no user id yet" means "there is no key to persist under"
404
+ * and the value lives in memory for this page-load only. The convention for
405
+ * the scoped half is copied exactly (`${base}:u:${userId}`) — the symbol
406
+ * cannot travel, because `@object-ui/app-shell` depends on `@object-ui/auth`
407
+ * and not the other way round.
408
+ */
409
+ function scopedActiveOrgKey() {
410
+ const userId = SessionUserScope.current();
411
+ return userId ? `${ACTIVE_ORG_BASE_KEY}:u:${userId}` : null;
412
+ }
413
+ /**
414
+ * Get/set the active organization id that {@link createAuthenticatedFetch}
415
+ * stamps as `X-Tenant-ID`.
416
+ *
417
+ * `AuthProvider` is the only writer, at four moments: `refreshOrganizations`
418
+ * sets it once the `getSession` -> `listOrganizations` ->
419
+ * `getActiveOrganization` chain resolves (including the ADR-0081
420
+ * single-membership repair), `switchOrganization` sets or clears it,
421
+ * `deleteOrganization` / `leaveOrganization` clear it when the active org is
422
+ * the one going away, and sign-out clears it.
423
+ *
424
+ * Because the first of those is asynchronous, this reads EMPTY for the first
425
+ * stretch of a boot, and every request that leaves in that window carries no
426
+ * tenant header at all. That window is a documented part of the header's
427
+ * contract, not an accident to paper over — see the "unstamped-first-request
428
+ * gap" section of this package's README (objectui#5279).
429
+ */
430
+ export const ActiveOrganizationStorage = {
431
+ _memoryValue: null,
432
+ /**
433
+ * Keys whose removal by {@link clear} did not stick — the value was still
434
+ * readable afterwards (objectui#5731).
435
+ *
436
+ * {@link get} refuses to answer from the persisted layer for a key in here,
437
+ * which is the mechanism that keeps a cleared organization cleared. Held in
438
+ * memory, so it lasts exactly one page-load: a browser that starts fresh
439
+ * re-measures rather than inheriting a verdict.
440
+ */
441
+ _unremovedKeys: new Set(),
442
+ /**
443
+ * The active org id for the CURRENT user, or `null`.
444
+ *
445
+ * READ ORDER — a non-null `localStorage` read wins; anything else falls
446
+ * through to `_memoryValue`. Returning the `localStorage` read
447
+ * UNCONDITIONALLY (what this did before objectui#5703) left the fallback
448
+ * reachable only when the read THREW, and there is a real browser state
449
+ * where the read does not throw and the fallback is still the only copy:
450
+ * `localStorage` present and readable but REJECTING WRITES — Safari private
451
+ * browsing, and any quota-exhausted origin, where `setItem` throws
452
+ * `QuotaExceededError`. `set()` swallows that failure into `_memoryValue`
453
+ * (correctly — the fallback is right there), and `get()` then never
454
+ * consulted it: the value was stored and could not be read back, so
455
+ * `X-Tenant-ID` went unstamped for the whole session, not just the early
456
+ * requests.
457
+ *
458
+ * WHY FALLING BACK ON `null` DOES NOT RESURRECT A CLEARED ORG. After
459
+ * `clear()` the `localStorage` read is null by construction, so this
460
+ * fallback fires — and it must answer `null`, because sign-out is one of
461
+ * `clear()`'s callers. It does, because `clear()` nulls `_memoryValue` too.
462
+ * That property is what makes this read order safe rather than an
463
+ * incidental detail of `clear()`'s body, so it is pinned by test
464
+ * (`__tests__/activeOrgStorageFallback-5703.test.tsx`) instead of being
465
+ * re-derived by whoever edits `clear()` next.
466
+ *
467
+ * The same reasoning is why a change of session user purges through
468
+ * {@link purgePreviousUserClientState} rather than by removing keys: the
469
+ * outgoing user's id in `_memoryValue` would otherwise outlive their
470
+ * persisted key on the SPA sign-out-then-sign-in path (objectui#5664).
471
+ */
472
+ get() {
473
+ const key = scopedActiveOrgKey();
474
+ // A key {@link clear} could not remove is QUARANTINED for the rest of this
475
+ // page-load, and skipping the persisted branch is what stops the cleared
476
+ // org from being handed straight back (objectui#5731). `_memoryValue` then
477
+ // answers, and for that key it is the only copy this page-load can trust:
478
+ // `clear()` nulled it, and a later `set()` refills it with the value that
479
+ // write was meant to persist — so this stays correct in both directions
480
+ // with no release step of its own. What it gives up is cross-tab freshness
481
+ // for one key, in a browser that has just demonstrated it cannot delete
482
+ // from storage. An unstamped `X-Tenant-ID` is a documented state of the
483
+ // edge contract (see `createAuthenticatedFetch`); a re-stamped signed-out
484
+ // org is not.
485
+ if (key && !this._unremovedKeys.has(key)) {
486
+ try {
487
+ const persisted = safeStore('local')?.getItem(key) ?? null;
488
+ if (persisted !== null)
489
+ return persisted;
490
+ }
491
+ catch { /* SSR / test */ }
492
+ }
493
+ return this._memoryValue;
494
+ },
495
+ /**
496
+ * With no session user known this writes to memory ONLY. That is not a
497
+ * degraded path to apologise for — it is the fix: a persisted write with no
498
+ * owner is the un-namespaced key this card retired.
499
+ */
500
+ set(orgId) {
501
+ this._memoryValue = orgId;
502
+ const key = scopedActiveOrgKey();
503
+ if (!key)
504
+ return;
505
+ writePersisted(key, orgId);
506
+ },
507
+ /**
508
+ * Drop the active organization, from memory and from the persisted key.
509
+ *
510
+ * POSTCONDITION: {@link get} answers `null` afterwards. Sign-out is one of
511
+ * the callers, so that is a security-relevant guarantee rather than a
512
+ * convenience — and before objectui#5731 it was not one, because a
513
+ * `removeItem` that failed was swallowed and the surviving key won `get()`'s
514
+ * read order.
515
+ *
516
+ * ## Why a failed removal is neither thrown nor returned (objectui#5731)
517
+ *
518
+ * Every caller arrives here AFTER the transition it is following up on has
519
+ * already happened, and not one of them can act on a storage failure:
520
+ *
521
+ * - `AuthProvider`'s `purgeSignedOutClientCaches` — the session is already
522
+ * ended. Sign-out cannot be refused because a key would not delete.
523
+ * - `switchOrganization` — the server already switched or cleared the
524
+ * active org; a throw would report a successful switch as a failure and
525
+ * route the caller into its error branch.
526
+ * - `deleteOrganization` / `leaveOrganization` — the org is already deleted
527
+ * or already left.
528
+ * - {@link purgePreviousUserClientState}, via {@link SessionUserScope.adopt}
529
+ * — runs on the SIGN-IN path inside an `AuthProvider` effect, where a
530
+ * throw breaks the boot of the ARRIVING user.
531
+ *
532
+ * A `boolean` return only moves the problem one step: all five call sites
533
+ * would have nothing to write in the failure branch, and a return value that
534
+ * every caller ignores reads as handled when it is not. So the invariant is
535
+ * restored HERE, where it can be, and the failure is reported to the console,
536
+ * where a human can find it.
537
+ *
538
+ * The third shape considered and rejected was re-writing the key with an
539
+ * empty value. That relocates the fix into every consumer's truthiness test
540
+ * (`createAuthenticatedFetch` does `if (activeOrgId)`), which is the lenient
541
+ * consumer AGENTS.md #0.1 forbids, and it leaves a signed-out browser holding
542
+ * a live key.
543
+ */
544
+ clear() {
545
+ // Nulling the fallback is SECURITY-RELEVANT, not bookkeeping: `get()`
546
+ // falls through to `_memoryValue` whenever the `localStorage` read comes
547
+ // back null, which is exactly the state this method leaves behind. Drop
548
+ // this line and sign-out's clear stops sticking — the removed key reads
549
+ // null, the fallback fires, and the cleared org goes back on the wire as
550
+ // `X-Tenant-ID`. Pinned by `__tests__/activeOrgStorageFallback-5703.test.tsx`.
551
+ this._memoryValue = null;
552
+ const key = scopedActiveOrgKey();
553
+ if (key) {
554
+ if (removePersisted(key)) {
555
+ // Confirmed gone. Recomputed on every call rather than left alone, so
556
+ // a key quarantined by an earlier failure is RELEASED the moment a
557
+ // removal sticks: the quarantine describes the last attempt, not a
558
+ // permanent verdict on the browser.
559
+ this._unremovedKeys.delete(key);
560
+ }
561
+ else {
562
+ // The removal did not stick and the value is still readable, so
563
+ // `get()` would hand the signed-out org straight back. Two things
564
+ // happen here and they are deliberately separable: the stale read is
565
+ // SUPPRESSED, which restores the postcondition without any caller's
566
+ // help, and the failure is REPORTED, because "sign-out did not fully
567
+ // stick" is something whoever is reading a console has to be able to
568
+ // find. Neither half substitutes for the other — a report alone leaves
569
+ // the org on the wire, and suppression alone is the same silence this
570
+ // card was filed about, just with a better outcome.
571
+ this._unremovedKeys.add(key);
572
+ console.warn(`[ActiveOrganizationStorage] clear() could not remove "${key}" from localStorage — ` +
573
+ 'it is still readable. Reads for it are answered from memory for the rest of this ' +
574
+ 'page-load, so the cleared organization is not re-stamped as X-Tenant-ID.');
575
+ }
576
+ }
577
+ // Also drop the pre-#5664 bare key, so a `clear()` on a browser upgrading
578
+ // from an older build leaves nothing behind under the retired spelling.
579
+ // Its verdict is deliberately ignored: `scopedActiveOrgKey()` never returns
580
+ // the bare spelling, so nothing reads it and a survival here cannot
581
+ // resurrect an org through `get()`.
582
+ removePersisted(LEGACY_ACTIVE_ORG_KEY);
583
+ },
584
+ };
@@ -5,7 +5,7 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import type { AuthUser, AuthClientSession, PreviewModeOptions, AuthOrganization, AuthOrganizationMember, AuthInvitation, AuthPublicConfig, SignInWithProviderOptions, DelegableScope } from './types';
8
+ import type { AuthUser, AuthClientSession, PreviewModeOptions, AuthOrganization, AuthOrganizationMember, AuthInvitation, AuthPublicConfig, SignInWithProviderOptions, DelegableScope } from './types.js';
9
9
  export interface AuthContextValue {
10
10
  /** Current authenticated user */
11
11
  user: AuthUser | null;
@@ -33,8 +33,10 @@ export interface AuthContextValue {
33
33
  /** Sign up with name, email, and password.
34
34
  * Returns `{ requiresVerification: true }` when the server accepted the
35
35
  * account but is gating sign-in on email verification; callers should
36
- * show a "check your inbox" UI instead of navigating to a protected page. */
37
- signUp: (name: string, email: string, password: string) => Promise<{
36
+ * show a "check your inbox" UI instead of navigating to a protected page.
37
+ * `callbackURL` is where the email-verification link lands — see
38
+ * `SignUpData.callbackURL` for the shape the server accepts. */
39
+ signUp: (name: string, email: string, password: string, callbackURL?: string) => Promise<{
38
40
  requiresVerification: boolean;
39
41
  }>;
40
42
  /** Sign out the current user */
@@ -121,6 +123,24 @@ export interface AuthContextValue {
121
123
  activeMember: AuthOrganizationMember | null;
122
124
  /** Whether organizations are loading */
123
125
  isOrganizationsLoading: boolean;
126
+ /**
127
+ * objectui#5619 — whether `organizations` / `activeOrganization` /
128
+ * `activeMember` MEAN what they say yet.
129
+ *
130
+ * Those three are `[]` / `null` / `null` both before the pipeline starts and
131
+ * after it finds nothing, and `isOrganizationsLoading` is `false` on both
132
+ * sides of the request — so no combination of the fields above can tell
133
+ * "not asked yet" from "asked, and there is none". That ambiguity is what let
134
+ * `useWorkspaceAdminStatus` answer "not an admin" about an administrator
135
+ * whose adminship lives only in the member row.
136
+ *
137
+ * True once the pipeline has reached a terminal state: the org list came back
138
+ * AND the active-member row either landed, failed, or was established not to
139
+ * exist (no active organization). Also true immediately where there is no
140
+ * pipeline to run at all — preview / auth-disabled mode, and outside an
141
+ * AuthProvider — so a gate on this can never hang.
142
+ */
143
+ isMembershipResolved: boolean;
124
144
  /** Switch the active organization (workspace) */
125
145
  switchOrganization: (orgId: string) => Promise<void>;
126
146
  /** Create a new organization */
@@ -1 +1 @@
1
- {"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../src/AuthContext.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../src/AuthContext.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,cAAc,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEzM,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,kCAAkC;IAClC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAClC,wCAAwC;IACxC,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,oCAAoC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,iDAAiD;IACjD,aAAa,EAAE,OAAO,CAAC;IACvB,uEAAuE;IACvE,WAAW,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACvC,sCAAsC;IACtC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;;;qEAKiE;IACjE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,oBAAoB,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5H,gCAAgC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,0BAA0B;IAC1B,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,6BAA6B;IAC7B,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,yEAAyE;IACzE,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,gCAAgC;IAChC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,uEAAuE;IACvE,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,yDAAyD;IACzD,kBAAkB,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,+EAA+E;IAC/E,uBAAuB,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,8EAA8E;IAC9E,yBAAyB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,4DAA4D;IAC5D,yBAAyB,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpG;;;OAGG;IACH,cAAc,EAAE,CAAC,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,mBAAmB,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7H;;;;OAIG;IACH,mBAAmB,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9D,2EAA2E;IAC3E,sBAAsB,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACjF,gFAAgF;IAChF,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACtF,oEAAoE;IACpE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,kBAAkB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,sEAAsE;IACtE,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,gEAAgE;IAChE,aAAa,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/C,gFAAgF;IAChF,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAI/F,4CAA4C;IAC5C,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,oCAAoC;IACpC,kBAAkB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC5C,8HAA8H;IAC9H,YAAY,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC5C,wCAAwC;IACxC,sBAAsB,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,EAAE,OAAO,CAAC;IAC9B,iDAAiD;IACjD,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,gCAAgC;IAChC,kBAAkB,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACvG;;;;OAIG;IACH,oBAAoB,EAAE,MAAM,OAAO,CAAC,gBAAgB,EAAE,GAAG,SAAS,CAAC,CAAC;IACpE,gDAAgD;IAChD,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC,CAAC,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/I,qCAAqC;IACrC,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iDAAiD;IACjD,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAIpD,sCAAsC;IACtC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACjE;;;;OAIG;IACH,YAAY,EAAE,CAAC,IAAI,EAAE;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACtB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC9B;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC7D,4BAA4B;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5F,6BAA6B;IAC7B,gBAAgB,EAAE,CAAC,IAAI,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAItG,mDAAmD;IACnD,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC9D,2BAA2B;IAC3B,gBAAgB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,mCAAmC;IACnC,aAAa,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACjE,+CAA+C;IAC/C,gBAAgB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,+CAA+C;IAC/C,gBAAgB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,qDAAqD;IACrD,mBAAmB,EAAE,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CACtD;AAED,eAAO,MAAM,OAAO,kDAA+C,CAAC"}
@@ -9,7 +9,14 @@ import React from 'react';
9
9
  export interface AuthGuardProps {
10
10
  /** Content to render when user is not authenticated */
11
11
  fallback?: React.ReactNode;
12
- /** Required roles (user must have at least one) */
12
+ /**
13
+ * Required position/role names — the user must hold at least one.
14
+ *
15
+ * Matched against `user.positions`, the one published spelling (framework
16
+ * ADR-0090 D3), falling back to the single `user.role` scalar only when the
17
+ * session carries no `positions` key at all (synthetic guest / legacy
18
+ * identities). The retired `roles` spelling is not read.
19
+ */
13
20
  requiredRoles?: string[];
14
21
  /** Required permissions (user must have all) */
15
22
  requiredPermissions?: string[];