@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.
- package/CHANGELOG.md +600 -0
- package/README.md +245 -15
- package/dist/ActiveOrganizationStorage.d.ts +199 -0
- package/dist/ActiveOrganizationStorage.d.ts.map +1 -0
- package/dist/ActiveOrganizationStorage.js +584 -0
- package/dist/AuthContext.d.ts +23 -3
- package/dist/AuthContext.d.ts.map +1 -1
- package/dist/AuthGuard.d.ts +8 -1
- package/dist/AuthGuard.d.ts.map +1 -1
- package/dist/AuthGuard.js +16 -3
- package/dist/AuthProvider.d.ts +1 -1
- package/dist/AuthProvider.d.ts.map +1 -1
- package/dist/AuthProvider.js +274 -9
- package/dist/ForgotPasswordForm.d.ts +1 -1
- package/dist/ForgotPasswordForm.d.ts.map +1 -1
- package/dist/ForgotPasswordForm.js +5 -5
- package/dist/LoginForm.d.ts +11 -2
- package/dist/LoginForm.d.ts.map +1 -1
- package/dist/LoginForm.js +5 -6
- package/dist/PreviewBanner.js +1 -1
- package/dist/RegisterForm.d.ts +22 -3
- package/dist/RegisterForm.d.ts.map +1 -1
- package/dist/RegisterForm.js +6 -7
- package/dist/SocialSignInButtons.d.ts +9 -2
- package/dist/SocialSignInButtons.d.ts.map +1 -1
- package/dist/SocialSignInButtons.js +5 -5
- package/dist/UserMenu.d.ts +1 -1
- package/dist/UserMenu.js +3 -3
- package/dist/createAuthClient.d.ts +1 -1
- package/dist/createAuthClient.d.ts.map +1 -1
- package/dist/createAuthClient.js +42 -4
- package/dist/createAuthenticatedFetch.d.ts +9 -9
- package/dist/createAuthenticatedFetch.d.ts.map +1 -1
- package/dist/createAuthenticatedFetch.js +50 -38
- package/dist/index.d.ts +21 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -17
- package/dist/invitation-status.d.ts +59 -0
- package/dist/invitation-status.d.ts.map +1 -0
- package/dist/invitation-status.js +32 -0
- package/dist/types.d.ts +25 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/useAuth.d.ts +1 -1
- package/dist/useAuth.d.ts.map +1 -1
- package/dist/useAuth.js +5 -1
- package/dist/useWorkspaceAdminStatus.d.ts +134 -0
- package/dist/useWorkspaceAdminStatus.d.ts.map +1 -0
- package/dist/useWorkspaceAdminStatus.js +208 -0
- package/package.json +6 -7
- package/dist/useIsWorkspaceAdmin.d.ts +0 -21
- package/dist/useIsWorkspaceAdmin.d.ts.map +0 -1
- package/dist/useIsWorkspaceAdmin.js +0 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,605 @@
|
|
|
1
1
|
# @object-ui/auth
|
|
2
2
|
|
|
3
|
+
## 17.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b57107d: fix(auth,app-shell,console): a browser that changes hands no longer keeps the previous account's UI language
|
|
8
|
+
|
|
9
|
+
Both device language slots (`objectui-locale` and `objectui-locale-seed`) hold the
|
|
10
|
+
language of whoever last signed in on the browser. A boot resolves its language from
|
|
11
|
+
them before anyone is signed in. When the session then turns out to belong to someone
|
|
12
|
+
else (an SSO redirect back into the console, or a sign-in in another window),
|
|
13
|
+
`SessionUserScope.adopt` already purged both slots (objectui#5664). But the live
|
|
14
|
+
language had already been derived from them, and nothing re-derived it. A new account
|
|
15
|
+
with no `sys_user.locale` of its own therefore read the previous account's language
|
|
16
|
+
for its whole first session.
|
|
17
|
+
|
|
18
|
+
- `@object-ui/auth` (new exports, hence minor): `getSessionOwnerChangeCount()` and
|
|
19
|
+
`subscribeSessionOwnerChange(listener)` report that the change-of-owner purge ran
|
|
20
|
+
in this page-load. They are held in memory, with no new storage key.
|
|
21
|
+
`SessionUserScope.adopt` now returns `true` when it purged a previous owner.
|
|
22
|
+
- `@object-ui/app-shell`: `useSignedInUserLocale` resets the UI language on an owner
|
|
23
|
+
change, the same way a boot of the swept storage would resolve it. It then
|
|
24
|
+
re-reads the new owner's `sys_user.locale`, which applies after the reset.
|
|
25
|
+
- `@object-ui/console`: `LocalizationFetchProvider` and the boot-time seed fetch no
|
|
26
|
+
longer write an answer that was requested before an owner change. Such an answer
|
|
27
|
+
describes the previous owner.
|
|
28
|
+
- `@object-ui/i18n` (docs only): the seed was documented as the "tenant's" locale.
|
|
29
|
+
It is the server's resolved locale for the signed-in caller: their own
|
|
30
|
+
`sys_user.locale`, else `Accept-Language`, else the deployment default. The docs
|
|
31
|
+
now say so, and declare what a visitor who has not signed in reads: the last
|
|
32
|
+
owner's language, until someone else signs in.
|
|
33
|
+
- 7ea8118: fix(auth, console): a registration started from an invitation link comes back to the invitation after email verification
|
|
34
|
+
|
|
35
|
+
better-auth builds the verification mail's link server-side from the
|
|
36
|
+
`/sign-up/email` request's `callbackURL`, defaulting it to `/`. The console kept
|
|
37
|
+
`?redirect=/accept-invitation/ID` from the login page through `/register` to the
|
|
38
|
+
"check your inbox" screen, but `createAuthClient().signUp` never sent a
|
|
39
|
+
`callbackURL`, so the mail read `callbackURL=/` and the invitee verified onto the
|
|
40
|
+
workspace picker ("Create workspace") instead of the invitation.
|
|
41
|
+
|
|
42
|
+
`@object-ui/auth` (additive):
|
|
43
|
+
|
|
44
|
+
- `SignUpData.callbackURL` is forwarded verbatim to `/sign-up/email`. Absent, the
|
|
45
|
+
key stays off the wire and the server default is untouched.
|
|
46
|
+
- `useAuth().signUp(name, email, password, callbackURL?)` takes it as an optional
|
|
47
|
+
fourth argument.
|
|
48
|
+
- `RegisterForm` gains a `verificationCallbackURL` prop, forwarded to `signUp`.
|
|
49
|
+
|
|
50
|
+
`@object-ui/console`:
|
|
51
|
+
|
|
52
|
+
- `RegisterPage` passes a safe `?redirect=` as the verification callback, and the
|
|
53
|
+
verify-email prompt's "Resend" sends the same value. Before, Resend sent the
|
|
54
|
+
bare router path, which the server redirects to at the ORIGIN root, outside
|
|
55
|
+
the `/_console` mount.
|
|
56
|
+
- Both resolve the route through the new `withConsoleBaseRootRelative`. The server
|
|
57
|
+
never sees `<base href>`, and better-auth refuses a document-relative `./…`
|
|
58
|
+
callback — what `withConsoleBase` answers in the embedded build — with
|
|
59
|
+
`403 INVALID_CALLBACK_URL`, failing the whole sign-up.
|
|
60
|
+
- 328abeb: Four Console surfaces that read English under a zh-CN session now read the session's
|
|
61
|
+
language (objectui#10900). English stays the default.
|
|
62
|
+
|
|
63
|
+
- **The generic action success toast.** When an action declares no `successMessage`, the
|
|
64
|
+
runner falls back to "Action completed successfully".
|
|
65
|
+
That fallback now goes through a translator: `ActionRunner.setTranslator(translate)` is
|
|
66
|
+
new in `@object-ui/core`, and `<ActionProvider>` and `useActionRunner` in
|
|
67
|
+
`@object-ui/react` install the session's `t` on the runner they build. The pack key is
|
|
68
|
+
`actions.completedSuccessfully`. An author's `successMessage` still reaches the toast
|
|
69
|
+
untranslated, and a runner with no translator installed still toasts the English sentence.
|
|
70
|
+
- **The social sign-in buttons on the login and sign-up pages.** `SocialSignInButtons`
|
|
71
|
+
takes a new `buttonText` prop, a template whose `{provider}` is replaced with the
|
|
72
|
+
provider's display name; unset, the buttons keep "Continue with {provider}" and
|
|
73
|
+
"Sign up with {provider}". `LoginForm` and `RegisterForm` take a new `socialButton`
|
|
74
|
+
label and pass it, with their existing `orText` label, to the buttons. `orText` was
|
|
75
|
+
documented as the divider label but rendered nowhere; it now sets the divider under the
|
|
76
|
+
buttons, and its documented default is corrected from "or" to "or continue with email",
|
|
77
|
+
the text that divider has always shown. The console's login and sign-up pages and
|
|
78
|
+
`@object-ui/app-shell`'s `DefaultLoginPage` / `DefaultRegisterPage` pass the new
|
|
79
|
+
`auth.login.*` and `auth.register.*` `socialButton` / `orText` keys. The provider's display
|
|
80
|
+
name is inserted as-is: the component's own label for the branded providers it knows,
|
|
81
|
+
otherwise the name the server reports.
|
|
82
|
+
- **Build Doctor.** The build conversation's Build Doctor button (its accessible name and
|
|
83
|
+
both tooltips) and the title of the drawer it opens read `console.ai.buildDoctor`,
|
|
84
|
+
`console.ai.buildDoctorTitle` and `console.ai.buildDoctorDisabledTitle`.
|
|
85
|
+
- **Setup → marketplace.** On `system/marketplace` and the pages under it, the breadcrumb
|
|
86
|
+
segment after System reads `console.breadcrumb.marketplace` instead of the humanized URL
|
|
87
|
+
slug; other `system/*` segments are unchanged. The zh marketplace search placeholder
|
|
88
|
+
reads 「按名称或标识搜索应用…」 instead of 「按名称或 manifest ID 搜索应用…」. The search
|
|
89
|
+
itself is unchanged: it matches the display name, the identifier and the description.
|
|
90
|
+
|
|
91
|
+
All ten locale packs carry the nine new keys; no existing `en` value changes.
|
|
92
|
+
|
|
93
|
+
**Clause-②: yes** — besides the nine pack keys, the public surface widens by four optional
|
|
94
|
+
members: `ActionRunner.setTranslator`, `SocialSignInButtonsProps.buttonText`,
|
|
95
|
+
`LoginFormLabels.socialButton` and `RegisterFormLabels.socialButton`. Nothing is removed,
|
|
96
|
+
renamed or narrowed, no accept set changes, and `@object-ui/react` exports nothing new.
|
|
97
|
+
- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate
|
|
98
|
+
the direction so the next one cannot land (objectui#8198).
|
|
99
|
+
|
|
100
|
+
`check:phantom-deps` judges imports that are not declared; nothing judged the reverse,
|
|
101
|
+
so a declaration could outlive its last consumer indefinitely. That is what happened to
|
|
102
|
+
`recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it
|
|
103
|
+
was removed by hand on objectui#7625, and nothing would have reported the next one. The
|
|
104
|
+
new `pnpm check:unused-deps` asks the reverse question over `dependencies` and
|
|
105
|
+
`optionalDependencies` of every released package.
|
|
106
|
+
|
|
107
|
+
**Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship
|
|
108
|
+
changes: their Vite `external` predicates are path-based and never read `dependencies`,
|
|
109
|
+
so no built artifact moves. What changes is the install graph — a project that imports
|
|
110
|
+
one of the removed packages while depending only on the ObjectUI package that used to
|
|
111
|
+
drag it in will no longer resolve it. Declare it directly; that is the correct
|
|
112
|
+
dependency edge in either case. The removals, by package:
|
|
113
|
+
|
|
114
|
+
- `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields`
|
|
115
|
+
- `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`)
|
|
116
|
+
- `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge`
|
|
117
|
+
- `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod`
|
|
118
|
+
- `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge`
|
|
119
|
+
- `@object-ui/core`: `lodash`, `zod`
|
|
120
|
+
- `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply
|
|
121
|
+
- `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect
|
|
122
|
+
- `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge`
|
|
123
|
+
- `@object-ui/fields`: `clsx`, `tailwind-merge`
|
|
124
|
+
- `@object-ui/console`: `@object-ui/react-runtime`, `sucrase`
|
|
125
|
+
- `@object-ui/auth`: `@object-ui/types`
|
|
126
|
+
- `@object-ui/plugin-calendar`: `@object-ui/fields`
|
|
127
|
+
- `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react`
|
|
128
|
+
- `@object-ui/react`: `react-hook-form`
|
|
129
|
+
|
|
130
|
+
Every one was verified by a whole-package grep before removal — the name appeared nowhere
|
|
131
|
+
under the package but its own manifest and CHANGELOG — and the whole workspace builds,
|
|
132
|
+
type-checks and tests green afterwards.
|
|
133
|
+
- 686d5d9: `AuthGuard`'s `requiredRoles` now matches against `user.positions` — the one published spelling (framework ADR-0090 D3) — keeping the `user.role` scalar fallback only for sessions that carry no `positions` key at all (guest / legacy identities). This restores position-holder admission on protocol-17 deployments (where the retired `roles` key is never emitted) and closes the matching over-admission: a coarse scalar `role` no longer passes a gate whose position the server says the user lacks. Preview mode now emits `positions: [role]` instead of the retired `roles` key — it was the last producer — so preview identities are shaped like protocol-17 sessions for every `positions` consumer. The hand-copied `roles?: string[]` mirror key is removed from the client `AuthUser` type; breaking only for TypeScript consumers that compiled against `AuthUser['roles']` (read `positions` instead — no runtime payload ever carried `roles` at protocol 17). Maintainer ruling 2026-08-22 on objectui#5424.
|
|
134
|
+
- c8da8b3: Stop showing an access-denied screen to a real administrator while their
|
|
135
|
+
adminship is still resolving.
|
|
136
|
+
|
|
137
|
+
`useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not
|
|
138
|
+
arrived yet" and "resolved: not an admin" were the same answer. One of its three
|
|
139
|
+
sources — the active organization member row — is fetched some round trips after
|
|
140
|
+
the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`),
|
|
141
|
+
so an administrator whose adminship lives only in that row rendered at least
|
|
142
|
+
once as a non-admin, and every gate downstream acted on it: the two marketplace
|
|
143
|
+
surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and
|
|
144
|
+
re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home"
|
|
145
|
+
replace>` that the later flip could not undo.
|
|
146
|
+
|
|
147
|
+
**Breaking (published API, hence `minor` per this repo's version policy):**
|
|
148
|
+
`useIsWorkspaceAdmin(): boolean` is replaced by
|
|
149
|
+
`useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old
|
|
150
|
+
name is removed rather than kept alongside, so a call site that ignores the
|
|
151
|
+
third state fails to compile instead of silently refusing an administrator.
|
|
152
|
+
|
|
153
|
+
-const isAdmin = useIsWorkspaceAdmin();
|
|
154
|
+
+const { isAdmin, isResolved } = useWorkspaceAdminStatus();
|
|
155
|
+
|
|
156
|
+
`AuthProvider` gains `isMembershipResolved` on its context — the organization /
|
|
157
|
+
member pipeline has reached a terminal state — because `organizations`,
|
|
158
|
+
`activeOrganization` and `activeMember` read `[]` / `null` / `null` both before
|
|
159
|
+
the pipeline starts and after it finds nothing.
|
|
160
|
+
|
|
161
|
+
No extra wait for administrators: `isResolved` is true the instant `isAdmin` is,
|
|
162
|
+
so an admin the session already identifies through `positions[]` never waits on
|
|
163
|
+
the member row.
|
|
164
|
+
|
|
165
|
+
### Patch Changes
|
|
166
|
+
|
|
167
|
+
- 639114c: Reconcile `@object-ui/app-shell` with `@objectstack/spec` 17.3.0 (objectui#7122).
|
|
168
|
+
|
|
169
|
+
`SchemaDiffEntryKind` gained an `unreachable` member in 17.3.0, and the external
|
|
170
|
+
datasource validation panel labels those kinds through a map that is TOTAL over
|
|
171
|
+
the union on purpose — so an upstream addition fails the build rather than
|
|
172
|
+
rendering a blank cell. That mechanism fired: the package did not compile against
|
|
173
|
+
17.3.0 until the kind was labelled. It now reads "Not checked — remote
|
|
174
|
+
unreachable", following the spec's own ruling that this kind asserts nothing
|
|
175
|
+
about the remote schema and must never be surfaced as "schema changed": it means
|
|
176
|
+
introspection could not complete, which is often transient, and labelling it like
|
|
177
|
+
a mismatch would tell an operator to repair a schema nobody has read.
|
|
178
|
+
|
|
179
|
+
Two internal flow-inspector types were renamed (`FlowNodeLike` →
|
|
180
|
+
`InspectorFlowNode` / `ScopeFlowNode`) because 17.3.0 began exporting its own
|
|
181
|
+
`FlowNodeLike`; neither name is in this package's published entry, so no consumer
|
|
182
|
+
import changes.
|
|
183
|
+
|
|
184
|
+
`@object-ui/auth`: a README sentence claiming the preview-mode prop aligns with
|
|
185
|
+
the spec's `PreviewModeConfig` is corrected — 17.3.0 removed that symbol. The
|
|
186
|
+
`previewMode` prop itself is host-supplied, unchanged, and unaffected.
|
|
187
|
+
- 9b1b105: `useWorkspaceAdminStatus` stops deriving platform-administrator standing from a NAME in the
|
|
188
|
+
session's `positions[]`, and reads the ADR-0095 posture rung (`user.isPlatformAdmin`) instead.
|
|
189
|
+
|
|
190
|
+
Framework objectstack#15948 (declared BREAKING) redefined that array: it used to be the
|
|
191
|
+
`sys_user.role` scalar split on commas, and it is now built by `resolveUserAuthzGrants` from
|
|
192
|
+
ADR-0057 D4 `sys_user_position` assignments. `sys_user_position` is `apiEnabled` with
|
|
193
|
+
unconstrained `position` values, so a caller holding tenant-administration authority can mint a
|
|
194
|
+
row spelling `platform_admin` for one of their own users — and the third leg of this hook, which
|
|
195
|
+
scanned `positions[]` for any admin-sounding name, answered `true` for them. The server was
|
|
196
|
+
already fixed (`platform-admin-gate.ts` dropped its positions leg and four evaluator sites moved
|
|
197
|
+
to the rung), so what such a principal got was a Console painting platform-administrator surfaces
|
|
198
|
+
while every underlying call was refused: misleading, in the direction that reveals platform
|
|
199
|
+
structure to a tenant-controlled account.
|
|
200
|
+
|
|
201
|
+
The array read is narrowed rather than deleted, name by name. `org_owner` and `org_admin` stay:
|
|
202
|
+
`resolveUserAuthzGrants` emits exactly those from the ACTIVE organization's `sys_member.role`, a
|
|
203
|
+
minted spelling of them claims authority inside the tenant that the minter already holds, and they
|
|
204
|
+
are what lets a stamped tenant administrator resolve on the first frame (objectui#5619) instead of
|
|
205
|
+
waiting for the member pipeline. `platform_admin` is dropped and replaced by the rung, which is
|
|
206
|
+
byte-for-byte what the server's own `hasPlatformAdminStanding` returns. `owner`, `admin`,
|
|
207
|
+
`super_admin`, `superadmin` and `system_admin` are dropped from the array read too — no server
|
|
208
|
+
derivation emits any of them into `positions[]` (the membership leg normalizes `owner`/`admin` to
|
|
209
|
+
`org_*` first), so a hit could only ever have been a tenant-written row. All five remain valid for
|
|
210
|
+
the two scalar legs, where they are the raw membership role and the stored `user.role`.
|
|
211
|
+
|
|
212
|
+
The docblock that argued AGAINST reading `user.isPlatformAdmin` — on the grounds that the server
|
|
213
|
+
computed it as `'platform_admin' in positions`, "two spellings for one fact" — is rewritten rather
|
|
214
|
+
than left standing. After objectstack#15948 the flag derives from the unscoped `admin_full_access` grant and
|
|
215
|
+
the array does not, so they can disagree and the rung is right.
|
|
216
|
+
|
|
217
|
+
No behaviour changes for a genuine administrator: a platform administrator on a single-tenant
|
|
218
|
+
deployment still resolves through the flag (objectui#5389 stays green), a tenant administrator
|
|
219
|
+
still resolves through the member row and through `org_owner`/`org_admin`, and the objectui#5619
|
|
220
|
+
`isResolved` semantics are unchanged. Pinned by `__tests__/workspaceAdminRung-8291.test.tsx`.
|
|
221
|
+
- ebfc3c2: `ActiveOrganizationStorage.clear()` now verifies that the persisted key is actually
|
|
222
|
+
gone and, when it is not, both reports the failure and stops `get()` answering from
|
|
223
|
+
the surviving value — instead of swallowing the failed removal (objectui#5731).
|
|
224
|
+
|
|
225
|
+
`clear()` nulled `_memoryValue` and then removed the persisted key inside a
|
|
226
|
+
`try`/`catch` that discarded any failure. Since objectui#5703 `get()` prefers a
|
|
227
|
+
NON-NULL `localStorage` read and only falls through to `_memoryValue`, so the two
|
|
228
|
+
halves of `clear()` were not equally strong: nulling memory always sticks, while a
|
|
229
|
+
removal that did not stick left the key readable and the read order preferred it.
|
|
230
|
+
Sign-out is one of `clear()`'s five callers, so the failure mode was "sign-out does
|
|
231
|
+
not stick", and it was silent — the cleared organization went back on the wire as
|
|
232
|
+
`X-Tenant-ID` on every subsequent request.
|
|
233
|
+
|
|
234
|
+
The removal is now judged by a READ-BACK rather than by catching the throw, which is
|
|
235
|
+
both narrower and wider in the right directions. Wider: a wrapped or proxied
|
|
236
|
+
`localStorage` whose `removeItem` is a silent no-op never throws and leaves identical
|
|
237
|
+
residue, and is now covered. Narrower: SSR and the partitioned-iframe browser where
|
|
238
|
+
every operation throws have nothing readable to resurrect, were already safe, and are
|
|
239
|
+
not reported as failures.
|
|
240
|
+
|
|
241
|
+
A key whose removal could not be verified is quarantined in memory for the rest of the
|
|
242
|
+
page-load: `get()` skips the persisted branch for it and answers from `_memoryValue`,
|
|
243
|
+
which `clear()` has just nulled and which a later `set()` refills with the value that
|
|
244
|
+
write was meant to persist. The quarantine is released as soon as a removal on that key
|
|
245
|
+
sticks. An unstamped `X-Tenant-ID` is a documented state of the edge contract
|
|
246
|
+
(objectui#5279); a re-stamped signed-out organization is not.
|
|
247
|
+
|
|
248
|
+
The failure is not thrown and not returned. All five call sites — sign-out's
|
|
249
|
+
`purgeSignedOutClientCaches`, `switchOrganization`, `deleteOrganization`,
|
|
250
|
+
`leaveOrganization`, and the session-user purge that runs on the SIGN-IN path — arrive
|
|
251
|
+
after the transition they follow up on has already happened, and none can act on a
|
|
252
|
+
storage failure; a `boolean` every caller ignores would read as handled when it is not.
|
|
253
|
+
So the invariant is restored inside `clear()` and the failure is reported to the
|
|
254
|
+
console.
|
|
255
|
+
|
|
256
|
+
A working `localStorage` behaves exactly as before: the removal sticks, nothing is
|
|
257
|
+
quarantined, nothing is reported, and a non-null persisted read is still authoritative.
|
|
258
|
+
`set()`'s swallowed write failure is deliberately untouched — that swallow is
|
|
259
|
+
objectui#5703's memory fallback, and it is the correct kind, because the memory copy
|
|
260
|
+
upholds `set()`'s postcondition where nulling memory could not uphold `clear()`'s.
|
|
261
|
+
- 9a5d669: `ActiveOrganizationStorage.get()` now prefers a non-null `localStorage` read and falls
|
|
262
|
+
back to the in-memory value otherwise, instead of returning the `localStorage` read
|
|
263
|
+
unconditionally — so the fallback is reachable in the browser state it was written for
|
|
264
|
+
(objectui#5703).
|
|
265
|
+
|
|
266
|
+
`set()` already swallowed a failed `localStorage.setItem` into `_memoryValue`, but
|
|
267
|
+
`get()` only consulted `_memoryValue` when the READ itself threw. There is a real
|
|
268
|
+
browser state where the read does not throw and the fallback is nonetheless the only
|
|
269
|
+
copy: `localStorage` present and readable but rejecting writes — Safari private
|
|
270
|
+
browsing, and any quota-exhausted origin, where `setItem` throws `QuotaExceededError`.
|
|
271
|
+
In that state the active org was stored and could not be read back, measured as
|
|
272
|
+
`_memoryValue = org-42` alongside `get() = null`.
|
|
273
|
+
|
|
274
|
+
The cost was silent and lasted the whole session rather than the documented first-boot
|
|
275
|
+
window: `createAuthenticatedFetch` reads `get()`, so `X-Tenant-ID` was never stamped on
|
|
276
|
+
any request. Per the edge contract documented on objectui#5279 that header is a routing
|
|
277
|
+
hint a reader falls through on — the framework scopes from the session — so no row
|
|
278
|
+
visibility rode on this; what was missing is the tenant-routing input, on every request.
|
|
279
|
+
`switchOrganization` also appeared to succeed while the client-side stamp never
|
|
280
|
+
followed.
|
|
281
|
+
|
|
282
|
+
Sign-out is unaffected, and that is the half worth stating: the new fallback fires
|
|
283
|
+
exactly when the `localStorage` read is null, which is the state `clear()` leaves
|
|
284
|
+
behind. It answers `null` there because `clear()` nulls `_memoryValue` too. That
|
|
285
|
+
property is now pinned by test rather than relied upon, so a future `clear()` that only
|
|
286
|
+
removed the persisted key fails a test instead of quietly re-stamping a cleared org.
|
|
287
|
+
|
|
288
|
+
A non-null persisted read still wins over the memory value, so a working `localStorage`
|
|
289
|
+
behaves exactly as before.
|
|
290
|
+
- 32ef595: Restore platform-admin detection for permission-set-derived administrators.
|
|
291
|
+
|
|
292
|
+
`useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace
|
|
293
|
+
gating and the "Build an app" CTAs. Its third source read `user.roles`, a key
|
|
294
|
+
the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it
|
|
295
|
+
to `positions`). An administrator whose adminship comes from the
|
|
296
|
+
`admin_full_access` permission set — the single-tenant deployment shape, where
|
|
297
|
+
there is no organization member row and the server deliberately no longer
|
|
298
|
+
overwrites `user.role` — matched none of the three sources and read as **not an
|
|
299
|
+
administrator**: Setup and Studio simply disappeared for them.
|
|
300
|
+
|
|
301
|
+
The hook now reads `user.positions[]`, the one spelling the session publishes.
|
|
302
|
+
Detection is restored for that path and unchanged everywhere else: an active
|
|
303
|
+
member row with an admin role, a stored `user.role` admin scalar, and
|
|
304
|
+
preview/no-auth mode all behave exactly as before, and nobody who was not an
|
|
305
|
+
administrator becomes one — pinned by four negative cases alongside the
|
|
306
|
+
positive one.
|
|
307
|
+
|
|
308
|
+
Also corrects the now-stale documentation that described the removed spelling:
|
|
309
|
+
the hook's own docblock, the `roles?: string[]` declaration on the client
|
|
310
|
+
`AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and
|
|
311
|
+
two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the
|
|
312
|
+
comment corrections.
|
|
313
|
+
- 343c598: The active organization id is now stored per user, and a change of session user drops the
|
|
314
|
+
previous user's client state wholesale (objectui#5664).
|
|
315
|
+
|
|
316
|
+
`auth-active-organization-id` was a single un-namespaced `localStorage` key while its
|
|
317
|
+
siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`,
|
|
318
|
+
`flow-palette-recents:u:`). On a browser handed from one account to another — a shared
|
|
319
|
+
machine, a kiosk, a handover, a support session — the arriving user's console read the
|
|
320
|
+
PREVIOUS user's organization id. The header workspace chip rendered the previous user's
|
|
321
|
+
workspace for a user whose `organization/list` was empty, and the consequence past the
|
|
322
|
+
cosmetics is the one worth stating: the polluted org context suppressed
|
|
323
|
+
`RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so
|
|
324
|
+
a brand-new user on that browser silently never got the new-user flow at all.
|
|
325
|
+
|
|
326
|
+
Nothing about row visibility rode on this. With the stale id the server answers
|
|
327
|
+
`403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero
|
|
328
|
+
environments; the damage was entirely in what the client believed about itself.
|
|
329
|
+
|
|
330
|
+
Three changes, and the third is the one that closes the class rather than the instance:
|
|
331
|
+
|
|
332
|
+
- The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention
|
|
333
|
+
its siblings already use.
|
|
334
|
+
- It can no longer be written un-namespaced at all. Where no session user is known yet the
|
|
335
|
+
value lives in memory for that page-load only — a namespacing that kept a bare-key
|
|
336
|
+
fallback would re-open the defect the first time a write happened before the user id
|
|
337
|
+
resolved.
|
|
338
|
+
- **A change of session user drops the previous user's client state wholesale.** This is an
|
|
339
|
+
allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so
|
|
340
|
+
the NEXT storage key someone adds without a `:u:` scope is covered before it is written.
|
|
341
|
+
Only device-scoped entries survive: the arriving session's own bearer token, the pointer
|
|
342
|
+
recording whose state the browser holds, and the UI theme.
|
|
343
|
+
|
|
344
|
+
Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a
|
|
345
|
+
non-null `localStorage` read and falls back to the in-memory value, and the memory value is
|
|
346
|
+
nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge
|
|
347
|
+
too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then-
|
|
348
|
+
sign-in path that never reloads the page.
|
|
349
|
+
|
|
350
|
+
Existing browsers are not migrated. A value sitting under the retired bare key is
|
|
351
|
+
unattributable — nothing recorded whose org id it is — so migrating it is precisely the
|
|
352
|
+
defect it would be migrating away from, and it is deleted instead. A signed-in user loses
|
|
353
|
+
nothing durable: the active organization is a server-owned fact that
|
|
354
|
+
`AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no
|
|
355
|
+
active org is held, including the ADR-0081 single-membership repair. One boot re-supplies
|
|
356
|
+
it; users with no organization land on the guided first-run flow, which is the outcome this
|
|
357
|
+
card is about.
|
|
358
|
+
|
|
359
|
+
`apps/console`'s pre-render auth preflight purges every spelling of the active-org key —
|
|
360
|
+
the retired bare one and each `:u:` scope — when it finds a dead bearer token, and
|
|
361
|
+
deliberately leaves the session-user pointer in place so the next sign-in can still tell
|
|
362
|
+
that the browser changed hands.
|
|
363
|
+
- 2dd9443: `purgeSignedOutClientCaches()` — the sweep that drops the signed-out user's
|
|
364
|
+
`objectui:metadata:*` seed cache on sign-out (objectui#5198) — now costs one key when a
|
|
365
|
+
single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5777).
|
|
366
|
+
|
|
367
|
+
The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key `n`
|
|
368
|
+
aborted the walk, so keys `n+1..end` were never swept, and the failure was swallowed —
|
|
369
|
+
`AuthProvider`'s `signOut` believed the purge had completed. The entries this sweeps are
|
|
370
|
+
the previous principal's org-scoped, PERMISSION-FILTERED app list — objectui#5198
|
|
371
|
+
classifies a surviving entry as a cross-principal disclosure on a shared browser, not
|
|
372
|
+
mere staleness — so a partial sweep here is the sharper half of the same defect class
|
|
373
|
+
objectui#5763 fixed on the sign-in path (`sweepStore` in `ActiveOrganizationStorage.ts`).
|
|
374
|
+
|
|
375
|
+
`Object.keys(sessionStorage)` — the reason a guard exists here at all — stays guarded on
|
|
376
|
+
its own; only the per-key guard is new. Same as `sweepStore`, a failed removal here is
|
|
377
|
+
not verified by read-back and not quarantined the way `ActiveOrganizationStorage.clear()`
|
|
378
|
+
(objectui#5731) quarantines a key: this function does not own reads for the metadata
|
|
379
|
+
seed cache (`MetadataProvider` in `@object-ui/app-shell` does), so there is no `get()` to
|
|
380
|
+
guard and nothing to quarantine — adding read-back verification would be a general
|
|
381
|
+
storage-error-handling refactor of the module, out of this card's scope. What is
|
|
382
|
+
mirrored is the reporting channel: a key whose `removeItem` throws is named in a
|
|
383
|
+
`console.warn`, the same channel `sweepStore` and `clear()` use, so a partial sweep is
|
|
384
|
+
discoverable instead of silent.
|
|
385
|
+
|
|
386
|
+
Adds a partial-failure test: a `sessionStorage` whose `removeItem` throws on one metadata
|
|
387
|
+
key, asserting every other metadata key on both sides of it is still swept and unrelated
|
|
388
|
+
non-matching keys are untouched, plus a control that the warning fires only on an actual
|
|
389
|
+
failure.
|
|
390
|
+
- ff2d547: `sweepStore()` — the walk that drops the previous user's `localStorage`/`sessionStorage`
|
|
391
|
+
state on a change of session user (objectui#5664 part 3) — now costs one key when a
|
|
392
|
+
single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5763).
|
|
393
|
+
|
|
394
|
+
The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key
|
|
395
|
+
`n` aborted the walk, so keys `n+1..end` were never swept, and the failure was
|
|
396
|
+
swallowed — `purgePreviousUserClientState()` returned normally and `SessionUserScope.adopt`
|
|
397
|
+
believed the sign-in purge had completed. This is an ALLOWLIST sweep precisely so the
|
|
398
|
+
next un-namespaced key — one nobody has written yet — cannot re-open the cross-user
|
|
399
|
+
pollution class #5664 fixed; a partial sweep is a partial allowlist, and which keys
|
|
400
|
+
survived depended on `Object.keys` iteration order rather than on anything bounded. The
|
|
401
|
+
previous user's org id, recents, favourites, or a `sessionStorage` metadata seed (their
|
|
402
|
+
permission-filtered app list, a cross-principal disclosure per objectui#5198) could all
|
|
403
|
+
land on the wrong side of the abort.
|
|
404
|
+
|
|
405
|
+
`Object.keys(store)` — the reason a guard exists here at all — stays guarded on its
|
|
406
|
+
own; only the per-key guard is new, so one uncooperative key now costs exactly that key.
|
|
407
|
+
|
|
408
|
+
Unlike `ActiveOrganizationStorage.clear()` (objectui#5731), a failed removal here is
|
|
409
|
+
NOT verified by read-back and NOT quarantined: `clear()` owns every future read of its
|
|
410
|
+
one key through `ActiveOrganizationStorage.get()`, so a "still readable" verdict and a
|
|
411
|
+
quarantine are what keep a failed `clear()` from handing the value straight back.
|
|
412
|
+
`sweepStore` walks keys it does not own reads for — another package's recents cache, a
|
|
413
|
+
metadata seed — so there is no `get()` here to guard and nothing to quarantine; adding
|
|
414
|
+
read-back verification for keys this function does not otherwise touch would be a
|
|
415
|
+
general storage-error-handling refactor of the module, which this card is scoped away
|
|
416
|
+
from. What IS mirrored is the reporting channel: a key whose `removeItem` throws is
|
|
417
|
+
named in a `console.warn`, the same channel `clear()` uses, so a partial sweep is
|
|
418
|
+
discoverable instead of silent. The caller (`SessionUserScope.adopt`, on the sign-in
|
|
419
|
+
path, inside an `AuthProvider` effect) still cannot act on the failure and must not
|
|
420
|
+
throw either.
|
|
421
|
+
|
|
422
|
+
A working `localStorage`/`sessionStorage` behaves exactly as before: every
|
|
423
|
+
non-device-scoped key is removed, nothing is reported, and the device-scoped allowlist
|
|
424
|
+
(`auth-session-token`, `auth-session-user-id`, `vite-ui-theme`) is unaffected.
|
|
425
|
+
- 36918bf: `switchOrganization` now re-resolves identity for the organization it just switched
|
|
426
|
+
to by its own explicit decision, instead of depending on an accidental
|
|
427
|
+
`TokenStorage` side effect to notice the switch (objectui#5750).
|
|
428
|
+
|
|
429
|
+
`AuthProvider.switchOrganization` has never called `loadSession()` itself. Identity
|
|
430
|
+
re-resolved across a switch only because `POST /organization/set-active` happens to
|
|
431
|
+
return the SIGNED `token.signature` spelling in `set-auth-token`, which differs from
|
|
432
|
+
the UNSIGNED `session.token` spelling `getSession()` normally stores — so
|
|
433
|
+
`TokenStorage.set` reads the flip as a rotation and the objectui#4467 subscription
|
|
434
|
+
calls `loadSession()` for it (measured and pinned in objectui#5749/#5719). That
|
|
435
|
+
signed spelling is deterministic on the raw session token, not on the organization:
|
|
436
|
+
two switches with no `get-session` landing in between produce the identical signed
|
|
437
|
+
value, so the SECOND `TokenStorage.set` sees no change, never notifies, and identity
|
|
438
|
+
is left answering for whichever organization the FIRST switch targeted even though
|
|
439
|
+
`activeOrganization` already reads as the new one.
|
|
440
|
+
|
|
441
|
+
Reachable in the console via `OrganizationLayout`'s slug-driven effect (the "Manage"
|
|
442
|
+
link on an org card, plus its own "Back to organizations" button) — ordinary
|
|
443
|
+
client-side navigation with no full-page reload and nothing debouncing repeat
|
|
444
|
+
switches. `WorkspaceSwitcher` and `OrganizationsPage`'s own card click were not
|
|
445
|
+
reachable paths for this: both force `window.location.href` immediately after a
|
|
446
|
+
successful switch, and the resulting fresh `AuthProvider` mount always performs an
|
|
447
|
+
authoritative `loadSession()` regardless of how the race above resolved.
|
|
448
|
+
|
|
449
|
+
`switchOrganization` now tracks the organization it last resolved to itself and
|
|
450
|
+
re-resolves explicitly whenever a switch's target differs from that, while
|
|
451
|
+
suppressing the (now redundant) rotation notification for its own `set-active`
|
|
452
|
+
call — so the common single-switch path still spends exactly one `get-session`, not
|
|
453
|
+
two. A generation guard discards a still-in-flight, now-superseded switch's answer
|
|
454
|
+
rather than let it clobber a later switch's fresher one.
|
|
455
|
+
- 934a532: Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the
|
|
456
|
+
unstamped-first-request window in which it is not sent (objectui#5279). Documentation
|
|
457
|
+
only — no behaviour changes.
|
|
458
|
+
|
|
459
|
+
The header had no written contract anywhere, and the shape of the missing information was
|
|
460
|
+
actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a
|
|
461
|
+
search confined to this repo and the framework (`objectstack`) returns zero readers and
|
|
462
|
+
reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was
|
|
463
|
+
held until a cloud-side reading came back non-empty. Without the contract written down,
|
|
464
|
+
the next person to grep reaches the same false conclusion and deletes a live routing
|
|
465
|
+
input.
|
|
466
|
+
|
|
467
|
+
`packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means
|
|
468
|
+
(a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim,
|
|
469
|
+
not an authorization input, not what scopes rows), who stamps it and under exactly which
|
|
470
|
+
condition, who reads it, and what a reader may and may not assume. The framework half is
|
|
471
|
+
stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the
|
|
472
|
+
API-key principal or `session.activeOrganizationId` and from no header — alongside
|
|
473
|
+
`plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability.
|
|
474
|
+
The configuration half is quoted from the contract this package can actually resolve,
|
|
475
|
+
`TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the
|
|
476
|
+
default of a configurable `tenantHeaderName` and `header` ranks second of six
|
|
477
|
+
identification sources behind `subdomain`.
|
|
478
|
+
|
|
479
|
+
The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is
|
|
480
|
+
filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` ->
|
|
481
|
+
`getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at
|
|
482
|
+
all. What a reader observes is documented as **absent, never present-and-empty**, with the
|
|
483
|
+
five situations that open the window and the instruction to fall through to the next
|
|
484
|
+
identification source rather than fail closed. The gap is recorded, deliberately not
|
|
485
|
+
closed: the cloud readers observe today's behaviour, so changing when the header first
|
|
486
|
+
appears is its own decision.
|
|
487
|
+
|
|
488
|
+
Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes
|
|
489
|
+
about the wire — no active organization means no header at all, the stamp is not gated on
|
|
490
|
+
`/api/` the way `Authorization` is, and the active organization overwrites a caller-set
|
|
491
|
+
`X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed.
|
|
492
|
+
|
|
493
|
+
## 17.6.0
|
|
494
|
+
|
|
495
|
+
### Minor Changes
|
|
496
|
+
|
|
497
|
+
- 1b21b1a: `AuthInvitation.status` is the closed four-member union it always documented, enforced at the auth client's wire boundary.
|
|
498
|
+
|
|
499
|
+
The field was declared `status: string` while its own doc comment enumerated
|
|
500
|
+
`'pending' | 'accepted' | 'rejected' | 'canceled'`, and `createAuthClient` cast
|
|
501
|
+
better-auth's `any` responses straight to `AuthInvitation[]` with no check. The
|
|
502
|
+
enumeration was therefore advisory: any value a backend stored reached the
|
|
503
|
+
console untouched, where the invitations screen coloured it through a
|
|
504
|
+
`default: 'secondary'` badge arm and printed it with
|
|
505
|
+
`defaultValue: inv.status` — the raw wire string rendered as interface copy, in
|
|
506
|
+
all ten packs (objectui#3879). Nobody could trigger it today, because the four
|
|
507
|
+
are what better-auth writes; it was the contract that was loose, and a loose
|
|
508
|
+
consumer is where an unexpected value would have hidden.
|
|
509
|
+
|
|
510
|
+
`AuthInvitationStatus` now carries the union and **binds better-auth's own
|
|
511
|
+
`InvitationStatus`** rather than restating it, the way `org-roles.ts` binds the
|
|
512
|
+
spec's `BUILTIN_MEMBERSHIP_ROLES` — a member-for-member copy is the state one
|
|
513
|
+
upstream release away from drifting silently. The runtime list
|
|
514
|
+
(`AUTH_INVITATION_STATUSES`) is derived from a total map keyed by that union, so
|
|
515
|
+
a fifth status upstream is a build failure here rather than a gap in the guard.
|
|
516
|
+
`isAuthInvitationStatus` is exported alongside, and all four invitation-returning
|
|
517
|
+
client methods (`listInvitations`, `listUserInvitations`, `getInvitation`,
|
|
518
|
+
`inviteMember`) narrow their wire rows through it.
|
|
519
|
+
|
|
520
|
+
Behaviour change, stated because it is one: an invitation whose `status` is
|
|
521
|
+
outside the set now **fails loudly** — the call rejects with a message naming the
|
|
522
|
+
value it refused and the four it expected — instead of resolving into a badge.
|
|
523
|
+
Both call paths already render a rejection with a retry, so the throw lands on a
|
|
524
|
+
designed surface. Degrading quietly was the alternative and was deliberately not
|
|
525
|
+
taken: a neutral label is how the raw value shipped in the first place, and
|
|
526
|
+
dropping the row would delete an invitation from an administrative ledger without
|
|
527
|
+
saying so. The console's badge switch is exhaustive over the union now and has no
|
|
528
|
+
`default:` arm, so if better-auth ever adds a member the type-check gate stops the
|
|
529
|
+
build at the one place a human has to choose a colour.
|
|
530
|
+
|
|
531
|
+
Consumers assigning an arbitrary string to `AuthInvitation.status` (a hand-built
|
|
532
|
+
fixture, a mock) will need one of the four members.
|
|
533
|
+
|
|
534
|
+
### Patch Changes
|
|
535
|
+
|
|
536
|
+
- 5458414: Publish relative import specifiers with explicit `.js` extensions so these six packages load under plain Node ESM.
|
|
537
|
+
|
|
538
|
+
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`.
|
|
539
|
+
- 61b097c: Sign-out now drops the client-side caches that belonged to the session it ends,
|
|
540
|
+
and the metadata seed cache is keyed by session identity.
|
|
541
|
+
|
|
542
|
+
`sessionStorage` is per-tab, not per-session, and no sign-out call site reloads
|
|
543
|
+
the page — so the `objectui:metadata:*` entries (the app list the server
|
|
544
|
+
permission-filters per session) and the active-organization id survived into
|
|
545
|
+
whatever happened next in that tab. A second person signing in on a shared,
|
|
546
|
+
kiosk or handover browser was seeded with the previous user's filtered app list.
|
|
547
|
+
Organization scoping did not close this: two users in the same organization
|
|
548
|
+
computed the same cache key.
|
|
549
|
+
|
|
550
|
+
`AuthProvider.signOut()` now purges the `objectui:metadata:` entries and clears
|
|
551
|
+
`ActiveOrganizationStorage` (and the in-memory organization block) on both the
|
|
552
|
+
success and failure paths, and `MetadataProvider` keys each entry by a
|
|
553
|
+
fingerprint of the session token, so an entry that escapes the purge is
|
|
554
|
+
unreadable by the next principal rather than merely undeleted. Entries left by
|
|
555
|
+
another principal are deleted the first time a console mounts.
|
|
556
|
+
- a9e17b4: `auth.forgotPassword.successDescription`'s address hole is now spelled with
|
|
557
|
+
single braces (`{email}`) instead of i18next's double braces (`{{email}}`),
|
|
558
|
+
in all ten locale packs.
|
|
559
|
+
|
|
560
|
+
This is a spelling-only change — rendered output is byte-identical in every
|
|
561
|
+
language, because the hole was never filled by i18next in the first place:
|
|
562
|
+
`ForgotPasswordForm` substitutes the address itself once the user submits
|
|
563
|
+
the form (the label renders before the address exists, so `t()` cannot do
|
|
564
|
+
it). `{{email}}` and a genuinely unfilled i18next hole were indistinguishable
|
|
565
|
+
at the call site, and passing `email` as an interpolation argument — the
|
|
566
|
+
natural "fix" for what looks like a missing argument — would let i18next
|
|
567
|
+
consume the hole and cause the address to be appended a second time
|
|
568
|
+
(objectui#4135).
|
|
569
|
+
|
|
570
|
+
Converging on `{x}` for every hole a component fills downstream of `t()`
|
|
571
|
+
(the convention `resendOtpCountdownText`'s `{seconds}` already used) puts
|
|
572
|
+
this hole outside i18next's `{{…}}` syntax entirely, so the ambiguity is
|
|
573
|
+
gone by construction rather than fenced by an exemption. Accordingly,
|
|
574
|
+
`scripts/check-i18n-call-site-keys.mjs`'s `EXTERNALLY_INTERPOLATED_HOLES`
|
|
575
|
+
registry entry for this key is retired — the gate needs no exemption for a
|
|
576
|
+
hole i18next was never going to touch.
|
|
577
|
+
|
|
578
|
+
`ForgotPasswordForm.tsx`'s replacement marker and its own built-in default
|
|
579
|
+
label move to the same spelling in the same change, as does the inline
|
|
580
|
+
`defaultValue` at `apps/console/src/pages/auth/ForgotPasswordPage.tsx`'s
|
|
581
|
+
call site.
|
|
582
|
+
- Updated dependencies [88085e3]
|
|
583
|
+
- Updated dependencies [279fb13]
|
|
584
|
+
- Updated dependencies [1184192]
|
|
585
|
+
- Updated dependencies [a2a9747]
|
|
586
|
+
- Updated dependencies [af5e292]
|
|
587
|
+
- Updated dependencies [7f96b10]
|
|
588
|
+
- Updated dependencies [f1d4748]
|
|
589
|
+
- Updated dependencies [578e025]
|
|
590
|
+
- Updated dependencies [598c89a]
|
|
591
|
+
- Updated dependencies [b8b9af4]
|
|
592
|
+
- Updated dependencies [97abb24]
|
|
593
|
+
- Updated dependencies [deb157a]
|
|
594
|
+
- Updated dependencies [d2ce342]
|
|
595
|
+
- Updated dependencies [9695da7]
|
|
596
|
+
- Updated dependencies [58b8346]
|
|
597
|
+
- Updated dependencies [3cf4de0]
|
|
598
|
+
- Updated dependencies [c9dc811]
|
|
599
|
+
- Updated dependencies [a0b9e91]
|
|
600
|
+
- Updated dependencies [99bd015]
|
|
601
|
+
- @object-ui/types@17.6.0
|
|
602
|
+
|
|
3
603
|
## 17.5.0
|
|
4
604
|
|
|
5
605
|
### Minor Changes
|