@object-ui/auth 17.5.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 +110 -0
  2. package/README.md +148 -0
  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 +19 -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 +272 -7
  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 +1 -1
  27. package/dist/createAuthClient.d.ts.map +1 -1
  28. package/dist/createAuthClient.js +36 -4
  29. package/dist/createAuthenticatedFetch.d.ts +9 -9
  30. package/dist/createAuthenticatedFetch.d.ts.map +1 -1
  31. package/dist/createAuthenticatedFetch.js +49 -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 +11 -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 +5 -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 +4 -4
  47. package/dist/useIsWorkspaceAdmin.d.ts +0 -21
  48. package/dist/useIsWorkspaceAdmin.d.ts.map +0 -1
  49. package/dist/useIsWorkspaceAdmin.js +0 -47
package/CHANGELOG.md CHANGED
@@ -1,5 +1,115 @@
1
1
  # @object-ui/auth
2
2
 
3
+ ## 17.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b21b1a: `AuthInvitation.status` is the closed four-member union it always documented, enforced at the auth client's wire boundary.
8
+
9
+ The field was declared `status: string` while its own doc comment enumerated
10
+ `'pending' | 'accepted' | 'rejected' | 'canceled'`, and `createAuthClient` cast
11
+ better-auth's `any` responses straight to `AuthInvitation[]` with no check. The
12
+ enumeration was therefore advisory: any value a backend stored reached the
13
+ console untouched, where the invitations screen coloured it through a
14
+ `default: 'secondary'` badge arm and printed it with
15
+ `defaultValue: inv.status` — the raw wire string rendered as interface copy, in
16
+ all ten packs (objectui#3879). Nobody could trigger it today, because the four
17
+ are what better-auth writes; it was the contract that was loose, and a loose
18
+ consumer is where an unexpected value would have hidden.
19
+
20
+ `AuthInvitationStatus` now carries the union and **binds better-auth's own
21
+ `InvitationStatus`** rather than restating it, the way `org-roles.ts` binds the
22
+ spec's `BUILTIN_MEMBERSHIP_ROLES` — a member-for-member copy is the state one
23
+ upstream release away from drifting silently. The runtime list
24
+ (`AUTH_INVITATION_STATUSES`) is derived from a total map keyed by that union, so
25
+ a fifth status upstream is a build failure here rather than a gap in the guard.
26
+ `isAuthInvitationStatus` is exported alongside, and all four invitation-returning
27
+ client methods (`listInvitations`, `listUserInvitations`, `getInvitation`,
28
+ `inviteMember`) narrow their wire rows through it.
29
+
30
+ Behaviour change, stated because it is one: an invitation whose `status` is
31
+ outside the set now **fails loudly** — the call rejects with a message naming the
32
+ value it refused and the four it expected — instead of resolving into a badge.
33
+ Both call paths already render a rejection with a retry, so the throw lands on a
34
+ designed surface. Degrading quietly was the alternative and was deliberately not
35
+ taken: a neutral label is how the raw value shipped in the first place, and
36
+ dropping the row would delete an invitation from an administrative ledger without
37
+ saying so. The console's badge switch is exhaustive over the union now and has no
38
+ `default:` arm, so if better-auth ever adds a member the type-check gate stops the
39
+ build at the one place a human has to choose a colour.
40
+
41
+ Consumers assigning an arbitrary string to `AuthInvitation.status` (a hand-built
42
+ fixture, a mock) will need one of the four members.
43
+
44
+ ### Patch Changes
45
+
46
+ - 5458414: Publish relative import specifiers with explicit `.js` extensions so these six packages load under plain Node ESM.
47
+
48
+ Node's ESM resolver does not extension-search relative specifiers and `tsc` never rewrites them, so an extensionless `./Foo` in the source shipped as an extensionless `./Foo` in `dist` and importing the package entry outside a bundler failed with `ERR_MODULE_NOT_FOUND`. Bundled consumers were unaffected. Unbundled consumers — plain Node ESM, an SSR host importing the package directly, anyone running the published tarball without a build step — can now import these entries, and so can the downstream `@object-ui/plugin-*` packages that evaluate through `mobile`, `permissions` and `providers`.
49
+ - 61b097c: Sign-out now drops the client-side caches that belonged to the session it ends,
50
+ and the metadata seed cache is keyed by session identity.
51
+
52
+ `sessionStorage` is per-tab, not per-session, and no sign-out call site reloads
53
+ the page — so the `objectui:metadata:*` entries (the app list the server
54
+ permission-filters per session) and the active-organization id survived into
55
+ whatever happened next in that tab. A second person signing in on a shared,
56
+ kiosk or handover browser was seeded with the previous user's filtered app list.
57
+ Organization scoping did not close this: two users in the same organization
58
+ computed the same cache key.
59
+
60
+ `AuthProvider.signOut()` now purges the `objectui:metadata:` entries and clears
61
+ `ActiveOrganizationStorage` (and the in-memory organization block) on both the
62
+ success and failure paths, and `MetadataProvider` keys each entry by a
63
+ fingerprint of the session token, so an entry that escapes the purge is
64
+ unreadable by the next principal rather than merely undeleted. Entries left by
65
+ another principal are deleted the first time a console mounts.
66
+ - a9e17b4: `auth.forgotPassword.successDescription`'s address hole is now spelled with
67
+ single braces (`{email}`) instead of i18next's double braces (`{{email}}`),
68
+ in all ten locale packs.
69
+
70
+ This is a spelling-only change — rendered output is byte-identical in every
71
+ language, because the hole was never filled by i18next in the first place:
72
+ `ForgotPasswordForm` substitutes the address itself once the user submits
73
+ the form (the label renders before the address exists, so `t()` cannot do
74
+ it). `{{email}}` and a genuinely unfilled i18next hole were indistinguishable
75
+ at the call site, and passing `email` as an interpolation argument — the
76
+ natural "fix" for what looks like a missing argument — would let i18next
77
+ consume the hole and cause the address to be appended a second time
78
+ (objectui#4135).
79
+
80
+ Converging on `{x}` for every hole a component fills downstream of `t()`
81
+ (the convention `resendOtpCountdownText`'s `{seconds}` already used) puts
82
+ this hole outside i18next's `{{…}}` syntax entirely, so the ambiguity is
83
+ gone by construction rather than fenced by an exemption. Accordingly,
84
+ `scripts/check-i18n-call-site-keys.mjs`'s `EXTERNALLY_INTERPOLATED_HOLES`
85
+ registry entry for this key is retired — the gate needs no exemption for a
86
+ hole i18next was never going to touch.
87
+
88
+ `ForgotPasswordForm.tsx`'s replacement marker and its own built-in default
89
+ label move to the same spelling in the same change, as does the inline
90
+ `defaultValue` at `apps/console/src/pages/auth/ForgotPasswordPage.tsx`'s
91
+ call site.
92
+ - Updated dependencies [88085e3]
93
+ - Updated dependencies [279fb13]
94
+ - Updated dependencies [1184192]
95
+ - Updated dependencies [a2a9747]
96
+ - Updated dependencies [af5e292]
97
+ - Updated dependencies [7f96b10]
98
+ - Updated dependencies [f1d4748]
99
+ - Updated dependencies [578e025]
100
+ - Updated dependencies [598c89a]
101
+ - Updated dependencies [b8b9af4]
102
+ - Updated dependencies [97abb24]
103
+ - Updated dependencies [deb157a]
104
+ - Updated dependencies [d2ce342]
105
+ - Updated dependencies [9695da7]
106
+ - Updated dependencies [58b8346]
107
+ - Updated dependencies [3cf4de0]
108
+ - Updated dependencies [c9dc811]
109
+ - Updated dependencies [a0b9e91]
110
+ - Updated dependencies [99bd015]
111
+ - @object-ui/types@17.6.0
112
+
3
113
  ## 17.5.0
4
114
 
5
115
  ### Minor Changes
package/README.md CHANGED
@@ -137,6 +137,154 @@ const authedFetch = createAuthenticatedFetch();
137
137
  const apiProviderFetch = createAuthenticatedFetch({ sameOriginOnly: true });
138
138
  ```
139
139
 
140
+ ## The `X-Tenant-ID` edge contract
141
+
142
+ `createAuthenticatedFetch` stamps `X-Tenant-ID` on the requests it wraps. This section is
143
+ that header's contract: what it means, who stamps it, who reads it, what a reader may
144
+ assume, and the one window in which it is not sent at all.
145
+
146
+ It is written down because **a grep cannot answer any of those questions from this
147
+ repository.** The header's only non-CORS consumer lives in the **cloud** repository, so a
148
+ search confined to this repo and the framework (`objectstack`) finds zero readers and
149
+ reads as *"nothing consumes this stamp"*. That reading is what
150
+ [objectui#5279](https://github.com/objectstack-ai/objectui/issues/5279) was filed on, and
151
+ it is false. Do not act on it.
152
+
153
+ ### What it means
154
+
155
+ A **routing hint** for the hosting edge — *this request belongs to tenant `<id>`*:
156
+
157
+ ```http
158
+ X-Tenant-ID: <better-auth activeOrganizationId>
159
+ ```
160
+
161
+ The value is the active **organization** id, the same id the session carries as
162
+ `session.activeOrganizationId`. The header is **not** an identity claim, **not** an
163
+ authorization input, and **not** what scopes rows.
164
+
165
+ ### Who stamps it
166
+
167
+ | | |
168
+ | --- | --- |
169
+ | Stamped by | `createAuthenticatedFetch` ([`src/createAuthenticatedFetch.ts`](src/createAuthenticatedFetch.ts)) |
170
+ | Value read from | `ActiveOrganizationStorage` ([`src/ActiveOrganizationStorage.ts`](src/ActiveOrganizationStorage.ts)) — `localStorage`, key `auth-active-organization-id:u:$userId` (per-user since objectui#5664; the bare key is the retired pre-#5664 spelling) |
171
+ | Condition | that storage holds a non-empty value |
172
+ | *Not* conditioned on | the URL being an `/api/` call. `Authorization` and `Accept-Language` are; this is not |
173
+ | Suppressed by | `createAuthenticatedFetch({ sameOriginOnly: true })` for cross-origin URLs — it returns before any header work |
174
+ | Precedence | overwrites an `X-Tenant-ID` the caller passed in `init.headers` |
175
+
176
+ `ActiveOrganizationStorage` has exactly one writer, [`AuthProvider`](src/AuthProvider.tsx):
177
+
178
+ | Event | Effect |
179
+ | --- | --- |
180
+ | `refreshOrganizations` — after `getSession` &rarr; `listOrganizations` &rarr; `getActiveOrganization` resolves (including the ADR-0081 single-membership repair) | set |
181
+ | `switchOrganization` — the org switcher | set, or clear when the server returns no org |
182
+ | `deleteOrganization` / `leaveOrganization`, when the active org is the one going away | clear |
183
+ | sign-out | clear |
184
+
185
+ ### Who reads it
186
+
187
+ **The cloud edge — yes.** The non-test readers are recorded on objectui#5279:
188
+ `packages/service-tenant/src/tenant-context.ts` (header resolution) and
189
+ `packages/tenant-router/src/spec/turso-multi-tenant.zod.ts`, both in the `cloud`
190
+ repository. That repository is not readable from this one, so those paths are cited as the
191
+ recorded reading rather than re-derived here.
192
+
193
+ **Its configuration contract, though, IS readable from here**, because this package
194
+ depends on `@objectstack/spec`. `TenantRoutingConfigSchema` (`@objectstack/spec/cloud`) is
195
+ what configures a tenant resolver; parsing an empty config on the version this package
196
+ resolves today (17.1.0) yields:
197
+
198
+ ```text
199
+ enabled: false
200
+ identificationSources: ["subdomain", "header", "jwt_claim"]
201
+ tenantHeaderName: "X-Tenant-ID"
202
+ jwtOrganizationClaim: "organizationId"
203
+ ```
204
+
205
+ Two consequences matter to a client author:
206
+
207
+ - **The header name is configurable.** `X-Tenant-ID` is its default, not a constant.
208
+ - **The header is one of six identification sources** (`subdomain`, `custom_domain`,
209
+ `header`, `jwt_claim`, `session`, `default`), and in the default precedence it ranks
210
+ **second, behind `subdomain`**. On a subdomain-routed deployment it is not the thing
211
+ that picks the tenant, and a deployment may leave it out of `identificationSources`
212
+ entirely.
213
+
214
+ **The framework (`objectstack`) — no.** `resolveAuthzContext`
215
+ (`packages/core/src/security/resolve-authz-context.ts`) derives `tenantId` from the
216
+ API-key principal or from `session.activeOrganizationId`, and from no header;
217
+ `packages/verify/src/harness.org-context.test.ts` pins it — *"`session.activeOrganizationId`
218
+ is the ONE field `resolveAuthzContext` reads into `tenantId`"*. Environment and kernel
219
+ routing read the hostname and `X-Environment-Id`. The framework's only other mentions are
220
+ the CORS preflight allow-list (`DEFAULT_CORS_ALLOW_HEADERS`, whose comment describes
221
+ `X-Tenant-ID` / `X-Environment-Id` as what routes "a request to its environment") and
222
+ `plugin-sharing`, which records that trusting `x-tenant-id` as identity **was a
223
+ vulnerability**: its secure default stopped reading identity from headers because doing so
224
+ let a client forge attribution and enumerate or revoke other users' links.
225
+
226
+ ### What a reader may assume
227
+
228
+ A reader **may**:
229
+
230
+ - use it to select the tenant database or environment to route to, subject to its own
231
+ `identificationSources` precedence;
232
+ - treat it as a hint that may be absent, stale, or contradicted by the session.
233
+
234
+ A reader **may not**:
235
+
236
+ - treat it as authenticated identity, or as an authorization decision. It is
237
+ client-controlled — any caller can send any value, and `plugin-sharing` is the recorded
238
+ precedent for what happens to a server that trusts it;
239
+ - assume the row scoping it sees downstream came from this header. It did not: the
240
+ framework scopes from the session;
241
+ - assume the header is present. See below.
242
+
243
+ ### The unstamped-first-request gap
244
+
245
+ The stamp reads storage that `AuthProvider` fills only **after** `getSession` &rarr;
246
+ `listOrganizations` &rarr; `getActiveOrganization` resolves. Every request that leaves
247
+ before that chain completes carries **no** `X-Tenant-ID` at all.
248
+
249
+ The window opens in five situations:
250
+
251
+ 1. a browser that has never signed in, or whose site data was cleared, including a private
252
+ window;
253
+ 2. the page load in which the user signs in — from the sign-in response until the
254
+ organization chain resolves;
255
+ 3. after sign-out, which clears the storage, until a new organization resolves;
256
+ 4. outside a browser (SSR, tests, a worker), where `localStorage` is unavailable and the
257
+ storage falls back to a per-process in-memory value that starts empty on every cold
258
+ start;
259
+ 5. a browser where `localStorage` exists but **rejects writes** (Safari private browsing,
260
+ quota exhaustion). There the window never closes — the in-memory fallback is written
261
+ but never read back, because `get()` only falls back when reading itself *throws*, and
262
+ a rejected write leaves reads working and returning `null`. Reported on objectui#5279
263
+ as a separate defect; it is not fixed here.
264
+
265
+ Case 1 is not theoretical in this codebase: `app-shell`'s `MetadataProvider` issues its
266
+ eager `app` metadata fetch inside exactly this window, which is why its first-boot cache
267
+ scope needed its own reasoning
268
+ ([`packages/app-shell/src/providers/MetadataProvider.tsx`](../app-shell/src/providers/MetadataProvider.tsx),
269
+ pinned by `MetadataProvider.firstBootOrgScope.test.tsx`).
270
+
271
+ **What a reader observes in the window:** the header is **absent**, never
272
+ present-and-empty. A resolver must fall through to its next configured identification
273
+ source — `subdomain` already outranks it by default, with `jwt_claim`, `session` and
274
+ `defaultTenantId` behind it — rather than fail closed on the absence. It must also not
275
+ cache a routing decision taken inside the window as *the* tenant for the session: the very
276
+ next request will normally carry the header.
277
+
278
+ **Why the gap does not corrupt data scoping.** A response computed inside the window is
279
+ still computed for the right tenant, because the framework takes `tenantId` from the
280
+ session rather than from the header. The gap is a *routing-input* gap, not a scoping gap.
281
+ This is the same reasoning objectui#5243 relied on when it relabelled a metadata cache
282
+ entry that had been written in the window.
283
+
284
+ **Closing the gap is a separate decision, not an omission.** The cloud readers observe the
285
+ current behaviour, so changing when the header first appears changes what they see. If it
286
+ should be closed, it needs its own card.
287
+
140
288
  ## Server Feature Flags (`GET /auth/config`)
141
289
 
142
290
  `createAuthClient().getConfig()` fetches the server's public auth configuration. The
@@ -0,0 +1,168 @@
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
+ * Drop the outgoing user's client state wholesale — part 3 of the fix.
10
+ *
11
+ * Order is load-bearing and mirrors `clear()`'s: the in-memory active org goes
12
+ * FIRST, because `ActiveOrganizationStorage.get()` falls through to
13
+ * `_memoryValue` whenever the persisted read is null, which is exactly the
14
+ * state this function leaves behind (objectui#5703). Sweeping storage first
15
+ * would leave the outgoing user's org id live in memory for the rest of the
16
+ * page-load — the SPA sign-out-then-sign-in path never reloads.
17
+ *
18
+ * Both stores are swept. `sessionStorage` is per-TAB, not per-session, so the
19
+ * previous user's `objectui:metadata:*` seed (their PERMISSION-FILTERED app
20
+ * list) and their `objectui-ctx-*` scope picks survive into whoever signs in
21
+ * next in that tab unless something removes them.
22
+ */
23
+ export declare function purgePreviousUserClientState(): void;
24
+ /**
25
+ * Which user's client state this browser holds, and the transition that drops
26
+ * the previous user's state.
27
+ */
28
+ export declare const SessionUserScope: {
29
+ /**
30
+ * Memory copy of the pointer. Read FIRST by {@link current}: once this
31
+ * page-load has adopted a user, that answer is authoritative for this tab
32
+ * even if another tab has since written a different id, and it is the only
33
+ * copy in a browser whose `localStorage` rejects writes.
34
+ */
35
+ _userId: string | null;
36
+ /** The user whose state this browser holds, or `null` if it has never held one. */
37
+ current(): string | null;
38
+ /**
39
+ * Record `userId` as the owner of this browser's client state, dropping the
40
+ * previous owner's state wholesale when it changes.
41
+ *
42
+ * Idempotent: re-adopting the same user purges nothing. Called from
43
+ * `AuthProvider` for real sessions only — the synthetic `preview-user` /
44
+ * `guest` identities never reach it, so a preview mount cannot purge a real
45
+ * user's state.
46
+ *
47
+ * Because the decision reads the PERSISTED pointer, anything that deletes
48
+ * that pointer without deleting the state it describes would silently retire
49
+ * this invariant. `apps/console/src/lib/auth-preflight.ts` is the one place
50
+ * that sweeps auth keys before render, and it says so in its own comment:
51
+ * it removes every active-org spelling and leaves `auth-session-user-id`
52
+ * alone on purpose.
53
+ */
54
+ adopt(userId: string): void;
55
+ /**
56
+ * Forget the in-memory pointer. Test seam only — production has no reason to
57
+ * un-know who is signed in, and sign-out deliberately KEEPS the persisted
58
+ * pointer so that the next sign-in by a DIFFERENT user still sees a previous
59
+ * owner to purge.
60
+ */
61
+ _resetForTests(): void;
62
+ };
63
+ /**
64
+ * Get/set the active organization id that {@link createAuthenticatedFetch}
65
+ * stamps as `X-Tenant-ID`.
66
+ *
67
+ * `AuthProvider` is the only writer, at four moments: `refreshOrganizations`
68
+ * sets it once the `getSession` -> `listOrganizations` ->
69
+ * `getActiveOrganization` chain resolves (including the ADR-0081
70
+ * single-membership repair), `switchOrganization` sets or clears it,
71
+ * `deleteOrganization` / `leaveOrganization` clear it when the active org is
72
+ * the one going away, and sign-out clears it.
73
+ *
74
+ * Because the first of those is asynchronous, this reads EMPTY for the first
75
+ * stretch of a boot, and every request that leaves in that window carries no
76
+ * tenant header at all. That window is a documented part of the header's
77
+ * contract, not an accident to paper over — see the "unstamped-first-request
78
+ * gap" section of this package's README (objectui#5279).
79
+ */
80
+ export declare const ActiveOrganizationStorage: {
81
+ _memoryValue: string | null;
82
+ /**
83
+ * Keys whose removal by {@link clear} did not stick — the value was still
84
+ * readable afterwards (objectui#5731).
85
+ *
86
+ * {@link get} refuses to answer from the persisted layer for a key in here,
87
+ * which is the mechanism that keeps a cleared organization cleared. Held in
88
+ * memory, so it lasts exactly one page-load: a browser that starts fresh
89
+ * re-measures rather than inheriting a verdict.
90
+ */
91
+ _unremovedKeys: Set<string>;
92
+ /**
93
+ * The active org id for the CURRENT user, or `null`.
94
+ *
95
+ * READ ORDER — a non-null `localStorage` read wins; anything else falls
96
+ * through to `_memoryValue`. Returning the `localStorage` read
97
+ * UNCONDITIONALLY (what this did before objectui#5703) left the fallback
98
+ * reachable only when the read THREW, and there is a real browser state
99
+ * where the read does not throw and the fallback is still the only copy:
100
+ * `localStorage` present and readable but REJECTING WRITES — Safari private
101
+ * browsing, and any quota-exhausted origin, where `setItem` throws
102
+ * `QuotaExceededError`. `set()` swallows that failure into `_memoryValue`
103
+ * (correctly — the fallback is right there), and `get()` then never
104
+ * consulted it: the value was stored and could not be read back, so
105
+ * `X-Tenant-ID` went unstamped for the whole session, not just the early
106
+ * requests.
107
+ *
108
+ * WHY FALLING BACK ON `null` DOES NOT RESURRECT A CLEARED ORG. After
109
+ * `clear()` the `localStorage` read is null by construction, so this
110
+ * fallback fires — and it must answer `null`, because sign-out is one of
111
+ * `clear()`'s callers. It does, because `clear()` nulls `_memoryValue` too.
112
+ * That property is what makes this read order safe rather than an
113
+ * incidental detail of `clear()`'s body, so it is pinned by test
114
+ * (`__tests__/activeOrgStorageFallback-5703.test.tsx`) instead of being
115
+ * re-derived by whoever edits `clear()` next.
116
+ *
117
+ * The same reasoning is why a change of session user purges through
118
+ * {@link purgePreviousUserClientState} rather than by removing keys: the
119
+ * outgoing user's id in `_memoryValue` would otherwise outlive their
120
+ * persisted key on the SPA sign-out-then-sign-in path (objectui#5664).
121
+ */
122
+ get(): string | null;
123
+ /**
124
+ * With no session user known this writes to memory ONLY. That is not a
125
+ * degraded path to apologise for — it is the fix: a persisted write with no
126
+ * owner is the un-namespaced key this card retired.
127
+ */
128
+ set(orgId: string): void;
129
+ /**
130
+ * Drop the active organization, from memory and from the persisted key.
131
+ *
132
+ * POSTCONDITION: {@link get} answers `null` afterwards. Sign-out is one of
133
+ * the callers, so that is a security-relevant guarantee rather than a
134
+ * convenience — and before objectui#5731 it was not one, because a
135
+ * `removeItem` that failed was swallowed and the surviving key won `get()`'s
136
+ * read order.
137
+ *
138
+ * ## Why a failed removal is neither thrown nor returned (objectui#5731)
139
+ *
140
+ * Every caller arrives here AFTER the transition it is following up on has
141
+ * already happened, and not one of them can act on a storage failure:
142
+ *
143
+ * - `AuthProvider`'s `purgeSignedOutClientCaches` — the session is already
144
+ * ended. Sign-out cannot be refused because a key would not delete.
145
+ * - `switchOrganization` — the server already switched or cleared the
146
+ * active org; a throw would report a successful switch as a failure and
147
+ * route the caller into its error branch.
148
+ * - `deleteOrganization` / `leaveOrganization` — the org is already deleted
149
+ * or already left.
150
+ * - {@link purgePreviousUserClientState}, via {@link SessionUserScope.adopt}
151
+ * — runs on the SIGN-IN path inside an `AuthProvider` effect, where a
152
+ * throw breaks the boot of the ARRIVING user.
153
+ *
154
+ * A `boolean` return only moves the problem one step: all five call sites
155
+ * would have nothing to write in the failure branch, and a return value that
156
+ * every caller ignores reads as handled when it is not. So the invariant is
157
+ * restored HERE, where it can be, and the failure is reported to the console,
158
+ * where a human can find it.
159
+ *
160
+ * The third shape considered and rejected was re-writing the key with an
161
+ * empty value. That relocates the fix into every consumer's truthiness test
162
+ * (`createAuthenticatedFetch` does `if (activeOrgId)`), which is the lenient
163
+ * consumer AGENTS.md #0.1 forbids, and it leaves a signed-out browser holding
164
+ * a live key.
165
+ */
166
+ clear(): void;
167
+ };
168
+ //# sourceMappingURL=ActiveOrganizationStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActiveOrganizationStorage.d.ts","sourceRoot":"","sources":["../src/ActiveOrganizationStorage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyOH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAInD;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;;;;OAKG;aACc,MAAM,GAAG,IAAI;IAE9B,mFAAmF;eACxE,MAAM,GAAG,IAAI;IAKxB;;;;;;;;;;;;;;;OAeG;kBACW,MAAM,GAAG,IAAI;IA0B3B;;;;;OAKG;sBACe,IAAI;CAGvB,CAAC;AAqBF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,yBAAyB;kBACd,MAAM,GAAG,IAAI;IAEnC;;;;;;;;OAQG;;IAGH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;WACI,MAAM,GAAG,IAAI;IAsBpB;;;;OAIG;eACQ,MAAM,GAAG,IAAI;IAOxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;aACM,IAAI;CAyCd,CAAC"}