Skip to content

Commit 78ab647

Browse files
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
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
---
4+
5+
fix(plugin-auth): the auth product name binds to `branding.workspace_name` only where the settings service offers the `branding` namespace, so a walled boot no longer warns about it
6+
7+
Clause-②: no (this package stops reading a namespace `@objectstack/service-settings` no longer offers under a wall; that withholding, declared BREAKING in its changeset, is what narrows the walled deployment, and nothing here widens)
8+
9+
`AuthPlugin` binds the product name in sign-in emails, the authenticator app entry and the MCP sign-in to the `branding.workspace_name` setting. `@objectstack/service-settings` (same release) no longer offers the `branding` namespace under the `group` and `isolated` tenancy postures, so on such a deployment that read would fail on every boot and log `Auth: failed to apply branding.workspace_name: No settings manifest registered for namespace 'branding'.` at `warn`.
10+
11+
The binding is now made only when the settings service lists the `branding` namespace, the same test the runtime config makes. On a single-organization deployment a saved name, or one pinned with `OS_BRANDING_WORKSPACE_NAME`, applies as before.
12+
13+
**On a walled deployment (`group` / `isolated`) the auth product name now comes from the `OS_APP_NAME` chain alone.** Nothing is read from the `branding` namespace and nothing is logged. Two sources that used to name sign-in emails, the authenticator issuer and the MCP sign-in there stop doing so:
14+
15+
- `OS_BRANDING_WORKSPACE_NAME`. The settings service served it as an `env` value whatever the posture, and this plugin applied it.
16+
- A `branding.workspace_name` row stored with no organization, for example one saved before the wall stood.
17+
18+
Migration, FROM → TO: `OS_BRANDING_WORKSPACE_NAME=Acme` → `OS_APP_NAME=Acme`. Set `OS_APP_NAME` to the name the deployment's sign-in emails and authenticator entries should carry. A deployment that set neither source sees no change.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/service-settings': minor
3+
---
4+
5+
fix(service-settings)!: the Branding settings drive the console's name, logo and colour on a single-organization deployment, retire `theme_mode` and `support_email`, and are not offered under a walled tenancy posture
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) The two retired keys were settings values, not metadata: only the platform-shipped brandingSettingsManifest declared them (only platform code declares a settings manifest), SettingsManifestSchema and every other spec declaration are unchanged, and a value stored for one of them lives in sys_setting (runtime state, ADR-0007), not sys_metadata, and is kept as it is. Nothing read either key in objectstack or in objectui at the console pin, so no consumer has anything to rewrite and objectstack migrate meta has nothing to reach. Withholding the branding namespace under a walled posture moves no stored row either. The one operator-facing consequence there, that OS_BRANDING_WORKSPACE_NAME stops naming sign-in mail and the OS_APP_NAME variable takes its place, is a deployment environment setting: no consumer code and no metadata is rewritten, so the ledger has nothing to carry and objectstack migrate meta nothing to reach. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a settings key (not already-registered); and the change is the value of an exported manifest constant and the plugin's registration behaviour, not an interface or a type alone (not runtime-interface-only or type-surface-only). -->
10+
11+
Setup → Branding saved values that nothing in the console ever read. Under the maintainer's ruling, its three keys now drive the console's own branding through the runtime config (`@objectstack/cloud-connection`, same release), the two keys with no counterpart retire, and the page is offered only where no organization wall stands.
12+
13+
**What the three keys do now** (a single-organization deployment, the `single` posture):
14+
15+
- `workspace_name` is the product name in the console, on the sign-in page and in the browser title, and it stays the product name in sign-in emails, the authenticator app and MCP sign-in.
16+
- `logo_url` is the logo on the sign-in page, the loading screen and the home header.
17+
- `accent_color` is the browser and installed-app theme colour. Each app keeps the colours its own `AppBranding` declares.
18+
- A key nobody set falls back to the deployment's own branding (`OS_PRODUCT_NAME`, `OS_LOGO_URL`, `OS_BRAND_COLOR`, or the host's `RuntimeConfigPlugin` options). A value pinned with the settings layer's `OS_BRANDING_WORKSPACE_NAME`, `OS_BRANDING_LOGO_URL` or `OS_BRANDING_ACCENT_COLOR` counts as set. A save shows on the next page load, with no restart. `accent_color` no longer declares the `#6366f1` default, which the page showed as a colour the console never used.
19+
20+
**BREAKING: `theme_mode` and `support_email` are retired.** The Branding manifest (`brandingSettingsManifest`) drops both specifiers and their copy in the `en`, `zh-CN`, `ja-JP` and `es-ES` settings bundles, and its `version` is now 2, as the `SettingsManifest.version` contract asks when keys are removed. Nothing ever read either key. A value a workspace already stored is kept:
21+
22+
- The `sys_setting` row stays exactly as it was. No read or save rewrites or deletes it, and it never blocks saving the live keys.
23+
- `GET /api/settings/branding` does not resolve it: the key is absent from both `manifest.specifiers` and `values`.
24+
- A `PUT /api/settings/branding` that names `theme_mode` or `support_email` is refused with `400 UNKNOWN_KEY` (`details.key` names it). The refusal covers the whole batch, so a live key sent beside it does not land either.
25+
- `OS_BRANDING_THEME_MODE` and `OS_BRANDING_SUPPORT_EMAIL` are no longer read on any deployment.
26+
27+
**BREAKING: under the `group` and `isolated` postures the Branding namespace is not offered.** It is absent from `GET /api/settings`, `GET` or `PUT /api/settings/branding` is refused with `404 UNKNOWN_NAMESPACE`, the answer every unknown namespace gets, and Setup lists no Branding entry: `SettingsServicePlugin` now contributes the `nav_settings_branding` Setup entry itself, with the namespace, so the entry exists exactly where the page does. The runtime config is an anonymous door that resolves no organization before sign-in, so on a walled deployment the branding is the operator's: the console's through `OS_PRODUCT_NAME`, `OS_LOGO_URL` and `OS_BRAND_COLOR`, and the auth product name through `OS_APP_NAME`.
28+
29+
**On a walled deployment the `OS_BRANDING_*` variables are no longer read.** With the namespace withheld, none of `OS_BRANDING_WORKSPACE_NAME`, `OS_BRANDING_LOGO_URL` and `OS_BRANDING_ACCENT_COLOR` reaches a reader there. One of them had an effect before this release: `OS_BRANDING_WORKSPACE_NAME` named sign-in emails, the authenticator issuer and the MCP sign-in (`@objectstack/plugin-auth` applied it as an `env` value whatever the posture). So did a `branding.workspace_name` row stored with no organization. Both now give way to the `OS_APP_NAME` chain. The fix is in the deployment's environment, not in any code or metadata: a walled deployment that set `OS_BRANDING_WORKSPACE_NAME=Acme` sets `OS_APP_NAME=Acme` instead. The other two variables had no reader before and have none on a walled deployment now. A value already stored is kept and is offered again if the deployment returns to `single`. A host that passes its own `branding` manifest through `SettingsServicePlugin({ manifests })` gets the same rule.
30+
31+
**What to do after upgrading.**
32+
33+
- On a single-organization deployment nothing has to be rewritten.
34+
- There is no replacement for `theme_mode` (the console follows the user's own theme choice) or for `support_email`.
35+
- On a walled deployment, set the console's branding with `OS_PRODUCT_NAME`, `OS_LOGO_URL` and `OS_BRAND_COLOR`. If you set `OS_BRANDING_WORKSPACE_NAME` there, move its value to `OS_APP_NAME`.
36+
37+
It ships as `minor` under the launch-window convention for narrowings.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/cloud-connection': minor
3+
---
4+
5+
feat(cloud-connection): `GET /api/v1/runtime/config` serves the Branding settings' name, logo and colour, falling back to `OS_PRODUCT_NAME`, `OS_LOGO_URL` and `OS_BRAND_COLOR`
6+
7+
Clause-②: no (the served `branding` block keeps its shape and no exported type changes; only where its values come from moves)
8+
9+
`RuntimeConfigPlugin` reads the `branding` settings namespace on every request, so Setup → Branding changes the console and the sign-in page on their next load:
10+
11+
- `branding.productName` ← `branding.workspace_name`
12+
- `branding.logoUrl` ← `branding.logo_url`
13+
- `branding.brandColor` ← `branding.accent_color`
14+
15+
A value somebody set wins (saved in Setup, or pinned by the settings layer's own `OS_BRANDING_*` variable). A key nobody set falls back exactly as before: the host's constructor option, then `OS_PRODUCT_NAME` / `OS_LOGO_URL` / `OS_BRAND_COLOR`, then the built-in default. `productShortName`, `pwaDescription` and `pwaThemeColor` follow the name and colour actually served, unless configured themselves. `brandColor` keeps its meaning: the console's browser and installed-app theme colour, beside each app's own `AppBranding` colours.
16+
17+
The door reads the settings only when the settings service lists the `branding` namespace. `@objectstack/service-settings` withholds it under the `group` and `isolated` tenancy postures, so on a walled deployment no tenant data reaches this anonymous door and the deployment's `OS_*` branding is served, as before. A settings read that fails serves the deployment's branding and logs one warning; the door never answers 500 for it.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
fix(platform-objects): `SETUP_NAV_CONTRIBUTIONS` no longer carries the Setup → Branding entry; `@objectstack/service-settings` contributes it with the `branding` namespace
6+
7+
Clause-②: no (the entry moves to the package that serves its page: a composition that serves Setup → Branding still lists it, and one that does not no longer lists a page it cannot open)
8+
9+
The `nav_settings_branding` entry (Setup → Configuration → Branding) was a static item of `SETUP_NAV_CONTRIBUTIONS`, listed whatever the settings service offered. From this release `@objectstack/service-settings` does not offer the `branding` namespace under the `group` and `isolated` tenancy postures, so a static entry would open a page the server refuses with `404 UNKNOWN_NAMESPACE`.
10+
11+
`SettingsServicePlugin` (`@objectstack/service-settings`, same release) now contributes the entry from the same `kernel:ready` step that offers the namespace, and only then. On a single-organization deployment Setup still lists Branding, now at the end of the Configuration group's platform entries. Its translated labels are unchanged and still keyed `apps.setup.navigation.nav_settings_branding`.

‎content/docs/ui/setup-app.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ anchors are:
5252
| **People & Organization** (`group_people_org`) | Users · Organization · Business Units · Teams · Organizations · Invitations — `platform-objects` |
5353
| **Access Control** (`group_access_control`) | Positions / Permission Sets — `plugin-security`; Sharing Rules / Record Shares — `plugin-sharing`; API Keys — `platform-objects` |
5454
| **Approvals** (`group_approvals`) | Approvals Inbox (the `approvals:inbox` component) · Requests · Action History · Delegations (OOO) — `plugin-approvals` |
55-
| **Configuration** (`group_configuration`) | All Settings · Localization · Company · Branding · Authentication · Email · File Storage · AI & Embedder · Knowledge · Feature Flags — `platform-objects` |
55+
| **Configuration** (`group_configuration`) | All Settings · Localization · Company · Authentication · Email · File Storage · AI & Embedder · Knowledge · Feature Flags — `platform-objects`; Branding — `service-settings`, only where the `branding` settings namespace is offered (the `single` tenancy posture) |
5656
| **Diagnostics** (`group_diagnostics`) | Sessions · Notification Events — `platform-objects`; Audit Log — `plugin-audit` |
5757
| **Integrations** (`group_integrations`) | `plugin-webhooks` |
5858
| **Advanced** (`group_advanced`) | OAuth Applications — `platform-objects` |

‎packages/cli/scripts/check-app-nav-i18n.mjs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,41 @@ const CONTRIBUTORS = [
368368
return { plugin: new DatasourceAdminServicePlugin({}) };
369369
},
370370
},
371+
{
372+
// `nav_settings_branding` (#24766). The plugin contributes it from its
373+
// `kernel:ready` hook, and only where no organization wall stands, so this
374+
// entry boots it through init → start → kernel:ready on a context of its
375+
// own that captures `kernel:ready` — the shared context below fires no
376+
// hooks — with no `tenancy` service, which is the `single` reading, and
377+
// returns what it registered.
378+
source: '@objectstack/service-settings (Setup entries of the single-posture namespaces)',
379+
apps: ['setup'],
380+
async load() {
381+
const { SettingsServicePlugin } = await import('@objectstack/service-settings');
382+
const manifests = [];
383+
const ready = [];
384+
const own = {
385+
getService: (name) => {
386+
if (name === 'manifest') return { register: (m) => manifests.push(m) };
387+
throw new Error(`Service '${name}' not found`);
388+
},
389+
registerService: () => {},
390+
registerServiceFactory: () => {},
391+
replaceService: () => {},
392+
getServiceScoped: async () => undefined,
393+
getServices: () => new Map(),
394+
hook: (name, fn) => { if (name === 'kernel:ready') ready.push(fn); },
395+
trigger: async () => {},
396+
logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
397+
getKernel: () => undefined,
398+
};
399+
const plugin = new SettingsServicePlugin({ env: {}, registerRoutes: false });
400+
await plugin.init(own);
401+
await plugin.start(own);
402+
for (const fn of ready) await fn();
403+
return { manifests };
404+
},
405+
},
371406
{
372407
source: '@objectstack/mcp (CONNECT_AGENT_UI_BUNDLE)',
373408
// The one contributor that lands in BOTH apps: `CONNECT_AGENT_UI_BUNDLE`

‎packages/cli/test/connect-agent-both-halves-wire.pin.test.ts‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,17 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
252252
expect(allowedIds).toContain('nav_connect_agent');
253253
expect(groupChildren(setupAllowed.body?.item, 'group_integrations'))
254254
.toContain('nav_connect_agent');
255-
// The Setup tree the platform admin is served: 9 authored groups less
256-
// `group_approvals`, which nothing contributes into and the filter
257-
// collapses, plus 23 contributed entries plus the MCP one.
258-
expect(allowedIds).toHaveLength(32);
255+
// The Setup tree the platform admin is served BY THIS COMPOSITION: 9
256+
// authored groups less `group_approvals`, which nothing contributes
257+
// into and the filter collapses, plus the 22 entries
258+
// `SETUP_NAV_CONTRIBUTIONS` carries, plus the MCP one.
259+
// `nav_settings_branding` is not among them, and not because of a
260+
// tenancy wall (this harness boots none): since #24766 it is
261+
// contributed by `@objectstack/service-settings` together with the
262+
// `branding` namespace, offered only where no organization wall stands,
263+
// and this harness composes platform-objects and the MCP bundle alone.
264+
expect(allowedIds).toHaveLength(31);
265+
expect(allowedIds).not.toContain('nav_settings_branding');
259266
// The two per-user stores are reached from the User record page, not
260267
// from Setup's menu (#23803).
261268
expect(allowedIds).not.toContain('nav_accounts');
@@ -277,7 +284,8 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
277284
expect(withAccountHalf.statusCode).toBe(200);
278285
expect(withoutAccountHalf.statusCode).toBe(200);
279286
expect(navIds(withAccountHalf.body?.item)).toEqual(navIds(withoutAccountHalf.body?.item));
280-
expect(navIds(withAccountHalf.body?.item)).toHaveLength(32);
287+
// 31: the composition the count in the test above states.
288+
expect(navIds(withAccountHalf.body?.item)).toHaveLength(31);
281289

282290
// The mechanism behind the invariance, asserted rather than assumed:
283291
// the bundle aims exactly one contribution at `setup` and one at

0 commit comments

Comments
 (0)