@object-ui/auth 17.3.0 → 17.6.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 (49) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +184 -13
  3. package/dist/ActiveOrganizationStorage.d.ts +168 -0
  4. package/dist/ActiveOrganizationStorage.d.ts.map +1 -0
  5. package/dist/ActiveOrganizationStorage.js +515 -0
  6. package/dist/AuthContext.d.ts +34 -1
  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 +359 -32
  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 +1 -1
  18. package/dist/LoginForm.d.ts.map +1 -1
  19. package/dist/LoginForm.js +4 -4
  20. package/dist/PreviewBanner.js +1 -1
  21. package/dist/RegisterForm.d.ts +1 -1
  22. package/dist/RegisterForm.d.ts.map +1 -1
  23. package/dist/RegisterForm.js +3 -3
  24. package/dist/SocialSignInButtons.js +2 -2
  25. package/dist/UserMenu.js +2 -2
  26. package/dist/createAuthClient.d.ts +23 -1
  27. package/dist/createAuthClient.d.ts.map +1 -1
  28. package/dist/createAuthClient.js +112 -25
  29. package/dist/createAuthenticatedFetch.d.ts +9 -9
  30. package/dist/createAuthenticatedFetch.d.ts.map +1 -1
  31. package/dist/createAuthenticatedFetch.js +82 -37
  32. package/dist/index.d.ts +20 -19
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +28 -17
  35. package/dist/invitation-status.d.ts +59 -0
  36. package/dist/invitation-status.d.ts.map +1 -0
  37. package/dist/invitation-status.js +32 -0
  38. package/dist/types.d.ts +52 -4
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/useAuth.d.ts +1 -1
  41. package/dist/useAuth.d.ts.map +1 -1
  42. package/dist/useAuth.js +6 -1
  43. package/dist/useWorkspaceAdminStatus.d.ts +85 -0
  44. package/dist/useWorkspaceAdminStatus.d.ts.map +1 -0
  45. package/dist/useWorkspaceAdminStatus.js +102 -0
  46. package/package.json +5 -5
  47. package/dist/useIsWorkspaceAdmin.d.ts +0 -21
  48. package/dist/useIsWorkspaceAdmin.d.ts.map +0 -1
  49. package/dist/useIsWorkspaceAdmin.js +0 -47
@@ -0,0 +1,515 @@
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
+ * Which user's client state this browser holds, and the transition that drops
257
+ * the previous user's state.
258
+ */
259
+ export const SessionUserScope = {
260
+ /**
261
+ * Memory copy of the pointer. Read FIRST by {@link current}: once this
262
+ * page-load has adopted a user, that answer is authoritative for this tab
263
+ * even if another tab has since written a different id, and it is the only
264
+ * copy in a browser whose `localStorage` rejects writes.
265
+ */
266
+ _userId: null,
267
+ /** The user whose state this browser holds, or `null` if it has never held one. */
268
+ current() {
269
+ if (this._userId !== null)
270
+ return this._userId;
271
+ return readPersisted(SESSION_USER_STORAGE_KEY);
272
+ },
273
+ /**
274
+ * Record `userId` as the owner of this browser's client state, dropping the
275
+ * previous owner's state wholesale when it changes.
276
+ *
277
+ * Idempotent: re-adopting the same user purges nothing. Called from
278
+ * `AuthProvider` for real sessions only — the synthetic `preview-user` /
279
+ * `guest` identities never reach it, so a preview mount cannot purge a real
280
+ * user's state.
281
+ *
282
+ * Because the decision reads the PERSISTED pointer, anything that deletes
283
+ * that pointer without deleting the state it describes would silently retire
284
+ * this invariant. `apps/console/src/lib/auth-preflight.ts` is the one place
285
+ * that sweeps auth keys before render, and it says so in its own comment:
286
+ * it removes every active-org spelling and leaves `auth-session-user-id`
287
+ * alone on purpose.
288
+ */
289
+ adopt(userId) {
290
+ if (!userId)
291
+ return;
292
+ // Already this page-load's owner. Short-circuits before the storage read
293
+ // below, which matters in a browser that REJECTS WRITES: there the pointer
294
+ // write further down silently fails, so every later call would re-read a
295
+ // stale persisted owner and purge again.
296
+ if (this._userId === userId)
297
+ return;
298
+ // WHOSE RESIDUE IS IN THIS STORE — read from storage, deliberately not
299
+ // from {@link current}. The two questions are different and take different
300
+ // authorities. `current()` answers "which key do I use right now", and
301
+ // memory is this tab's truth for that. This answers "is there another
302
+ // user's state PERSISTED here", and the persisted pointer is the only
303
+ // witness to that: a browser that never persisted anything has no residue
304
+ // to drop, and purging on the strength of an in-memory id would delete
305
+ // state that was written for the arriving user.
306
+ const persistedPrevious = readPersisted(SESSION_USER_STORAGE_KEY);
307
+ if (persistedPrevious !== null && persistedPrevious !== userId) {
308
+ purgePreviousUserClientState();
309
+ }
310
+ this._userId = userId;
311
+ writePersisted(SESSION_USER_STORAGE_KEY, userId);
312
+ // Unattributable legacy state, cleaned up on any adopt rather than only on
313
+ // a change — see LEGACY_ACTIVE_ORG_KEY.
314
+ removePersisted(LEGACY_ACTIVE_ORG_KEY);
315
+ },
316
+ /**
317
+ * Forget the in-memory pointer. Test seam only — production has no reason to
318
+ * un-know who is signed in, and sign-out deliberately KEEPS the persisted
319
+ * pointer so that the next sign-in by a DIFFERENT user still sees a previous
320
+ * owner to purge.
321
+ */
322
+ _resetForTests() {
323
+ this._userId = null;
324
+ },
325
+ };
326
+ /**
327
+ * The `localStorage` key holding the active org id for the current user, or
328
+ * `null` when no user is known yet.
329
+ *
330
+ * `null` — not the bare base key — is the whole point. `@object-ui/app-shell`'s
331
+ * `scopedKey(base, userId)` falls back to the un-namespaced key for
332
+ * "no user id yet", and that is correct THERE: its callers persist recents and
333
+ * favourites, where a signed-out bucket leaking is cosmetic. Here the bare key
334
+ * IS the defect, so "no user id yet" means "there is no key to persist under"
335
+ * and the value lives in memory for this page-load only. The convention for
336
+ * the scoped half is copied exactly (`${base}:u:${userId}`) — the symbol
337
+ * cannot travel, because `@object-ui/app-shell` depends on `@object-ui/auth`
338
+ * and not the other way round.
339
+ */
340
+ function scopedActiveOrgKey() {
341
+ const userId = SessionUserScope.current();
342
+ return userId ? `${ACTIVE_ORG_BASE_KEY}:u:${userId}` : null;
343
+ }
344
+ /**
345
+ * Get/set the active organization id that {@link createAuthenticatedFetch}
346
+ * stamps as `X-Tenant-ID`.
347
+ *
348
+ * `AuthProvider` is the only writer, at four moments: `refreshOrganizations`
349
+ * sets it once the `getSession` -> `listOrganizations` ->
350
+ * `getActiveOrganization` chain resolves (including the ADR-0081
351
+ * single-membership repair), `switchOrganization` sets or clears it,
352
+ * `deleteOrganization` / `leaveOrganization` clear it when the active org is
353
+ * the one going away, and sign-out clears it.
354
+ *
355
+ * Because the first of those is asynchronous, this reads EMPTY for the first
356
+ * stretch of a boot, and every request that leaves in that window carries no
357
+ * tenant header at all. That window is a documented part of the header's
358
+ * contract, not an accident to paper over — see the "unstamped-first-request
359
+ * gap" section of this package's README (objectui#5279).
360
+ */
361
+ export const ActiveOrganizationStorage = {
362
+ _memoryValue: null,
363
+ /**
364
+ * Keys whose removal by {@link clear} did not stick — the value was still
365
+ * readable afterwards (objectui#5731).
366
+ *
367
+ * {@link get} refuses to answer from the persisted layer for a key in here,
368
+ * which is the mechanism that keeps a cleared organization cleared. Held in
369
+ * memory, so it lasts exactly one page-load: a browser that starts fresh
370
+ * re-measures rather than inheriting a verdict.
371
+ */
372
+ _unremovedKeys: new Set(),
373
+ /**
374
+ * The active org id for the CURRENT user, or `null`.
375
+ *
376
+ * READ ORDER — a non-null `localStorage` read wins; anything else falls
377
+ * through to `_memoryValue`. Returning the `localStorage` read
378
+ * UNCONDITIONALLY (what this did before objectui#5703) left the fallback
379
+ * reachable only when the read THREW, and there is a real browser state
380
+ * where the read does not throw and the fallback is still the only copy:
381
+ * `localStorage` present and readable but REJECTING WRITES — Safari private
382
+ * browsing, and any quota-exhausted origin, where `setItem` throws
383
+ * `QuotaExceededError`. `set()` swallows that failure into `_memoryValue`
384
+ * (correctly — the fallback is right there), and `get()` then never
385
+ * consulted it: the value was stored and could not be read back, so
386
+ * `X-Tenant-ID` went unstamped for the whole session, not just the early
387
+ * requests.
388
+ *
389
+ * WHY FALLING BACK ON `null` DOES NOT RESURRECT A CLEARED ORG. After
390
+ * `clear()` the `localStorage` read is null by construction, so this
391
+ * fallback fires — and it must answer `null`, because sign-out is one of
392
+ * `clear()`'s callers. It does, because `clear()` nulls `_memoryValue` too.
393
+ * That property is what makes this read order safe rather than an
394
+ * incidental detail of `clear()`'s body, so it is pinned by test
395
+ * (`__tests__/activeOrgStorageFallback-5703.test.tsx`) instead of being
396
+ * re-derived by whoever edits `clear()` next.
397
+ *
398
+ * The same reasoning is why a change of session user purges through
399
+ * {@link purgePreviousUserClientState} rather than by removing keys: the
400
+ * outgoing user's id in `_memoryValue` would otherwise outlive their
401
+ * persisted key on the SPA sign-out-then-sign-in path (objectui#5664).
402
+ */
403
+ get() {
404
+ const key = scopedActiveOrgKey();
405
+ // A key {@link clear} could not remove is QUARANTINED for the rest of this
406
+ // page-load, and skipping the persisted branch is what stops the cleared
407
+ // org from being handed straight back (objectui#5731). `_memoryValue` then
408
+ // answers, and for that key it is the only copy this page-load can trust:
409
+ // `clear()` nulled it, and a later `set()` refills it with the value that
410
+ // write was meant to persist — so this stays correct in both directions
411
+ // with no release step of its own. What it gives up is cross-tab freshness
412
+ // for one key, in a browser that has just demonstrated it cannot delete
413
+ // from storage. An unstamped `X-Tenant-ID` is a documented state of the
414
+ // edge contract (see `createAuthenticatedFetch`); a re-stamped signed-out
415
+ // org is not.
416
+ if (key && !this._unremovedKeys.has(key)) {
417
+ try {
418
+ const persisted = safeStore('local')?.getItem(key) ?? null;
419
+ if (persisted !== null)
420
+ return persisted;
421
+ }
422
+ catch { /* SSR / test */ }
423
+ }
424
+ return this._memoryValue;
425
+ },
426
+ /**
427
+ * With no session user known this writes to memory ONLY. That is not a
428
+ * degraded path to apologise for — it is the fix: a persisted write with no
429
+ * owner is the un-namespaced key this card retired.
430
+ */
431
+ set(orgId) {
432
+ this._memoryValue = orgId;
433
+ const key = scopedActiveOrgKey();
434
+ if (!key)
435
+ return;
436
+ writePersisted(key, orgId);
437
+ },
438
+ /**
439
+ * Drop the active organization, from memory and from the persisted key.
440
+ *
441
+ * POSTCONDITION: {@link get} answers `null` afterwards. Sign-out is one of
442
+ * the callers, so that is a security-relevant guarantee rather than a
443
+ * convenience — and before objectui#5731 it was not one, because a
444
+ * `removeItem` that failed was swallowed and the surviving key won `get()`'s
445
+ * read order.
446
+ *
447
+ * ## Why a failed removal is neither thrown nor returned (objectui#5731)
448
+ *
449
+ * Every caller arrives here AFTER the transition it is following up on has
450
+ * already happened, and not one of them can act on a storage failure:
451
+ *
452
+ * - `AuthProvider`'s `purgeSignedOutClientCaches` — the session is already
453
+ * ended. Sign-out cannot be refused because a key would not delete.
454
+ * - `switchOrganization` — the server already switched or cleared the
455
+ * active org; a throw would report a successful switch as a failure and
456
+ * route the caller into its error branch.
457
+ * - `deleteOrganization` / `leaveOrganization` — the org is already deleted
458
+ * or already left.
459
+ * - {@link purgePreviousUserClientState}, via {@link SessionUserScope.adopt}
460
+ * — runs on the SIGN-IN path inside an `AuthProvider` effect, where a
461
+ * throw breaks the boot of the ARRIVING user.
462
+ *
463
+ * A `boolean` return only moves the problem one step: all five call sites
464
+ * would have nothing to write in the failure branch, and a return value that
465
+ * every caller ignores reads as handled when it is not. So the invariant is
466
+ * restored HERE, where it can be, and the failure is reported to the console,
467
+ * where a human can find it.
468
+ *
469
+ * The third shape considered and rejected was re-writing the key with an
470
+ * empty value. That relocates the fix into every consumer's truthiness test
471
+ * (`createAuthenticatedFetch` does `if (activeOrgId)`), which is the lenient
472
+ * consumer AGENTS.md #0.1 forbids, and it leaves a signed-out browser holding
473
+ * a live key.
474
+ */
475
+ clear() {
476
+ // Nulling the fallback is SECURITY-RELEVANT, not bookkeeping: `get()`
477
+ // falls through to `_memoryValue` whenever the `localStorage` read comes
478
+ // back null, which is exactly the state this method leaves behind. Drop
479
+ // this line and sign-out's clear stops sticking — the removed key reads
480
+ // null, the fallback fires, and the cleared org goes back on the wire as
481
+ // `X-Tenant-ID`. Pinned by `__tests__/activeOrgStorageFallback-5703.test.tsx`.
482
+ this._memoryValue = null;
483
+ const key = scopedActiveOrgKey();
484
+ if (key) {
485
+ if (removePersisted(key)) {
486
+ // Confirmed gone. Recomputed on every call rather than left alone, so
487
+ // a key quarantined by an earlier failure is RELEASED the moment a
488
+ // removal sticks: the quarantine describes the last attempt, not a
489
+ // permanent verdict on the browser.
490
+ this._unremovedKeys.delete(key);
491
+ }
492
+ else {
493
+ // The removal did not stick and the value is still readable, so
494
+ // `get()` would hand the signed-out org straight back. Two things
495
+ // happen here and they are deliberately separable: the stale read is
496
+ // SUPPRESSED, which restores the postcondition without any caller's
497
+ // help, and the failure is REPORTED, because "sign-out did not fully
498
+ // stick" is something whoever is reading a console has to be able to
499
+ // find. Neither half substitutes for the other — a report alone leaves
500
+ // the org on the wire, and suppression alone is the same silence this
501
+ // card was filed about, just with a better outcome.
502
+ this._unremovedKeys.add(key);
503
+ console.warn(`[ActiveOrganizationStorage] clear() could not remove "${key}" from localStorage — ` +
504
+ 'it is still readable. Reads for it are answered from memory for the rest of this ' +
505
+ 'page-load, so the cleared organization is not re-stamped as X-Tenant-ID.');
506
+ }
507
+ }
508
+ // Also drop the pre-#5664 bare key, so a `clear()` on a browser upgrading
509
+ // from an older build leaves nothing behind under the retired spelling.
510
+ // Its verdict is deliberately ignored: `scopedActiveOrgKey()` never returns
511
+ // the bare spelling, so nothing reads it and a survival here cannot
512
+ // resurrect an org through `get()`.
513
+ removePersisted(LEGACY_ACTIVE_ORG_KEY);
514
+ },
515
+ };
@@ -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;
@@ -39,6 +39,21 @@ export interface AuthContextValue {
39
39
  }>;
40
40
  /** Sign out the current user */
41
41
  signOut: () => Promise<void>;
42
+ /**
43
+ * objectui#4467 — re-resolve `user`/`session` from the server, in place.
44
+ *
45
+ * The same loader the provider runs on mount, exposed for the transitions
46
+ * that change WHO the session is without going through `signIn`/`signOut`:
47
+ * starting and stopping impersonation. Deliberately does NOT raise
48
+ * `isLoading` — a refresh must not blank the console it is running under.
49
+ *
50
+ * Callers rarely need it: a session rotation observed on the wire refreshes
51
+ * identity on its own (see `TokenStorage.subscribeRotation`). Call it when
52
+ * you must AWAIT the new identity before deciding what to show — the
53
+ * impersonation banner's exit does, so a stop that did not restore the
54
+ * administrator can fail loudly instead of appearing to succeed.
55
+ */
56
+ refreshSession: () => Promise<void>;
42
57
  /** Update user profile */
43
58
  updateUser: (data: Partial<AuthUser>) => Promise<void>;
44
59
  /** Request password reset */
@@ -106,6 +121,24 @@ export interface AuthContextValue {
106
121
  activeMember: AuthOrganizationMember | null;
107
122
  /** Whether organizations are loading */
108
123
  isOrganizationsLoading: boolean;
124
+ /**
125
+ * objectui#5619 — whether `organizations` / `activeOrganization` /
126
+ * `activeMember` MEAN what they say yet.
127
+ *
128
+ * Those three are `[]` / `null` / `null` both before the pipeline starts and
129
+ * after it finds nothing, and `isOrganizationsLoading` is `false` on both
130
+ * sides of the request — so no combination of the fields above can tell
131
+ * "not asked yet" from "asked, and there is none". That ambiguity is what let
132
+ * `useWorkspaceAdminStatus` answer "not an admin" about an administrator
133
+ * whose adminship lives only in the member row.
134
+ *
135
+ * True once the pipeline has reached a terminal state: the org list came back
136
+ * AND the active-member row either landed, failed, or was established not to
137
+ * exist (no active organization). Also true immediately where there is no
138
+ * pipeline to run at all — preview / auth-disabled mode, and outside an
139
+ * AuthProvider — so a gate on this can never hang.
140
+ */
141
+ isMembershipResolved: boolean;
109
142
  /** Switch the active organization (workspace) */
110
143
  switchOrganization: (orgId: string) => Promise<void>;
111
144
  /** Create a new organization */
@@ -1 +1 @@
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,SAAS,CAAC;AAEtM,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;;;kFAG8E;IAC9E,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,oBAAoB,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtG,gCAAgC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,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,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"}
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;;;kFAG8E;IAC9E,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,oBAAoB,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtG,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[];
@@ -1 +1 @@
1
- {"version":3,"file":"AuthGuard.d.ts","sourceRoot":"","sources":["../src/AuthGuard.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,qCAAqC;IACrC,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,4CAA4C;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAe,EACf,aAAa,EACb,eAAe,EACf,QAAQ,GACT,EAAE,cAAc,qBAqBhB"}
1
+ {"version":3,"file":"AuthGuard.d.ts","sourceRoot":"","sources":["../src/AuthGuard.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,qCAAqC;IACrC,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,4CAA4C;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAe,EACf,aAAa,EACb,eAAe,EACf,QAAQ,GACT,EAAE,cAAc,qBAkChB"}
package/dist/AuthGuard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { useAuth } from './useAuth';
2
+ import { useAuth } from './useAuth.js';
3
3
  /**
4
4
  * Route guard component that conditionally renders children
5
5
  * based on authentication and authorization state.
@@ -23,9 +23,22 @@ export function AuthGuard({ fallback = null, requiredRoles, loadingFallback, chi
23
23
  if (!isAuthenticated) {
24
24
  return _jsx(_Fragment, { children: fallback });
25
25
  }
26
- // Check role requirements
26
+ // Check role requirements.
27
+ //
28
+ // `positions` is the ONE published spelling (framework ADR-0090 D3 renamed
29
+ // `roles` → `positions` with no deprecation window; maintainer ruling on
30
+ // objectui#5424). When the session carries the key — protocol 17 always
31
+ // emits it — it is the whole answer, empty included: falling back to the
32
+ // coarse `user.role` scalar when the server said "no positions" would be
33
+ // exactly the over-admission this gate used to have (`role: 'admin'`
34
+ // passing a gate meant for an `admin` position). The scalar fallback exists
35
+ // for identities that lack the key entirely: the guest user the provider
36
+ // seeds when auth is disabled, and pre-positions deployments. The retired
37
+ // `roles` spelling is deliberately NOT read — resurrecting it as a fallback
38
+ // dialect is what ADR-0090 D3 forbids
39
+ // (`__tests__/authGuardPositions-5424.test.tsx` pins the refusal).
27
40
  if (requiredRoles && requiredRoles.length > 0 && user) {
28
- const userRoles = user.roles ?? (user.role ? [user.role] : []);
41
+ const userRoles = user.positions ?? (user.role ? [user.role] : []);
29
42
  const hasRole = requiredRoles.some((role) => userRoles.includes(role));
30
43
  if (!hasRole) {
31
44
  return _jsx(_Fragment, { children: fallback });
@@ -6,7 +6,7 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import React from 'react';
9
- import type { AuthProviderOptions, PreviewModeOptions } from './types';
9
+ import type { AuthProviderOptions, PreviewModeOptions } from './types.js';
10
10
  export interface AuthProviderProps extends AuthProviderOptions {
11
11
  children: React.ReactNode;
12
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"AuthProvider.d.ts","sourceRoot":"","sources":["../src/AuthProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAoD,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAwB,mBAAmB,EAAE,kBAAkB,EAAyG,MAAM,SAAS,CAAC;AAKpM,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,MAAM,EAAE,cAAc,EACtB,iBAAiB,EACjB,OAAc,EACd,WAAW,EACX,QAAQ,GACT,EAAE,iBAAiB,qBA6pBnB"}
1
+ {"version":3,"file":"AuthProvider.d.ts","sourceRoot":"","sources":["../src/AuthProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,OAAO,KAAK,EAAwB,mBAAmB,EAAE,kBAAkB,EAAyG,MAAM,YAAY,CAAC;AA4GvM,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,MAAM,EAAE,cAAc,EACtB,iBAAiB,EACjB,OAAc,EACd,WAAW,EACX,QAAQ,GACT,EAAE,iBAAiB,qBA43BnB"}