@object-ui/auth 17.6.0 → 18.0.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 +598 -0
- package/README.md +124 -19
- package/dist/ActiveOrganizationStorage.d.ts +32 -1
- package/dist/ActiveOrganizationStorage.d.ts.map +1 -1
- package/dist/ActiveOrganizationStorage.js +72 -3
- package/dist/AuthContext.d.ts +10 -3
- package/dist/AuthContext.d.ts.map +1 -1
- package/dist/AuthProvider.d.ts.map +1 -1
- package/dist/AuthProvider.js +101 -18
- package/dist/LoginForm.d.ts +35 -4
- package/dist/LoginForm.d.ts.map +1 -1
- package/dist/LoginForm.js +16 -4
- package/dist/RegisterForm.d.ts +21 -2
- package/dist/RegisterForm.d.ts.map +1 -1
- package/dist/RegisterForm.js +3 -4
- package/dist/SocialSignInButtons.d.ts +15 -2
- package/dist/SocialSignInButtons.d.ts.map +1 -1
- package/dist/SocialSignInButtons.js +3 -3
- package/dist/UserMenu.d.ts +1 -1
- package/dist/UserMenu.js +1 -1
- package/dist/auth-gate-events.d.ts +6 -4
- package/dist/auth-gate-events.d.ts.map +1 -1
- package/dist/auth-gate-events.js +11 -1
- package/dist/authStyles.d.ts +8 -3
- package/dist/authStyles.d.ts.map +1 -1
- package/dist/authStyles.js +8 -3
- package/dist/createAuthClient.d.ts +15 -1
- package/dist/createAuthClient.d.ts.map +1 -1
- package/dist/createAuthClient.js +54 -1
- package/dist/createAuthenticatedFetch.d.ts +5 -4
- package/dist/createAuthenticatedFetch.d.ts.map +1 -1
- package/dist/createAuthenticatedFetch.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/types.d.ts +53 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/useWorkspaceAdminStatus.d.ts +76 -27
- package/dist/useWorkspaceAdminStatus.d.ts.map +1 -1
- package/dist/useWorkspaceAdminStatus.js +146 -40
- package/package.json +6 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,603 @@
|
|
|
1
1
|
# @object-ui/auth
|
|
2
2
|
|
|
3
|
+
## 18.0.0
|
|
4
|
+
|
|
5
|
+
### Dependency floors
|
|
6
|
+
|
|
7
|
+
Computed from `.objectstack-sha` `06cab4849a8a`: the earliest published `@objectstack/spec` that contains it is `18.0.0`.
|
|
8
|
+
|
|
9
|
+
- `@objectstack/spec` (`dependencies`): `^18.0.0-next.1` → `^18.0.0`
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- 6f3299f: objectui moves to major 18, following `@objectstack/*` from 17 to 18 (objectui#12334). objectui's major is not a count of its own breaking changes: it tracks the major of the `@objectstack/spec` its packages depend on, so that the same major means compatible across the two projects. These packages already declare `@objectstack/*` on the 18 line (objectui#12030), so every package in the fixed release group crosses to 18 together. It is the one `major` that rule admits: objectui's own breaking changes ship as `minor`, described in their entries.
|
|
14
|
+
|
|
15
|
+
The 18 line is released from Changesets pre mode on the `next` tag (`.changeset/pre.json`), as `@objectstack/*` 18 is: these releases are `18.0.0-next.N` on the npm `next` dist-tag, and they do not move `latest`. The first `18.0.0` on `latest` comes when `@objectstack` 18 is generally available and this repository leaves pre mode.
|
|
16
|
+
|
|
17
|
+
What a consumer must do to take it is in the `@objectstack/*` 18 entry of this release (objectui#12030): move its own `@objectstack/*` packages to the 18 line at the same time.
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- f1a177c: `LoginForm` shows its "Don't have an account? Sign up" row only when the caller passes `registerUrl` (objectui#11634). The prop used to default to `'/register'`, so a caller that passed `undefined` to withhold the link got it back. Both console login pages pass `undefined` when the server's `/auth/config` reports `emailPassword.disableSignUp: true`, so a deployment with sign-up turned off still offered a sign-up link. The server refused the sign-up and `/register` sent the user back to the login page.
|
|
22
|
+
|
|
23
|
+
**Behaviour change.** A `LoginForm` rendered without `registerUrl` no longer shows a sign-up link. A caller that left the prop out and relied on the `'/register'` default now passes the URL itself, `registerUrl="/register"`, to keep the link. The console app's login page and `@object-ui/app-shell`'s `DefaultLoginPage` already pass the URL whenever sign-up is on, so neither changes. Leaving the prop out, or passing `undefined`, is the one way to render no link: no second "off" value is added.
|
|
24
|
+
|
|
25
|
+
**Clause-②: no.** The prop's type is unchanged (`registerUrl?: string`). No export, prop, type member or i18n key is added or removed.
|
|
26
|
+
- daa7caf: The console's login and register pages offer a generic sign-up only where the server would accept one (objectui#11691). `/api/v1/auth/config` states the sign-up rule as two keys, `emailPassword.disableSignUp` and `features.audiencePosture`, and the server deliberately does not force the first from the second: under `invite_only` its sign-up route still admits a pending invitee. The pages read only `disableSignUp`, so under the default `invite_only` posture `/login` offered "Sign up" and `/register` refused the finished form with `SELF_REGISTRATION_CLOSED`.
|
|
27
|
+
|
|
28
|
+
Both pages now read both keys:
|
|
29
|
+
|
|
30
|
+
- `disableSignUp: true` still hides sign-up outright, invitation links included.
|
|
31
|
+
- Under `open` or `email_domain`, nothing changes.
|
|
32
|
+
- Under `invite_only`, `/login` shows no "Sign up" link. A visitor who arrived from an invitation (`?redirect=/accept-invitation/ID`, the signed-out bounce of the invitation page) still gets the link, and `/register` still renders the form for them. A deployment with no owner yet (`GET /api/v1/auth/bootstrap-status` answers `hasOwner: false`) keeps the link for its first owner, because the server admits the first account under every posture.
|
|
33
|
+
- Otherwise `/register` says that self-registration is not open and points back to sign-in, before any field is filled in, instead of refusing the submitted form.
|
|
34
|
+
- A server that sends no `audiencePosture` is answered as before, by `disableSignUp` alone. A posture value the console does not recognise reads as closed.
|
|
35
|
+
|
|
36
|
+
**Clause-②: yes.** `@object-ui/auth`'s published `AuthPublicConfig.features` gains an optional `audiencePosture` member, typed as `@objectstack/spec`'s `AudiencePosture`. The package's `@objectstack/spec` range moves from `^17.0.0` to `^17.3.0`, the first release that declares that type. No export, prop or i18n key is added or removed: the register page's explanation reuses the existing `auth.register.errors.selfRegistrationClosed` sentence.
|
|
37
|
+
- dfa15c8: A new workspace now takes its creator's browser timezone when it is created, so its first administrator is no longer asked for a zone the browser already knows (objectui#11908).
|
|
38
|
+
|
|
39
|
+
`createOrganization` accepts an optional `timezone`, an IANA zone such as `Asia/Shanghai`, on both `AuthClient` and the `useAuth()` context. The client sends it as the `timezone` query parameter of `POST /organization/create`; the request body stays `{ name, slug }` (plus `logo` when given). When the zone is absent or empty, no parameter is sent and the server keeps its default zone. A server that does not read the parameter ignores it.
|
|
40
|
+
|
|
41
|
+
The console's create-workspace dialog and the first-run setup page's create branch both pass the browser's `Intl.DateTimeFormat().resolvedOptions().timeZone`. A browser that reports no zone sends none. Existing workspaces are unchanged, and the one-time timezone prompt still asks about a workspace whose zone is still the default.
|
|
42
|
+
- c5406c2: Depend on the `@objectstack/*` 18 prerelease line (objectui#12030).
|
|
43
|
+
|
|
44
|
+
Every `@objectstack/*` range these packages declare in `dependencies` or `peerDependencies` moves from `^17.x` to `^18.0.0-next.0`, the `next` prerelease of `@objectstack/*` 18 (raised to `^18.0.0-next.1` by objectui#12370 before release).
|
|
45
|
+
|
|
46
|
+
**Breaking for consumers:** an install that resolves `@objectstack/spec` (or `@objectstack/client`, `@objectstack/formula`, `@objectstack/lint`) 17.x no longer satisfies these ranges. A consumer must move its own `@objectstack/*` packages to the 18 line, starting with `18.0.0-next.1` (objectui#12370), at the same time as it takes this release.
|
|
47
|
+
|
|
48
|
+
**objectui's own surface moves with it:**
|
|
49
|
+
|
|
50
|
+
- `element:record_picker`'s registration stops publishing the `object`, `filter`, `sort` and `limit` inputs, which spec 18 retires; a JSX page that writes one now gets `unknown-prop`.
|
|
51
|
+
- The `CapabilityMultiSelectField` widget's curated capability set (`@object-ui/fields`) follows spec 18: `manage_mailboxes` in, `manage_org_presentation` out.
|
|
52
|
+
- `element:number` and `element:repeater` nodes that bind no object are accepted, as spec 18's row and `PageComponentSchema` accept them; the two `ELEMENT_*_OBJECT_REQUIRED` refusals are gone.
|
|
53
|
+
|
|
54
|
+
**Retired with the spec:** `@objectstack/spec` 18 tombstones `object-grid`'s `defaultFilters` and refuses it by name, and `ObjectGrid` and `ObjectView` stop reading it (`table.defaultFilters` on an `object-view` included). A view that still carries it renders unfiltered: rename the key to `filter`, whose value is the same rule array.
|
|
55
|
+
- 37db09f: A user whose administrator forced a password change gets the change-password overlay instead of raw `403` responses (objectui#12365).
|
|
56
|
+
|
|
57
|
+
The ObjectStack backend answers an administrator-forced password change with `403 PASSWORD_RESET_REQUIRED`, its own code distinct from natural expiry `PASSWORD_EXPIRED` (objectstack#23287 ruling C′, carried by objectstack#23440).
|
|
58
|
+
|
|
59
|
+
- **`@object-ui/auth`:** the authenticated fetch wrapper now treats `PASSWORD_RESET_REQUIRED` as an auth-policy gate, like `MFA_REQUIRED` and `PASSWORD_EXPIRED`, so `AuthProvider` sets `remediationRequired` for it. Before the backend emits the code this changes nothing.
|
|
60
|
+
- **`@object-ui/app-shell`:** `RemediationOverlay` routes `PASSWORD_RESET_REQUIRED` to the change-password form. It shows the title "You must change your password" for it, because "Your password has expired" is false for a forced reset. `PASSWORD_EXPIRED` keeps its title.
|
|
61
|
+
- **`@object-ui/i18n`:** adds `auth.remediation.password.resetTitle` to all ten built-in language packs.
|
|
62
|
+
|
|
63
|
+
No export is added or removed, and the type of `remediationRequired` (`{ code: string; message: string }`) is unchanged.
|
|
64
|
+
- ff9df88: Depend on `@objectstack/*` `18.0.0-next.1` (objectui#12370).
|
|
65
|
+
|
|
66
|
+
Every `@objectstack/*` range these packages declare moves from `^18.0.0-next.0` to `^18.0.0-next.1`; the console and the runner bundle `18.0.0-next.1`. A consumer moves its own `@objectstack/*` packages to `18.0.0-next.1` with this release.
|
|
67
|
+
|
|
68
|
+
**Accept sets that narrow with it.** `@object-ui/types` validates page action rows and view data bindings through the spec's own rows, so its faces (`safeValidateSchema` and `StrictAnyComponentSchema`) now refuse what `18.0.0-next.1` refuses. Each was accepted on `18.0.0-next.0`:
|
|
69
|
+
|
|
70
|
+
- `onSuccess` on an `action:button` / `action:icon` row, or an `action:group` / `action:menu` member, whose executor is the `url`, `navigation`, `modal`, `flow` or `form` built-in, or that has no executor at all. Write the hop on an `api` or `script` action (or a host handler name), or drop it.
|
|
71
|
+
- `onSuccess` beside `operation: 'update'`, and an `onSuccess` block carrying a key outside `{ navigate, openIn }`.
|
|
72
|
+
- `actionType: 'navigation'` on a row (and `type: 'navigation'` on a container member): write `'url'`.
|
|
73
|
+
- A `url` executor's `target` that is not openable, judged on the executor the runner runs, including a row whose `name` selects it.
|
|
74
|
+
- A view `data` binding with `provider: 'api'`: bind the object or static rows; an API call is the page element's binding.
|
|
75
|
+
- 8ed7e5a: `LoginForm` gains an optional `hideHeader` prop and a password reveal toggle, and the shared auth form styles take the console login artboard's sizes (objectui#12600).
|
|
76
|
+
|
|
77
|
+
- **`hideHeader`** (default `false`) renders no header — no icon disc, no title, no description — for a host that draws its own. Left unset, the header renders exactly as before. Under SSO-only mode a headless form still shows the SSO-only description, as one line above the federated buttons.
|
|
78
|
+
- **Password reveal toggle.** The password field carries a 28 px show/hide button. Its accessible name comes from two new optional `LoginFormLabels` members, `showPasswordText` and `hidePasswordText` (defaults "Show password" / "Hide password").
|
|
79
|
+
- **Shared styles, every auth form:** `AUTH_INPUT_CLASS` and `AUTH_PRIMARY_BUTTON_CLASS` are 40 px tall (`h-10`, were `h-11`); the primary button is one flat `bg-primary` fill (was a `--primary` gradient); `AUTH_FIELD_LABEL_CLASS` is 13 px. `LoginForm`'s "Sign up" line is 13 px and its enterprise SSO button is 40 px with the inputs' radius.
|
|
80
|
+
- `LoginForm`'s root caps its width at 400 px (`sm:max-w-[400px]`) instead of fixing it there (`sm:w-[400px]`), so it fits a host card narrower than 400 px; wherever it had 400 px or more, nothing changes.
|
|
81
|
+
|
|
82
|
+
**Clause-②: yes.** Added: `LoginFormProps.hideHeader`, `LoginFormLabels.showPasswordText`, `LoginFormLabels.hidePasswordText`, all optional. Nothing is removed or renamed.
|
|
83
|
+
- 9b69e13: `LoginForm` no longer tells an SSO-enforced environment's users to use a password (objectui#12682).
|
|
84
|
+
|
|
85
|
+
- **While the auth config resolves**, the header shows its title and no description. It used to show the password form's description ("Enter your email and password to continue", or the `description` the host passed) because SSO enforcement was not known yet. Once the config answers, the description is what it was: `labels.ssoOnlyDescription` under SSO-only mode, `description` otherwise. A failed config read still falls back to the password form and its description.
|
|
86
|
+
- **Under SSO-only mode** (`features.ssoEnforced`), the provider buttons are no longer followed by an "or continue with email" divider with no email form under it. The divider now renders only while the password form is shown, so it comes back with the form when "Use a password instead" is clicked. A server that does not enforce SSO keeps the divider exactly as before.
|
|
87
|
+
- **`SocialSignInButtons` gains `hideDivider`** (optional, default `false`), which omits the divider under the buttons. Left unset, the component renders exactly as before, so `RegisterForm` and every other caller are unchanged.
|
|
88
|
+
|
|
89
|
+
**Clause-②: yes.** Added: `SocialSignInButtonsProps.hideDivider`, optional. Nothing is removed or renamed.
|
|
90
|
+
- 84b8ffa: The console splash no longer waits for requests the first screen does not need (objectui#12809).
|
|
91
|
+
|
|
92
|
+
- **The organization chain runs in parallel.** When the `get-session` answer carries the organization plugin's `activeOrganizationId`, `AuthProvider` names the active organization from that id and the `organization/list` answer, and asks for the member row (`get-active-member`) at the same time as the list. It no longer asks for `get-full-organization` on boot: nothing the console reads uses the members or invitations that answer adds. An id the list does not carry names no organization, and the stored id is cleared, as before. A session answer without the key (a sign-in response, or a server without the organization plugin) takes the old path. Switching organization, the single-membership repair, sign-in and sign-out are unchanged.
|
|
93
|
+
- **`AuthClientSession.activeOrganizationId`** is a new optional member: the id better-auth's organization plugin puts on the session, or `null` when the session holds none.
|
|
94
|
+
- **The console waits only for the metadata the current route reads.** `AppContent` used to hold the splash until the object, dashboard, report and page lists had all loaded, on every route. It now waits for the lists the current route reads (an object page waits for objects; a dashboard for dashboards and objects). After the first screen, opening a route whose lists have not loaded shows a skeleton where the page will be, inside the console, instead of an empty list or a "not found". A route a host adds through `extraRoutes` still waits for all four lists.
|
|
95
|
+
|
|
96
|
+
### Patch Changes
|
|
97
|
+
|
|
98
|
+
- d75f212: Published text no longer teaches the retired view-level `provider: 'api'` data source (objectui#12364). A view binds `provider: 'object'` or `provider: 'value'`; a metadata-declared API call is the page element's `dataSource` binding. Text only: no schema accepts or refuses anything new, and no renderer behaves differently.
|
|
99
|
+
|
|
100
|
+
- **`@object-ui/types`:** the `object-tree` `data` description drops the `api` provider from the providers it names.
|
|
101
|
+
- **`@object-ui/plugin-grid` / `@object-ui/plugin-map`:** the `data` input descriptions drop the `api` provider.
|
|
102
|
+
- **`@object-ui/plugin-calendar` / `@object-ui/plugin-gantt` / `@object-ui/plugin-map`:** the error thrown when no `DataSource` is available now reads `DataSource required for the object provider` (was `DataSource required for object/api providers`). It fires in exactly the same conditions as before.
|
|
103
|
+
- **`@object-ui/react` / `@object-ui/auth`:** the README and docblocks describe `SchemaRendererProvider`'s `apiFetch` as what it is now: the host's authenticated fetch the renderers use for their own reads of the host's API (chart metadata, dataset dimension labels, record-permission probes). It is not a channel for a view's data source.
|
|
104
|
+
- **`@object-ui/app-shell`:** comment wording only in `ConsoleShell`, `useConsoleActionRuntime` and `ObjectView` (these comments ship in the built JS): no behaviour change.
|
|
105
|
+
- **`@object-ui/core`:** docblock wording only on `normalizeListViewSchema` and `resolveRecordSourceObjectName` (these ship in `dist/*.d.ts` and the built JS): no behaviour change.
|
|
106
|
+
- 6e141d9: After a reload, the console no longer keeps the previous organization's id once the server says the session has no active organization (objectui#12698). better-auth clears a session's active organization before it refuses a `set-active` to an organization the user is not a member of. On the next boot `AuthProvider` asked the server, heard "none", and left the old id in `ActiveOrganizationStorage`, so `X-Tenant-ID` and the metadata seed cache kept naming that organization. `refreshOrganizations` now clears the stored id whenever the server answers with no active organization, including when the single-membership repair (ADR-0081) is refused. When the repair succeeds, the server is re-stamped and the stored id follows the server's answer, as before. Nothing is added to or removed from the package entry.
|
|
107
|
+
|
|
108
|
+
The console header (`WorkspaceSwitcher`, `CurrentOrganizationIndicator`) now names only the organization the server reports as active, and shows no organization at all when there is none, instead of falling back to the first membership.
|
|
109
|
+
- 5d69b0e: `AuthProvider` no longer asks `get-session` a second time when the console has just validated the stored session before rendering. The validated answer becomes the provider's first state, once per page load; signing in or out, switching organisation and refreshing the session read it from the server as before.
|
|
110
|
+
|
|
111
|
+
## 17.7.0
|
|
112
|
+
|
|
113
|
+
### Minor Changes
|
|
114
|
+
|
|
115
|
+
- b57107d: fix(auth,app-shell,console): a browser that changes hands no longer keeps the previous account's UI language
|
|
116
|
+
|
|
117
|
+
Both device language slots (`objectui-locale` and `objectui-locale-seed`) hold the
|
|
118
|
+
language of whoever last signed in on the browser. A boot resolves its language from
|
|
119
|
+
them before anyone is signed in. When the session then turns out to belong to someone
|
|
120
|
+
else (an SSO redirect back into the console, or a sign-in in another window),
|
|
121
|
+
`SessionUserScope.adopt` already purged both slots (objectui#5664). But the live
|
|
122
|
+
language had already been derived from them, and nothing re-derived it. A new account
|
|
123
|
+
with no `sys_user.locale` of its own therefore read the previous account's language
|
|
124
|
+
for its whole first session.
|
|
125
|
+
|
|
126
|
+
- `@object-ui/auth` (new exports, hence minor): `getSessionOwnerChangeCount()` and
|
|
127
|
+
`subscribeSessionOwnerChange(listener)` report that the change-of-owner purge ran
|
|
128
|
+
in this page-load. They are held in memory, with no new storage key.
|
|
129
|
+
`SessionUserScope.adopt` now returns `true` when it purged a previous owner.
|
|
130
|
+
- `@object-ui/app-shell`: `useSignedInUserLocale` resets the UI language on an owner
|
|
131
|
+
change, the same way a boot of the swept storage would resolve it. It then
|
|
132
|
+
re-reads the new owner's `sys_user.locale`, which applies after the reset.
|
|
133
|
+
- `@object-ui/console`: `LocalizationFetchProvider` and the boot-time seed fetch no
|
|
134
|
+
longer write an answer that was requested before an owner change. Such an answer
|
|
135
|
+
describes the previous owner.
|
|
136
|
+
- `@object-ui/i18n` (docs only): the seed was documented as the "tenant's" locale.
|
|
137
|
+
It is the server's resolved locale for the signed-in caller: their own
|
|
138
|
+
`sys_user.locale`, else `Accept-Language`, else the deployment default. The docs
|
|
139
|
+
now say so, and declare what a visitor who has not signed in reads: the last
|
|
140
|
+
owner's language, until someone else signs in.
|
|
141
|
+
- 7ea8118: fix(auth, console): a registration started from an invitation link comes back to the invitation after email verification
|
|
142
|
+
|
|
143
|
+
better-auth builds the verification mail's link server-side from the
|
|
144
|
+
`/sign-up/email` request's `callbackURL`, defaulting it to `/`. The console kept
|
|
145
|
+
`?redirect=/accept-invitation/ID` from the login page through `/register` to the
|
|
146
|
+
"check your inbox" screen, but `createAuthClient().signUp` never sent a
|
|
147
|
+
`callbackURL`, so the mail read `callbackURL=/` and the invitee verified onto the
|
|
148
|
+
workspace picker ("Create workspace") instead of the invitation.
|
|
149
|
+
|
|
150
|
+
`@object-ui/auth` (additive):
|
|
151
|
+
|
|
152
|
+
- `SignUpData.callbackURL` is forwarded verbatim to `/sign-up/email`. Absent, the
|
|
153
|
+
key stays off the wire and the server default is untouched.
|
|
154
|
+
- `useAuth().signUp(name, email, password, callbackURL?)` takes it as an optional
|
|
155
|
+
fourth argument.
|
|
156
|
+
- `RegisterForm` gains a `verificationCallbackURL` prop, forwarded to `signUp`.
|
|
157
|
+
|
|
158
|
+
`@object-ui/console`:
|
|
159
|
+
|
|
160
|
+
- `RegisterPage` passes a safe `?redirect=` as the verification callback, and the
|
|
161
|
+
verify-email prompt's "Resend" sends the same value. Before, Resend sent the
|
|
162
|
+
bare router path, which the server redirects to at the ORIGIN root, outside
|
|
163
|
+
the `/_console` mount.
|
|
164
|
+
- Both resolve the route through the new `withConsoleBaseRootRelative`. The server
|
|
165
|
+
never sees `<base href>`, and better-auth refuses a document-relative `./…`
|
|
166
|
+
callback — what `withConsoleBase` answers in the embedded build — with
|
|
167
|
+
`403 INVALID_CALLBACK_URL`, failing the whole sign-up.
|
|
168
|
+
- 328abeb: Four Console surfaces that read English under a zh-CN session now read the session's
|
|
169
|
+
language (objectui#10900). English stays the default.
|
|
170
|
+
|
|
171
|
+
- **The generic action success toast.** When an action declares no `successMessage`, the
|
|
172
|
+
runner falls back to "Action completed successfully".
|
|
173
|
+
That fallback now goes through a translator: `ActionRunner.setTranslator(translate)` is
|
|
174
|
+
new in `@object-ui/core`, and `<ActionProvider>` and `useActionRunner` in
|
|
175
|
+
`@object-ui/react` install the session's `t` on the runner they build. The pack key is
|
|
176
|
+
`actions.completedSuccessfully`. An author's `successMessage` still reaches the toast
|
|
177
|
+
untranslated, and a runner with no translator installed still toasts the English sentence.
|
|
178
|
+
- **The social sign-in buttons on the login and sign-up pages.** `SocialSignInButtons`
|
|
179
|
+
takes a new `buttonText` prop, a template whose `{provider}` is replaced with the
|
|
180
|
+
provider's display name; unset, the buttons keep "Continue with {provider}" and
|
|
181
|
+
"Sign up with {provider}". `LoginForm` and `RegisterForm` take a new `socialButton`
|
|
182
|
+
label and pass it, with their existing `orText` label, to the buttons. `orText` was
|
|
183
|
+
documented as the divider label but rendered nowhere; it now sets the divider under the
|
|
184
|
+
buttons, and its documented default is corrected from "or" to "or continue with email",
|
|
185
|
+
the text that divider has always shown. The console's login and sign-up pages and
|
|
186
|
+
`@object-ui/app-shell`'s `DefaultLoginPage` / `DefaultRegisterPage` pass the new
|
|
187
|
+
`auth.login.*` and `auth.register.*` `socialButton` / `orText` keys. The provider's display
|
|
188
|
+
name is inserted as-is: the component's own label for the branded providers it knows,
|
|
189
|
+
otherwise the name the server reports.
|
|
190
|
+
- **Build Doctor.** The build conversation's Build Doctor button (its accessible name and
|
|
191
|
+
both tooltips) and the title of the drawer it opens read `console.ai.buildDoctor`,
|
|
192
|
+
`console.ai.buildDoctorTitle` and `console.ai.buildDoctorDisabledTitle`.
|
|
193
|
+
- **Setup → marketplace.** On `system/marketplace` and the pages under it, the breadcrumb
|
|
194
|
+
segment after System reads `console.breadcrumb.marketplace` instead of the humanized URL
|
|
195
|
+
slug; other `system/*` segments are unchanged. The zh marketplace search placeholder
|
|
196
|
+
reads 「按名称或标识搜索应用…」 instead of 「按名称或 manifest ID 搜索应用…」. The search
|
|
197
|
+
itself is unchanged: it matches the display name, the identifier and the description.
|
|
198
|
+
|
|
199
|
+
All ten locale packs carry the nine new keys; no existing `en` value changes.
|
|
200
|
+
|
|
201
|
+
**Clause-②: yes** — besides the nine pack keys, the public surface widens by four optional
|
|
202
|
+
members: `ActionRunner.setTranslator`, `SocialSignInButtonsProps.buttonText`,
|
|
203
|
+
`LoginFormLabels.socialButton` and `RegisterFormLabels.socialButton`. Nothing is removed,
|
|
204
|
+
renamed or narrowed, no accept set changes, and `@object-ui/react` exports nothing new.
|
|
205
|
+
- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate
|
|
206
|
+
the direction so the next one cannot land (objectui#8198).
|
|
207
|
+
|
|
208
|
+
`check:phantom-deps` judges imports that are not declared; nothing judged the reverse,
|
|
209
|
+
so a declaration could outlive its last consumer indefinitely. That is what happened to
|
|
210
|
+
`recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it
|
|
211
|
+
was removed by hand on objectui#7625, and nothing would have reported the next one. The
|
|
212
|
+
new `pnpm check:unused-deps` asks the reverse question over `dependencies` and
|
|
213
|
+
`optionalDependencies` of every released package.
|
|
214
|
+
|
|
215
|
+
**Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship
|
|
216
|
+
changes: their Vite `external` predicates are path-based and never read `dependencies`,
|
|
217
|
+
so no built artifact moves. What changes is the install graph — a project that imports
|
|
218
|
+
one of the removed packages while depending only on the ObjectUI package that used to
|
|
219
|
+
drag it in will no longer resolve it. Declare it directly; that is the correct
|
|
220
|
+
dependency edge in either case. The removals, by package:
|
|
221
|
+
|
|
222
|
+
- `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields`
|
|
223
|
+
- `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`)
|
|
224
|
+
- `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge`
|
|
225
|
+
- `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod`
|
|
226
|
+
- `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge`
|
|
227
|
+
- `@object-ui/core`: `lodash`, `zod`
|
|
228
|
+
- `@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
|
|
229
|
+
- `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect
|
|
230
|
+
- `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge`
|
|
231
|
+
- `@object-ui/fields`: `clsx`, `tailwind-merge`
|
|
232
|
+
- `@object-ui/console`: `@object-ui/react-runtime`, `sucrase`
|
|
233
|
+
- `@object-ui/auth`: `@object-ui/types`
|
|
234
|
+
- `@object-ui/plugin-calendar`: `@object-ui/fields`
|
|
235
|
+
- `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react`
|
|
236
|
+
- `@object-ui/react`: `react-hook-form`
|
|
237
|
+
|
|
238
|
+
Every one was verified by a whole-package grep before removal — the name appeared nowhere
|
|
239
|
+
under the package but its own manifest and CHANGELOG — and the whole workspace builds,
|
|
240
|
+
type-checks and tests green afterwards.
|
|
241
|
+
- 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.
|
|
242
|
+
- c8da8b3: Stop showing an access-denied screen to a real administrator while their
|
|
243
|
+
adminship is still resolving.
|
|
244
|
+
|
|
245
|
+
`useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not
|
|
246
|
+
arrived yet" and "resolved: not an admin" were the same answer. One of its three
|
|
247
|
+
sources — the active organization member row — is fetched some round trips after
|
|
248
|
+
the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`),
|
|
249
|
+
so an administrator whose adminship lives only in that row rendered at least
|
|
250
|
+
once as a non-admin, and every gate downstream acted on it: the two marketplace
|
|
251
|
+
surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and
|
|
252
|
+
re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home"
|
|
253
|
+
replace>` that the later flip could not undo.
|
|
254
|
+
|
|
255
|
+
**Breaking (published API, hence `minor` per this repo's version policy):**
|
|
256
|
+
`useIsWorkspaceAdmin(): boolean` is replaced by
|
|
257
|
+
`useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old
|
|
258
|
+
name is removed rather than kept alongside, so a call site that ignores the
|
|
259
|
+
third state fails to compile instead of silently refusing an administrator.
|
|
260
|
+
|
|
261
|
+
-const isAdmin = useIsWorkspaceAdmin();
|
|
262
|
+
+const { isAdmin, isResolved } = useWorkspaceAdminStatus();
|
|
263
|
+
|
|
264
|
+
`AuthProvider` gains `isMembershipResolved` on its context — the organization /
|
|
265
|
+
member pipeline has reached a terminal state — because `organizations`,
|
|
266
|
+
`activeOrganization` and `activeMember` read `[]` / `null` / `null` both before
|
|
267
|
+
the pipeline starts and after it finds nothing.
|
|
268
|
+
|
|
269
|
+
No extra wait for administrators: `isResolved` is true the instant `isAdmin` is,
|
|
270
|
+
so an admin the session already identifies through `positions[]` never waits on
|
|
271
|
+
the member row.
|
|
272
|
+
|
|
273
|
+
### Patch Changes
|
|
274
|
+
|
|
275
|
+
- 639114c: Reconcile `@object-ui/app-shell` with `@objectstack/spec` 17.3.0 (objectui#7122).
|
|
276
|
+
|
|
277
|
+
`SchemaDiffEntryKind` gained an `unreachable` member in 17.3.0, and the external
|
|
278
|
+
datasource validation panel labels those kinds through a map that is TOTAL over
|
|
279
|
+
the union on purpose — so an upstream addition fails the build rather than
|
|
280
|
+
rendering a blank cell. That mechanism fired: the package did not compile against
|
|
281
|
+
17.3.0 until the kind was labelled. It now reads "Not checked — remote
|
|
282
|
+
unreachable", following the spec's own ruling that this kind asserts nothing
|
|
283
|
+
about the remote schema and must never be surfaced as "schema changed": it means
|
|
284
|
+
introspection could not complete, which is often transient, and labelling it like
|
|
285
|
+
a mismatch would tell an operator to repair a schema nobody has read.
|
|
286
|
+
|
|
287
|
+
Two internal flow-inspector types were renamed (`FlowNodeLike` →
|
|
288
|
+
`InspectorFlowNode` / `ScopeFlowNode`) because 17.3.0 began exporting its own
|
|
289
|
+
`FlowNodeLike`; neither name is in this package's published entry, so no consumer
|
|
290
|
+
import changes.
|
|
291
|
+
|
|
292
|
+
`@object-ui/auth`: a README sentence claiming the preview-mode prop aligns with
|
|
293
|
+
the spec's `PreviewModeConfig` is corrected — 17.3.0 removed that symbol. The
|
|
294
|
+
`previewMode` prop itself is host-supplied, unchanged, and unaffected.
|
|
295
|
+
- 9b1b105: `useWorkspaceAdminStatus` stops deriving platform-administrator standing from a NAME in the
|
|
296
|
+
session's `positions[]`, and reads the ADR-0095 posture rung (`user.isPlatformAdmin`) instead.
|
|
297
|
+
|
|
298
|
+
Framework objectstack#15948 (declared BREAKING) redefined that array: it used to be the
|
|
299
|
+
`sys_user.role` scalar split on commas, and it is now built by `resolveUserAuthzGrants` from
|
|
300
|
+
ADR-0057 D4 `sys_user_position` assignments. `sys_user_position` is `apiEnabled` with
|
|
301
|
+
unconstrained `position` values, so a caller holding tenant-administration authority can mint a
|
|
302
|
+
row spelling `platform_admin` for one of their own users — and the third leg of this hook, which
|
|
303
|
+
scanned `positions[]` for any admin-sounding name, answered `true` for them. The server was
|
|
304
|
+
already fixed (`platform-admin-gate.ts` dropped its positions leg and four evaluator sites moved
|
|
305
|
+
to the rung), so what such a principal got was a Console painting platform-administrator surfaces
|
|
306
|
+
while every underlying call was refused: misleading, in the direction that reveals platform
|
|
307
|
+
structure to a tenant-controlled account.
|
|
308
|
+
|
|
309
|
+
The array read is narrowed rather than deleted, name by name. `org_owner` and `org_admin` stay:
|
|
310
|
+
`resolveUserAuthzGrants` emits exactly those from the ACTIVE organization's `sys_member.role`, a
|
|
311
|
+
minted spelling of them claims authority inside the tenant that the minter already holds, and they
|
|
312
|
+
are what lets a stamped tenant administrator resolve on the first frame (objectui#5619) instead of
|
|
313
|
+
waiting for the member pipeline. `platform_admin` is dropped and replaced by the rung, which is
|
|
314
|
+
byte-for-byte what the server's own `hasPlatformAdminStanding` returns. `owner`, `admin`,
|
|
315
|
+
`super_admin`, `superadmin` and `system_admin` are dropped from the array read too — no server
|
|
316
|
+
derivation emits any of them into `positions[]` (the membership leg normalizes `owner`/`admin` to
|
|
317
|
+
`org_*` first), so a hit could only ever have been a tenant-written row. All five remain valid for
|
|
318
|
+
the two scalar legs, where they are the raw membership role and the stored `user.role`.
|
|
319
|
+
|
|
320
|
+
The docblock that argued AGAINST reading `user.isPlatformAdmin` — on the grounds that the server
|
|
321
|
+
computed it as `'platform_admin' in positions`, "two spellings for one fact" — is rewritten rather
|
|
322
|
+
than left standing. After objectstack#15948 the flag derives from the unscoped `admin_full_access` grant and
|
|
323
|
+
the array does not, so they can disagree and the rung is right.
|
|
324
|
+
|
|
325
|
+
No behaviour changes for a genuine administrator: a platform administrator on a single-tenant
|
|
326
|
+
deployment still resolves through the flag (objectui#5389 stays green), a tenant administrator
|
|
327
|
+
still resolves through the member row and through `org_owner`/`org_admin`, and the objectui#5619
|
|
328
|
+
`isResolved` semantics are unchanged. Pinned by `__tests__/workspaceAdminRung-8291.test.tsx`.
|
|
329
|
+
- ebfc3c2: `ActiveOrganizationStorage.clear()` now verifies that the persisted key is actually
|
|
330
|
+
gone and, when it is not, both reports the failure and stops `get()` answering from
|
|
331
|
+
the surviving value — instead of swallowing the failed removal (objectui#5731).
|
|
332
|
+
|
|
333
|
+
`clear()` nulled `_memoryValue` and then removed the persisted key inside a
|
|
334
|
+
`try`/`catch` that discarded any failure. Since objectui#5703 `get()` prefers a
|
|
335
|
+
NON-NULL `localStorage` read and only falls through to `_memoryValue`, so the two
|
|
336
|
+
halves of `clear()` were not equally strong: nulling memory always sticks, while a
|
|
337
|
+
removal that did not stick left the key readable and the read order preferred it.
|
|
338
|
+
Sign-out is one of `clear()`'s five callers, so the failure mode was "sign-out does
|
|
339
|
+
not stick", and it was silent — the cleared organization went back on the wire as
|
|
340
|
+
`X-Tenant-ID` on every subsequent request.
|
|
341
|
+
|
|
342
|
+
The removal is now judged by a READ-BACK rather than by catching the throw, which is
|
|
343
|
+
both narrower and wider in the right directions. Wider: a wrapped or proxied
|
|
344
|
+
`localStorage` whose `removeItem` is a silent no-op never throws and leaves identical
|
|
345
|
+
residue, and is now covered. Narrower: SSR and the partitioned-iframe browser where
|
|
346
|
+
every operation throws have nothing readable to resurrect, were already safe, and are
|
|
347
|
+
not reported as failures.
|
|
348
|
+
|
|
349
|
+
A key whose removal could not be verified is quarantined in memory for the rest of the
|
|
350
|
+
page-load: `get()` skips the persisted branch for it and answers from `_memoryValue`,
|
|
351
|
+
which `clear()` has just nulled and which a later `set()` refills with the value that
|
|
352
|
+
write was meant to persist. The quarantine is released as soon as a removal on that key
|
|
353
|
+
sticks. An unstamped `X-Tenant-ID` is a documented state of the edge contract
|
|
354
|
+
(objectui#5279); a re-stamped signed-out organization is not.
|
|
355
|
+
|
|
356
|
+
The failure is not thrown and not returned. All five call sites — sign-out's
|
|
357
|
+
`purgeSignedOutClientCaches`, `switchOrganization`, `deleteOrganization`,
|
|
358
|
+
`leaveOrganization`, and the session-user purge that runs on the SIGN-IN path — arrive
|
|
359
|
+
after the transition they follow up on has already happened, and none can act on a
|
|
360
|
+
storage failure; a `boolean` every caller ignores would read as handled when it is not.
|
|
361
|
+
So the invariant is restored inside `clear()` and the failure is reported to the
|
|
362
|
+
console.
|
|
363
|
+
|
|
364
|
+
A working `localStorage` behaves exactly as before: the removal sticks, nothing is
|
|
365
|
+
quarantined, nothing is reported, and a non-null persisted read is still authoritative.
|
|
366
|
+
`set()`'s swallowed write failure is deliberately untouched — that swallow is
|
|
367
|
+
objectui#5703's memory fallback, and it is the correct kind, because the memory copy
|
|
368
|
+
upholds `set()`'s postcondition where nulling memory could not uphold `clear()`'s.
|
|
369
|
+
- 9a5d669: `ActiveOrganizationStorage.get()` now prefers a non-null `localStorage` read and falls
|
|
370
|
+
back to the in-memory value otherwise, instead of returning the `localStorage` read
|
|
371
|
+
unconditionally — so the fallback is reachable in the browser state it was written for
|
|
372
|
+
(objectui#5703).
|
|
373
|
+
|
|
374
|
+
`set()` already swallowed a failed `localStorage.setItem` into `_memoryValue`, but
|
|
375
|
+
`get()` only consulted `_memoryValue` when the READ itself threw. There is a real
|
|
376
|
+
browser state where the read does not throw and the fallback is nonetheless the only
|
|
377
|
+
copy: `localStorage` present and readable but rejecting writes — Safari private
|
|
378
|
+
browsing, and any quota-exhausted origin, where `setItem` throws `QuotaExceededError`.
|
|
379
|
+
In that state the active org was stored and could not be read back, measured as
|
|
380
|
+
`_memoryValue = org-42` alongside `get() = null`.
|
|
381
|
+
|
|
382
|
+
The cost was silent and lasted the whole session rather than the documented first-boot
|
|
383
|
+
window: `createAuthenticatedFetch` reads `get()`, so `X-Tenant-ID` was never stamped on
|
|
384
|
+
any request. Per the edge contract documented on objectui#5279 that header is a routing
|
|
385
|
+
hint a reader falls through on — the framework scopes from the session — so no row
|
|
386
|
+
visibility rode on this; what was missing is the tenant-routing input, on every request.
|
|
387
|
+
`switchOrganization` also appeared to succeed while the client-side stamp never
|
|
388
|
+
followed.
|
|
389
|
+
|
|
390
|
+
Sign-out is unaffected, and that is the half worth stating: the new fallback fires
|
|
391
|
+
exactly when the `localStorage` read is null, which is the state `clear()` leaves
|
|
392
|
+
behind. It answers `null` there because `clear()` nulls `_memoryValue` too. That
|
|
393
|
+
property is now pinned by test rather than relied upon, so a future `clear()` that only
|
|
394
|
+
removed the persisted key fails a test instead of quietly re-stamping a cleared org.
|
|
395
|
+
|
|
396
|
+
A non-null persisted read still wins over the memory value, so a working `localStorage`
|
|
397
|
+
behaves exactly as before.
|
|
398
|
+
- 32ef595: Restore platform-admin detection for permission-set-derived administrators.
|
|
399
|
+
|
|
400
|
+
`useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace
|
|
401
|
+
gating and the "Build an app" CTAs. Its third source read `user.roles`, a key
|
|
402
|
+
the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it
|
|
403
|
+
to `positions`). An administrator whose adminship comes from the
|
|
404
|
+
`admin_full_access` permission set — the single-tenant deployment shape, where
|
|
405
|
+
there is no organization member row and the server deliberately no longer
|
|
406
|
+
overwrites `user.role` — matched none of the three sources and read as **not an
|
|
407
|
+
administrator**: Setup and Studio simply disappeared for them.
|
|
408
|
+
|
|
409
|
+
The hook now reads `user.positions[]`, the one spelling the session publishes.
|
|
410
|
+
Detection is restored for that path and unchanged everywhere else: an active
|
|
411
|
+
member row with an admin role, a stored `user.role` admin scalar, and
|
|
412
|
+
preview/no-auth mode all behave exactly as before, and nobody who was not an
|
|
413
|
+
administrator becomes one — pinned by four negative cases alongside the
|
|
414
|
+
positive one.
|
|
415
|
+
|
|
416
|
+
Also corrects the now-stale documentation that described the removed spelling:
|
|
417
|
+
the hook's own docblock, the `roles?: string[]` declaration on the client
|
|
418
|
+
`AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and
|
|
419
|
+
two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the
|
|
420
|
+
comment corrections.
|
|
421
|
+
- 343c598: The active organization id is now stored per user, and a change of session user drops the
|
|
422
|
+
previous user's client state wholesale (objectui#5664).
|
|
423
|
+
|
|
424
|
+
`auth-active-organization-id` was a single un-namespaced `localStorage` key while its
|
|
425
|
+
siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`,
|
|
426
|
+
`flow-palette-recents:u:`). On a browser handed from one account to another — a shared
|
|
427
|
+
machine, a kiosk, a handover, a support session — the arriving user's console read the
|
|
428
|
+
PREVIOUS user's organization id. The header workspace chip rendered the previous user's
|
|
429
|
+
workspace for a user whose `organization/list` was empty, and the consequence past the
|
|
430
|
+
cosmetics is the one worth stating: the polluted org context suppressed
|
|
431
|
+
`RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so
|
|
432
|
+
a brand-new user on that browser silently never got the new-user flow at all.
|
|
433
|
+
|
|
434
|
+
Nothing about row visibility rode on this. With the stale id the server answers
|
|
435
|
+
`403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero
|
|
436
|
+
environments; the damage was entirely in what the client believed about itself.
|
|
437
|
+
|
|
438
|
+
Three changes, and the third is the one that closes the class rather than the instance:
|
|
439
|
+
|
|
440
|
+
- The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention
|
|
441
|
+
its siblings already use.
|
|
442
|
+
- It can no longer be written un-namespaced at all. Where no session user is known yet the
|
|
443
|
+
value lives in memory for that page-load only — a namespacing that kept a bare-key
|
|
444
|
+
fallback would re-open the defect the first time a write happened before the user id
|
|
445
|
+
resolved.
|
|
446
|
+
- **A change of session user drops the previous user's client state wholesale.** This is an
|
|
447
|
+
allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so
|
|
448
|
+
the NEXT storage key someone adds without a `:u:` scope is covered before it is written.
|
|
449
|
+
Only device-scoped entries survive: the arriving session's own bearer token, the pointer
|
|
450
|
+
recording whose state the browser holds, and the UI theme.
|
|
451
|
+
|
|
452
|
+
Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a
|
|
453
|
+
non-null `localStorage` read and falls back to the in-memory value, and the memory value is
|
|
454
|
+
nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge
|
|
455
|
+
too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then-
|
|
456
|
+
sign-in path that never reloads the page.
|
|
457
|
+
|
|
458
|
+
Existing browsers are not migrated. A value sitting under the retired bare key is
|
|
459
|
+
unattributable — nothing recorded whose org id it is — so migrating it is precisely the
|
|
460
|
+
defect it would be migrating away from, and it is deleted instead. A signed-in user loses
|
|
461
|
+
nothing durable: the active organization is a server-owned fact that
|
|
462
|
+
`AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no
|
|
463
|
+
active org is held, including the ADR-0081 single-membership repair. One boot re-supplies
|
|
464
|
+
it; users with no organization land on the guided first-run flow, which is the outcome this
|
|
465
|
+
card is about.
|
|
466
|
+
|
|
467
|
+
`apps/console`'s pre-render auth preflight purges every spelling of the active-org key —
|
|
468
|
+
the retired bare one and each `:u:` scope — when it finds a dead bearer token, and
|
|
469
|
+
deliberately leaves the session-user pointer in place so the next sign-in can still tell
|
|
470
|
+
that the browser changed hands.
|
|
471
|
+
- 2dd9443: `purgeSignedOutClientCaches()` — the sweep that drops the signed-out user's
|
|
472
|
+
`objectui:metadata:*` seed cache on sign-out (objectui#5198) — now costs one key when a
|
|
473
|
+
single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5777).
|
|
474
|
+
|
|
475
|
+
The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key `n`
|
|
476
|
+
aborted the walk, so keys `n+1..end` were never swept, and the failure was swallowed —
|
|
477
|
+
`AuthProvider`'s `signOut` believed the purge had completed. The entries this sweeps are
|
|
478
|
+
the previous principal's org-scoped, PERMISSION-FILTERED app list — objectui#5198
|
|
479
|
+
classifies a surviving entry as a cross-principal disclosure on a shared browser, not
|
|
480
|
+
mere staleness — so a partial sweep here is the sharper half of the same defect class
|
|
481
|
+
objectui#5763 fixed on the sign-in path (`sweepStore` in `ActiveOrganizationStorage.ts`).
|
|
482
|
+
|
|
483
|
+
`Object.keys(sessionStorage)` — the reason a guard exists here at all — stays guarded on
|
|
484
|
+
its own; only the per-key guard is new. Same as `sweepStore`, a failed removal here is
|
|
485
|
+
not verified by read-back and not quarantined the way `ActiveOrganizationStorage.clear()`
|
|
486
|
+
(objectui#5731) quarantines a key: this function does not own reads for the metadata
|
|
487
|
+
seed cache (`MetadataProvider` in `@object-ui/app-shell` does), so there is no `get()` to
|
|
488
|
+
guard and nothing to quarantine — adding read-back verification would be a general
|
|
489
|
+
storage-error-handling refactor of the module, out of this card's scope. What is
|
|
490
|
+
mirrored is the reporting channel: a key whose `removeItem` throws is named in a
|
|
491
|
+
`console.warn`, the same channel `sweepStore` and `clear()` use, so a partial sweep is
|
|
492
|
+
discoverable instead of silent.
|
|
493
|
+
|
|
494
|
+
Adds a partial-failure test: a `sessionStorage` whose `removeItem` throws on one metadata
|
|
495
|
+
key, asserting every other metadata key on both sides of it is still swept and unrelated
|
|
496
|
+
non-matching keys are untouched, plus a control that the warning fires only on an actual
|
|
497
|
+
failure.
|
|
498
|
+
- ff2d547: `sweepStore()` — the walk that drops the previous user's `localStorage`/`sessionStorage`
|
|
499
|
+
state on a change of session user (objectui#5664 part 3) — now costs one key when a
|
|
500
|
+
single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5763).
|
|
501
|
+
|
|
502
|
+
The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key
|
|
503
|
+
`n` aborted the walk, so keys `n+1..end` were never swept, and the failure was
|
|
504
|
+
swallowed — `purgePreviousUserClientState()` returned normally and `SessionUserScope.adopt`
|
|
505
|
+
believed the sign-in purge had completed. This is an ALLOWLIST sweep precisely so the
|
|
506
|
+
next un-namespaced key — one nobody has written yet — cannot re-open the cross-user
|
|
507
|
+
pollution class #5664 fixed; a partial sweep is a partial allowlist, and which keys
|
|
508
|
+
survived depended on `Object.keys` iteration order rather than on anything bounded. The
|
|
509
|
+
previous user's org id, recents, favourites, or a `sessionStorage` metadata seed (their
|
|
510
|
+
permission-filtered app list, a cross-principal disclosure per objectui#5198) could all
|
|
511
|
+
land on the wrong side of the abort.
|
|
512
|
+
|
|
513
|
+
`Object.keys(store)` — the reason a guard exists here at all — stays guarded on its
|
|
514
|
+
own; only the per-key guard is new, so one uncooperative key now costs exactly that key.
|
|
515
|
+
|
|
516
|
+
Unlike `ActiveOrganizationStorage.clear()` (objectui#5731), a failed removal here is
|
|
517
|
+
NOT verified by read-back and NOT quarantined: `clear()` owns every future read of its
|
|
518
|
+
one key through `ActiveOrganizationStorage.get()`, so a "still readable" verdict and a
|
|
519
|
+
quarantine are what keep a failed `clear()` from handing the value straight back.
|
|
520
|
+
`sweepStore` walks keys it does not own reads for — another package's recents cache, a
|
|
521
|
+
metadata seed — so there is no `get()` here to guard and nothing to quarantine; adding
|
|
522
|
+
read-back verification for keys this function does not otherwise touch would be a
|
|
523
|
+
general storage-error-handling refactor of the module, which this card is scoped away
|
|
524
|
+
from. What IS mirrored is the reporting channel: a key whose `removeItem` throws is
|
|
525
|
+
named in a `console.warn`, the same channel `clear()` uses, so a partial sweep is
|
|
526
|
+
discoverable instead of silent. The caller (`SessionUserScope.adopt`, on the sign-in
|
|
527
|
+
path, inside an `AuthProvider` effect) still cannot act on the failure and must not
|
|
528
|
+
throw either.
|
|
529
|
+
|
|
530
|
+
A working `localStorage`/`sessionStorage` behaves exactly as before: every
|
|
531
|
+
non-device-scoped key is removed, nothing is reported, and the device-scoped allowlist
|
|
532
|
+
(`auth-session-token`, `auth-session-user-id`, `vite-ui-theme`) is unaffected.
|
|
533
|
+
- 36918bf: `switchOrganization` now re-resolves identity for the organization it just switched
|
|
534
|
+
to by its own explicit decision, instead of depending on an accidental
|
|
535
|
+
`TokenStorage` side effect to notice the switch (objectui#5750).
|
|
536
|
+
|
|
537
|
+
`AuthProvider.switchOrganization` has never called `loadSession()` itself. Identity
|
|
538
|
+
re-resolved across a switch only because `POST /organization/set-active` happens to
|
|
539
|
+
return the SIGNED `token.signature` spelling in `set-auth-token`, which differs from
|
|
540
|
+
the UNSIGNED `session.token` spelling `getSession()` normally stores — so
|
|
541
|
+
`TokenStorage.set` reads the flip as a rotation and the objectui#4467 subscription
|
|
542
|
+
calls `loadSession()` for it (measured and pinned in objectui#5749/#5719). That
|
|
543
|
+
signed spelling is deterministic on the raw session token, not on the organization:
|
|
544
|
+
two switches with no `get-session` landing in between produce the identical signed
|
|
545
|
+
value, so the SECOND `TokenStorage.set` sees no change, never notifies, and identity
|
|
546
|
+
is left answering for whichever organization the FIRST switch targeted even though
|
|
547
|
+
`activeOrganization` already reads as the new one.
|
|
548
|
+
|
|
549
|
+
Reachable in the console via `OrganizationLayout`'s slug-driven effect (the "Manage"
|
|
550
|
+
link on an org card, plus its own "Back to organizations" button) — ordinary
|
|
551
|
+
client-side navigation with no full-page reload and nothing debouncing repeat
|
|
552
|
+
switches. `WorkspaceSwitcher` and `OrganizationsPage`'s own card click were not
|
|
553
|
+
reachable paths for this: both force `window.location.href` immediately after a
|
|
554
|
+
successful switch, and the resulting fresh `AuthProvider` mount always performs an
|
|
555
|
+
authoritative `loadSession()` regardless of how the race above resolved.
|
|
556
|
+
|
|
557
|
+
`switchOrganization` now tracks the organization it last resolved to itself and
|
|
558
|
+
re-resolves explicitly whenever a switch's target differs from that, while
|
|
559
|
+
suppressing the (now redundant) rotation notification for its own `set-active`
|
|
560
|
+
call — so the common single-switch path still spends exactly one `get-session`, not
|
|
561
|
+
two. A generation guard discards a still-in-flight, now-superseded switch's answer
|
|
562
|
+
rather than let it clobber a later switch's fresher one.
|
|
563
|
+
- 934a532: Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the
|
|
564
|
+
unstamped-first-request window in which it is not sent (objectui#5279). Documentation
|
|
565
|
+
only — no behaviour changes.
|
|
566
|
+
|
|
567
|
+
The header had no written contract anywhere, and the shape of the missing information was
|
|
568
|
+
actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a
|
|
569
|
+
search confined to this repo and the framework (`objectstack`) returns zero readers and
|
|
570
|
+
reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was
|
|
571
|
+
held until a cloud-side reading came back non-empty. Without the contract written down,
|
|
572
|
+
the next person to grep reaches the same false conclusion and deletes a live routing
|
|
573
|
+
input.
|
|
574
|
+
|
|
575
|
+
`packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means
|
|
576
|
+
(a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim,
|
|
577
|
+
not an authorization input, not what scopes rows), who stamps it and under exactly which
|
|
578
|
+
condition, who reads it, and what a reader may and may not assume. The framework half is
|
|
579
|
+
stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the
|
|
580
|
+
API-key principal or `session.activeOrganizationId` and from no header — alongside
|
|
581
|
+
`plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability.
|
|
582
|
+
The configuration half is quoted from the contract this package can actually resolve,
|
|
583
|
+
`TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the
|
|
584
|
+
default of a configurable `tenantHeaderName` and `header` ranks second of six
|
|
585
|
+
identification sources behind `subdomain`.
|
|
586
|
+
|
|
587
|
+
The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is
|
|
588
|
+
filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` ->
|
|
589
|
+
`getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at
|
|
590
|
+
all. What a reader observes is documented as **absent, never present-and-empty**, with the
|
|
591
|
+
five situations that open the window and the instruction to fall through to the next
|
|
592
|
+
identification source rather than fail closed. The gap is recorded, deliberately not
|
|
593
|
+
closed: the cloud readers observe today's behaviour, so changing when the header first
|
|
594
|
+
appears is its own decision.
|
|
595
|
+
|
|
596
|
+
Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes
|
|
597
|
+
about the wire — no active organization means no header at all, the stamp is not gated on
|
|
598
|
+
`/api/` the way `Authorization` is, and the active organization overwrites a caller-set
|
|
599
|
+
`X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed.
|
|
600
|
+
|
|
3
601
|
## 17.6.0
|
|
4
602
|
|
|
5
603
|
### Minor Changes
|