Repository navigation
Commit 78ab647
fix(service-settings,cloud-connection)!: Branding settings drive runtime-config under single; theme_mode and support_email retire; namespace withheld under walled postures (#24959)
Fixes #24766
Clause-②: no (narrowing)
## What this implements
The maintainer's ruling B′ (`6293793948`) with its amendment
(`6293825075`), under the standing rule 「甲」 (#6015 `6292005854`).
**Under `single`: runtime-config's `branding` block reads the Branding
settings, per request.** `RuntimeConfigPlugin`
(`packages/cloud-connection`) reads the `branding` namespace on every
`GET /api/v1/runtime/config` (and the legacy alias), so a save shows on
the console's and the sign-in page's next load:
| served field | settings key | fallback when nobody set the key |
|:--|:--|:--|
| `productName` | `branding.workspace_name` | host option, then
`OS_PRODUCT_NAME`, then `ObjectOS` |
| `logoUrl` | `branding.logo_url` | host option, then `OS_LOGO_URL` |
| `brandColor` | `branding.accent_color` | host option, then
`OS_BRAND_COLOR` |
- "Set" means any `source` but `default`: saved in Setup, or pinned by
the settings layer's own `OS_BRANDING_*` variable. That is the same rule
`plugin-auth` already applies to `workspace_name`, so the two readers of
that key agree. The manifest default (`ObjectStack`) is never served on
this door.
- Precedence decision, stated because the ruling names only the env
vars: the host's constructor option sits with the env var as the
deployment default, so a saved value outranks both. Pinned.
- `productShortName`, `pwaDescription` and `pwaThemeColor` follow the
served name and colour unless configured themselves. Before, they were
derived once at construction from the deployment values.
- `brandColor` keeps its one meaning: the console's browser and
installed-app theme colour (objectui `getPwaThemeColor`), beside each
app's own `AppBranding` colours. No precedence rule is added.
- A settings read that fails serves the deployment branding and logs one
`warn` per run of failures. The door never answers 500 for it.
- The served `branding` block keeps its shape. No exported type or
option changed: `RuntimeConfigPluginConfig` gained no member (JSDoc
only).
**`theme_mode` and `support_email` retire, manifest version 1 → 2, in
#21970's shape.** `branding.manifest.ts`, the four translation bundles
and the README follow. `accent_color` also loses its `#6366f1` default:
the page showed it as a colour the console never used, since an unset
key is now served as the deployment's colour.
**`workspace_name`'s auth readers keep working.** `plugin-auth` still
reads the key for sign-in mail, the authenticator issuer and MCP
sign-in; its binding now runs only where the namespace is offered (see
the patch-round section below). The key's description states both
effects: "The product name in the console, on the sign-in page and in
the browser title, and in sign-in emails, the authenticator app and MCP
sign-in."
**Under `group` / `isolated` the server does not serve the `branding`
namespace.** `SettingsServicePlugin` holds a manifest in
`UNWALLED_ONLY_NAMESPACES` (today only `branding`) back at `init()`. It
registers that manifest from its `kernel:ready` hook only when the
posture in force puts up no organization wall. It reads the posture the
way its own door and service already read it
(`resolveAdmissionTenancyPosture`). It decides at `kernel:ready`, not at
`init()`: during `init()` the `tenancy` service may not be registered
yet, and `isolated` is in force only once `org-scoping` has registered.
The registry's own answer then covers every door:
- `GET /api/settings` does not list `branding`;
- `GET` and `PUT /api/settings/branding` are refused `404
UNKNOWN_NAMESPACE` by the existing unknown-namespace path, never a
silent 200;
- the runtime-config door reads the namespace only when the settings
service lists it. Absent, it reads nothing, so no tenant data reaches
that anonymous door, and the deployment's `OS_*` branding is served as
before. No organization is resolved before sign-in.
A posture that cannot be read withholds the namespace and logs one
`warn` naming it: offering is the one direction a guess must not take.
No `tenancy` service, or a host with no async registry, offers it, which
is how every lean kernel has always read the posture.
`settings-service.ts` (claimed by epic #15194) is not touched.
**Walled deployments, the auth product name.** With `branding` withheld,
`OS_BRANDING_WORKSPACE_NAME`, and an organization-less
`branding.workspace_name` row, no longer name sign-in mail, the
authenticator issuer or MCP sign-in; the `OS_APP_NAME` chain does.
Operators move the value to `OS_APP_NAME`. This is stated in the
service-settings and plugin-auth changesets.
### The retired-key refusal under 「甲」: confirmed
A save naming a retired key gets the loud refusal through the version
step: `PUT /api/settings/branding` naming `theme_mode` or
`support_email` answers `400 UNKNOWN_KEY` with `details: { namespace:
'branding', key }`, and the refusal covers the batch (a `logo_url` sent
beside it does not land). Pinned in `settings-routes.test.ts` with
stored rows seeded, and ablation-verified below. So the fallback (a
warning in 18.x, with the refusal filed for 19.0) is not needed.
## Clause-② measured
`no (narrowing)`. Published types: `@objectstack/service-settings`
exports the same symbols with the same types; only the value of
`brandingSettingsManifest` changed. `SettingsServicePluginOptions` and
`RuntimeConfigPluginConfig` changed in JSDoc only, and
`RuntimeConfigPlugin` changed only in private members. The served
`runtime/config` shape is unchanged. Narrowed accept sets, three:
- `branding.theme_mode` / `branding.support_email` on every posture (now
`400 UNKNOWN_KEY`);
- under walled postures, the whole `branding` namespace (now `404
UNKNOWN_NAMESPACE`);
- under walled postures, the `OS_BRANDING_WORKSPACE_NAME` override and
any organization-less `workspace_name` row, which no longer name the
auth product name.
The ruling's "wiring the three keys is a widening" is the capability
sense, which is why `@objectstack/cloud-connection` takes a `minor`. No
published surface widens in the Clause-② sense.
**Cross-lane in this PR:** `packages/qa/dogfood` (`domain:cli`) for the
fixture re-point, and `content/docs/ui/setup-app.mdx` (`domain:devx`)
for the Configuration row. `content/docs/releases/` is untouched.
## The Setup entry and the auth binding (patch round 1)
- **The Setup → Branding entry moves with the capability.**
`SettingsServicePlugin` contributes `nav_settings_branding` through the
`manifest` service from the `kernel:ready` step that offers the
`branding` namespace, and only then. It sits in Configuration at
priority 110, after platform-objects' entries and before
service-messaging's; the pattern is `@objectstack/mcp`'s `connect-ui.ts`
(cloud ADR-0009). `SETUP_NAV_CONTRIBUTIONS` drops the static line.
`check-app-nav-i18n.mjs` boots the plugin through `kernel:ready` in a
new roster entry, so the id stays in that gate's population: setup still
53 merged ids. Labels stay keyed
`apps.setup.navigation.nav_settings_branding`.
- **plugin-auth binds `branding.workspace_name` only where the settings
service lists the `branding` namespace**, the runtime-config door's
test. A walled boot no longer logs `Auth: failed to apply
branding.workspace_name: No settings manifest registered for namespace
'branding'.`, and the configured app name stays in force.
- `packages/cli/test/connect-agent-both-halves-wire.pin.test.ts` counts
31 Setup ids, exactly: that harness composes `SETUP_NAV_CONTRIBUTIONS`
and the MCP bundle only, and the Branding entry now travels with
service-settings. It also asserts `nav_settings_branding` is absent
there.
- Changesets are now four: `@objectstack/platform-objects` minor
(`24766-setup-branding-entry`) and `@objectstack/plugin-auth` minor
(`24766-auth-brand-binding`) join the two above. `Clause-②: no
(narrowing)` is unchanged for the PR.
## Tests (head `0c71c4190`, after merging `origin/main` `644e4b2c5`)
- `pnpm --filter @objectstack/service-settings exec vitest run
--maxWorkers=2`: 56 files, 924 tests passed.
- `pnpm --filter @objectstack/cloud-connection exec vitest run
--maxWorkers=2`: 53 files, 625 tests passed.
- `typecheck` for both (`tsc --noEmit`, plus `-p tsconfig.test.json` for
cloud-connection): exit 0. `--listFiles` shows both programs compile
every new and edited test file.
- Importer spot-check (behaviour moved from `init()` to `kernel:ready`):
`plugin-auth`'s `auth-settings-ordering.pin`,
`auth-settings-seeded-boot.pin` and `dev-admin-seed-workspace-timezone`
tests, 3 files and 12 tests passed. `plugin-auth` aliases
`@objectstack/service-settings` to source.
- The dependency closures were built first (`turbo run build
--filter=@objectstack/cloud-connection...
--filter=@objectstack/service-settings...`, 34 tasks).
New pins:
- `service-settings/src/settings-branding-posture.pin.test.ts` boots the
real `SettingsServicePlugin` sequence with `tenancy` on a real
`ObjectKernel`, and drives the real `registerSettingsRoutes` over the
service it built:
- under `group` and `isolated`: absent from the listing (the other
namespaces are still listed); `PUT` and `GET` refused `404
UNKNOWN_NAMESPACE`;
- under `single`, with no tenancy service, and on a host with no async
registry: listed, saved, read back;
- a posture that cannot be read: withheld, one warning naming it.
- `service-settings/src/settings-routes.test.ts`: the two retired keys.
Not offered and not resolved, even with a stored row. `PUT` refused `400
UNKNOWN_KEY` by name with the whole batch. A stored row never blocks a
live save and is kept.
- `service-settings/src/manifests/branding.manifest.test.ts`: version 2;
exactly `workspace_name` / `accent_color` / `logo_url` in `identity` /
`appearance`; no retired key; no default on `accent_color` / `logo_url`.
- `cloud-connection/src/runtime-config-branding-settings.test.ts`:
- under `single`: a saved logo URL is the one served, and an unset one
falls back to `OS_LOGO_URL`; the same for the name (never the manifest
default) and the colour;
- the derived fields follow; a saved value outranks the host option;
reads are per request; `source: env` counts as set; a failing read
serves the env branding with one warning;
- under `group`: the setting is not served and the door never calls
`getMany`;
- no settings service: unchanged.
- `settings-service.test.ts`: the #5204 env-override pins used
`branding.theme_mode` as their example select. They now use
`localization.locale`, also a domain-less select, and pin the same
behaviour, as #21970 re-pointed its pins.
### Ablations (each negative pin, put back, seen red, restored)
Each used `scripts/ablation-replace.mjs` in WRAP mode, with a literal
anchor that must hit and restore proven by blob. Every restore ended
"blob == HEAD and `git diff HEAD` is empty". The test subjects resolve
by relative import to `src`, so no `dist` rebuild was involved.
1. **Posture gate removed** (`if (posture !== undefined &&
postureEnforcesWall(posture))` became `if (false && …)`, blob
`a282064cb1d8` → `5e3180ac9016`): 6 red in
`settings-branding-posture.pin.test.ts`. They are listing / PUT / GET
under `group` and `isolated`: `expected [ 'company', 'localization',
…(2) ] to not include 'branding'` and `expected 200 to be 404`. Restored
to `a282064cb1d8`.
2. **Door ignores the listing** (the `listManifests().find(…)` replaced
by a constant manifest, blob `1cedb39aa721` → `88ff9db94c1d`): the
walled-posture pin went red, `expected { productName: 'Tenant Org', …(6)
} to match object { productName: 'Env Product', …(2) }`. Restored to
`1cedb39aa721`.
3. **Manifest default served as set** (`if (!entry || entry.source ===
'default') continue;` became `if (!entry) continue;`, blob →
`f429c41343c8`): 3 red, `expected 'ObjectStack' to be 'Env Product'`
(and `… 'Host Product'`). Restored to `1cedb39aa721`.
4. **`theme_mode` put back into the manifest** (blob `6e63f4cce79b` →
`85c21c5efa98`): 7 red. The route pins answered `expected
'SETTINGS_VALIDATION' to be 'UNKNOWN_KEY'` and `theme_mode must not be
offered`, the manifest pins failed, and the zh-CN / ja-JP / es-ES
coverage reported `key:theme_mode` missing. Restored to `6e63f4cce79b`.
5. **Read memoized at the first request** (blob `1cedb39aa721` →
`bff4ef12d8d3`): the per-request pin went red, `expected 'Env Product'
to be 'Renamed'`. Restored to `1cedb39aa721`.
### Patch round 1 (head `e6522a957`)
- `service-settings`: full suite 56 files, 929 passed;
`settings-branding-posture.pin` re-run at `e6522a957`: 15 passed.
- `plugin-auth`: full suite 173 files, 3319 passed, 10 skipped (before
the test-only typing fix); `auth-plugin.test.ts` re-run at `e6522a957`:
112 passed; `auth-settings-ordering.pin`,
`auth-settings-seeded-boot.pin`, `dev-admin-seed-workspace-timezone`
passed.
- `platform-objects`: full suite 82 files, 1232 passed.
- `cli` unit project, the files that compose the Setup nav: 4 files, 70
tests passed (`connect-agent-both-halves-wire.pin`,
`platform-page-i18n-parity`, `adr-0048-app-split`,
`format.server-ready-dev-admin-audience`).
- Typecheck exit 0 for `service-settings`, `plugin-auth` (with
`check:test-typecheck`), `platform-objects` (with
`check:test-typecheck`), `cloud-connection` and `cli` (with
`check:test-typecheck`).
- New and changed pins: `settings-branding-posture.pin` folds the real
`SETUP_APP` + `SETUP_NAV_CONTRIBUTIONS` plus what the boot registered
through the real `SchemaRegistry`: under `group` and `isolated` no
`nav_settings_branding` in the merged Setup tree (controls
`nav_settings_company`, `nav_settings_localization` present); under
`single`, with no tenancy service, and with no async registry, exactly
once in `group_configuration`, after `nav_settings_feature_flags`.
`auth-plugin.test.ts`: walled pin (namespace not listed: no branding
read, no subscription, no warn naming branding, `setAppName` never
called) and a real-`SettingsService` pin (no branding manifest: unbound
and silent; registered with a saved `Acme Single`: `setAppName('Acme
Single')`).
- Ablations (`scripts/ablation-replace.mjs` WRAP; every restore "blob ==
HEAD and `git diff HEAD` is empty"):
- A. plugin-auth gate forced open: 2 red (the walled pin and the
real-service pin, the latter showing the warn line). Restored to
`9f496bd8c738`.
- B1. Static line put back into `SETUP_NAV_CONTRIBUTIONS`: 5 red (walled
postures: `to not include nav_settings_branding`; the three offering
postures: `to have a length of 1 but got 2`). Restored to
`ad98264e484d`.
- B2. Posture gate removed: 8 red (listing / PUT / GET / Setup entry
under `group` and `isolated`). Restored to `171c958f5702`.
- C. The gate roster entry fires no `kernel:ready`: `check-app-nav-i18n`
exits 1, "landed NO setup navigation id". Restored to `23f2ff594226`.
### Patch round 2 (head `d616d4d87`)
- **The dogfood re-point** (contract review item 10):
-
`packages/qa/dogfood/test/settings-organization-isolation.dogfood.test.ts`
now uses `company.legal_name` as its tenant fixture. Run for real under
the `isolated` posture it boots (its guard leg asserts posture
`isolated`, degraded `false`): 6 passed.
- Control leg: the file restored from `e6522a957` and run on the same
code gives 4 failed, 2 passed. Three failures carry
`{"code":"UNKNOWN_NAMESPACE","message":"No settings manifest registered
for namespace 'branding'."}` (`expected 404 to be 200` twice, `expected
404 to be 400`), and the data-API positive control fails `expected false
to be true`. Restored: blob `531e8f552e34` == HEAD.
- **The no-wall branding suites:** `settings-config-change-audit`,
`settings-audit-global-rung-sibling` and `audit-log-parent-read-gate`, 3
files, 16 tests passed.
- **Typecheck,** all exit 0: dogfood, plugin-auth
(`check:test-typecheck` OK), service-settings, platform-objects
(`check:test-typecheck` OK), cloud-connection.
- **No ablation this round:** no mechanism file changed.
## Gates
- `dispatch-gates --commands` at the final head `d616d4d87` derived 106
commands; the 29 new ones come from the docs page and the dogfood path.
Each ran with its exit code captured before any pipe: 104 exited 0.
`check:skill-examples` (spec) and `check:dual-build-cjs-loads` exited 3
(PREREQUISITE NOT MET: missing dists) and re-ran exit 0 after building
their closures ("262 prose examples type-check across 3 surface(s)";
"112 published require entry point(s) across 69 package(s) load").
- `--ran` with exit codes: "106 derived famil(ies) accounted for — 106
run, 0 NOT-MEASURED (a DERIVED zero — all 106 recorded an exit code and
none of them is 3)".
- The changeset gates at `d616d4d87`, each exit 0:
`check-adr-0087-registration --base origin/main` ("1 declared-breaking
changeset(s), each carrying an ADR-0087 disposition … not-required
(no-migration-prescription)"), `check-changeset-no-major`,
`check-empty-changeset`, and the adr-0087 `--self-test` (548
assertions).
- Check Changeset was red on `acaf3231b`: a literal FROM/TO migration
label tripped the ADR-0087 prescription detector. Reproduced locally
(exit 1), reworded as the environment fix it is, and `success` on
`d616d4d87`.
- Earlier rounds, still standing: `check:startup-registry-verdict`,
`check:settings-bind-window` and `check:init-service-contract` exit 0 at
`0c71c4190`; `pnpm check:app-nav-i18n` exit 0 at `e6522a957` (setup: 53
merged nav ids).
- ESLint over the dogfood test: exit 0. Control-byte scan of the round's
files: no hit.
## Acceptance notes (not filed)
- `docs/qa/platform-checklist/areas/platform-core.json`,
`runtime-config-boot-read` step 1 (`logoUrl` / `brandColor` only when
configured by the plugin config or env vars): a saved Branding setting
now serves them too, under `single`. Its namespace count is a
`single`-posture count; under a walled posture `branding` is not served.
QA text drift, not edited. Carrier: none.
-
`packages/qa/dogfood/test/settings-organization-isolation.dogfood.test.ts`
uses `company.legal_name` as its tenant fixture, because `branding` is
not offered under the `isolated` posture it boots.
- `packages/qa/dogfood/test/anonymous-door-sweep.classification.ts`
classifies `runtime/config` as "no tenant data". Under `single` the door
now serves the one organization's saved branding, which the ruling
defines as the deployment's. Under a wall it serves none. The wording
still reads true by the ruling's definition. Carrier: none.
-
`packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts`
keeps `CONDITIONAL_SETUP_NAV_IDS = ['nav_sso_providers']`, the ids whose
labels must stay although no single composition contributes them.
`nav_settings_branding` is now such an id (contributed only under
`single`) and could join it. Carrier: none.
- `packages/qa/dogfood/test/platform-app-object-entry-views.test.ts`
says its roster mirrors `check-app-nav-i18n`'s CONTRIBUTORS. The new
service-settings entry contributes one `url` item, which that file
(object-entry views) has nothing to judge, so its verdict is unchanged.
Carrier: none.
- On a multi-tenant host that resolves environments by hostname
(`env-registry`), the branding read is the host kernel's own settings,
process-wide, exactly as `OS_PRODUCT_NAME` was. Per-environment branding
was never served here and still is not. Carrier: none.
- `workspace_name`'s manifest default `ObjectStack` still differs from
the console's built-in `ObjectOS`. That is the two-defaults reading
#24887 carries for the operator docs. Carrier: #24887.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f802213 commit 78ab647
24 files changed
Lines changed: 1373 additions & 174 deletions
File tree
- .changeset
- content/docs/ui
- packages
- cli
- scripts
- test
- cloud-connection/src
- platform-objects/src/apps
- plugins/plugin-auth/src
- qa/dogfood/test
- services/service-settings
- src
- manifests
- translations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | | - | |
| 55 | + | |
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
371 | 406 | | |
372 | 407 | | |
373 | 408 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
252 | 252 | | |
253 | 253 | | |
254 | 254 | | |
255 | | - | |
256 | | - | |
257 | | - | |
258 | | - | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
259 | 266 | | |
260 | 267 | | |
261 | 268 | | |
| |||
277 | 284 | | |
278 | 285 | | |
279 | 286 | | |
280 | | - | |
| 287 | + | |
| 288 | + | |
281 | 289 | | |
282 | 290 | | |
283 | 291 | | |
| |||
0 commit comments