Repository navigation
Commit 4b06a54
feat(spec): declare the nine served features keys on the auth config response and pin the registry to the schema (#29583)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8TGJRGUFpQY9Kxs3zg4DBsC)_
Fixes #29473
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — `git grep` under `skills/` finds no `auth/config`
reference, and the one hit for any of the nine key names is
`skills/objectui/guides/auth-permissions.md` using
`features.multiOrgEnabled` as an expression example, which stays true;
the API reference page is regenerated in this diff
## 维护者速读
- 改了什么:spec 的 `GET /api/v1/auth/config` 响应契约 `AuthFeaturesConfigSchema`
新增九个可选键(服务端早就在发、控制台早就在读,只是契约没声明),并加一条测试把「已分类的键」与「契约声明的键」钉成同一集合。
- 为什么改:按契约解析响应的客户端会把这九个键静默丢掉;以前每漏一个键就要单开一张卡(`sso`、`cloudManaged`
都是这样),这条钉子让下一个漏声明的键在 CI 直接变红。
- 风险与回滚:纯加宽,以前能解析的响应原样解析;九个键都无默认值,不发就仍不存在;运行时代码一行未动;回滚即撤掉此 PR。
-
先看:`packages/spec/src/api/auth-endpoints.zod.ts`(九个新键与说明、`devSeedAdmin`
不声明的理由)、`packages/spec/src/api/auth-endpoints.test.ts`(钉子测试)、`.changeset/29473-auth-config-features-served-keys.md`。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/api/auth-endpoints.test.ts`,预期 69 个测试全部通过。
-
需要你做的:确认下列九个新公开键名(均为服务端现有键名,未改名):`multiOrgEnabled`、`degradedTenancy`、`oidcProvider`、`deviceAuthorization`、`admin`、`tenancyPosture`、`audiencePosture`、`termsUrl`、`privacyUrl`;并确认
`devSeedAdmin`(仅开发环境、含明文密码)不进契约。其余席位落地。
## What changes
- **Nine optional keys on `AuthFeaturesConfigSchema`**
(`packages/spec/src/api/auth-endpoints.zod.ts`), each with a describe
that says what the producer means and what absent means. None carries a
default, so a key the server does not send stays absent.
- Booleans, all classified in `PUBLIC_AUTH_FEATURES`: `multiOrgEnabled`
(an organization wall is enforced; default-on, so absent reads as on),
`degradedTenancy` (isolation requested but inactive; a status signal),
`oidcProvider` (the embedded OIDC authorization server is wired;
default-on), `deviceAuthorization` (device-code CLI login wired),
`admin` (admin plugin wired, forced on by SCIM). Each describe's
"absent" sentence follows the registry entry's `semantics`.
- Values, all in `PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS`: `tenancyPosture`
reuses `TenancyPostureSchema` (`security/tenancy-posture.ts`, the
vocabulary the producer's `TenancyPosture` type is), `audiencePosture`
reuses `AUDIENCE_POSTURES` (`system/auth-config.zod.ts`; the producer
reports an off-vocabulary configured value as `invite_only`), and
`termsUrl` / `privacyUrl` are plain strings, because the producer passes
the trimmed `OS_TERMS_URL` / `OS_PRIVACY_URL` value through without URL
validation (a relative path is served as-is). Both imported modules
import only `zod` and `lazySchema`, so no cycle.
- **`devSeedAdmin` is ruled out as dev-only, not declared.** plugin-auth
adds it at the top level of the response only under
`NODE_ENV=development`, while the auto-seeded admin still signs in with
the default password, and it carries `{ email, password }`. Declaring it
would put a credential-bearing field on a published response contract
(and its generated reference page) that no production deployment serves.
Its one reader, `ui/apps/console/src/pages/auth/LoginPage.tsx`, casts
the raw response and never parses it through the schema, so nothing is
lost. The reason sits in a TSDoc block on `GetAuthConfigResponseSchema`.
- **The registry-to-schema pin**
(`packages/spec/src/api/auth-endpoints.test.ts`, block
`AuthFeaturesConfig declares every classified served key`): the schema's
declared keys, minus the retired `PUBLIC_AUTH_FEATURES_NOT_ADVERTISED`
tombstones (`passkeys`, `magicLink`), equal `PUBLIC_AUTH_FEATURE_NAMES`
plus `PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS`, exactly, in both directions.
With plugin-auth's existing drift guard (served keys equal the
registry), the served keys and the contract keys are now held equal
through the registry. It is a test, not a gate.
- Regenerated: `packages/spec/authorable-surface/api.json` (nine
`api/AuthFeaturesConfig:*` rows, written by the build) and
`content/docs/references/api/auth-endpoints.mdx` (`gen:docs`, the one
artifact `check:generated` named stale). `api-surface` did not move: no
export was added.
- Changeset `.changeset/29473-auth-config-features-served-keys.md`:
`@objectstack/spec` `minor`, both declaration lines.
**Producer readings** (origin/main `390bd2ae`,
`packages/plugins/plugin-auth/src/auth-manager.ts` `getPublicConfig()`):
the `features` literal serves `twoFactor`, `organization`,
`multiOrgEnabled`, `tenancyPosture`, `audiencePosture`,
`degradedTenancy`, `oidcProvider`, `sso`, `ssoEnforced`,
`deviceAuthorization`, `admin`, `phoneNumber`, `phoneNumberOtp`,
`cloudManaged`, and conditionally `termsUrl`, `privacyUrl`,
`turnstileSiteKey`: 17 keys, exactly the registry's 12 flags plus the 5
non-flag keys, so the served set, the registry and (after this PR) the
schema agree. `devSeedAdmin` sits beside `features`, not in it.
## Why
`GET /api/v1/auth/config` serves these nine keys and the console reads
several of them (for example `deviceAuthorization` in
`ui/apps/console/src/pages/auth/DeviceAuthPage.tsx`), but the contract
did not declare them, so a client parsing through
`GetAuthConfigResponseSchema` lost all nine: a plain object schema
strips unknown keys. Nothing tied the registry to the schema, which is
how `sso` and `cloudManaged` each needed their own card.
**Why `Metadata-protocol: added`, not the claim's `none`.** The claim
recorded `none` ("a wire response, not authorable metadata") and asked
the dev to confirm against AGENTS.md Post-Task Checklist step 3. Step 3
defines `added` as "a new key, accepted value or metadata type", and
says that where the ratchets can see a protocol change, they decide:
`packages/spec/authorable-surface/` records this response schema
(`api/AuthFeaturesConfig:*`, including the `sso` row PR #29455 added
under `added`), and this diff adds nine rows to `api.json`. Measured
offline with this diff and a PR body that differs only in that line:
`node scripts/check-changeset-no-major.mjs --base origin/main --event`
exits 1 under `none` ("metadata-protocol declares `none` while an
authorable-surface or authorable-defaults baseline moved … Moved:
packages/spec/authorable-surface/api.json") and exits 0 under `added`
("LEVEL AXIS … `yes (widening)` and metadata-protocol `added`"). So
`added`, with `@objectstack/spec` at `minor`, is the only declaration
the gate accepts, and it matches the `sso` precedent on the same schema.
## Tests
`packages/spec/src/api/auth-endpoints.test.ts`, new block, 38 cases:
- the declared-keys set (retired tombstones excepted) equals the
registry plus the non-flag list, and each retired key keeps its
tombstone;
- each of the 12 registry flags parses as `true` and `false` and refuses
`'yes'` with exactly one `invalid_type` issue at its own path, expected
`boolean`;
- each of the 17 declared keys carries a describe with no tracker id;
- a served-shape fixture naming all 17 keys (checked against the
classified list) survives a whole `GetAuthConfigResponseSchema` parse
unchanged;
- CONTROL: absent new keys stay absent (no default is invented);
- an off-vocabulary `tenancyPosture` / `audiencePosture` is refused with
one `invalid_value` issue at its path; a non-string `termsUrl` /
`privacyUrl` is refused with one `invalid_type` issue.
<details><summary>Local readings (head d68dd29)</summary>
- Build and regen: `pnpm --filter @objectstack/spec build` under
os-verify-lock, VERDICT 0, then `check:generated` exit 1 naming one
stale artifact (`content/docs/references/**`); `gen:docs` VERDICT 0;
`check:generated` again exit 0 ("All 14 generated artifacts are up to
date").
- `pnpm --filter @objectstack/spec test`: VERDICT command-exit 0, Test
Files 794 passed (794), Tests 23587 passed, 1 todo.
- `pnpm --filter @objectstack/spec typecheck`: VERDICT 0 (`tsc
--noEmit`, `check:scripts-typecheck`, `check:test-typecheck`, which
compiles the test layer).
- Consumer: `turbo run build --concurrency=2
--filter=@objectstack/client...` then `pnpm --filter @objectstack/client
typecheck` (it re-exports `AuthFeaturesConfig` /
`GetAuthConfigResponse`): VERDICT 0. Reverse check that it reads the
rebuilt declarations: a probe in `packages/client/src` typing `{
deviceAuthorization: true, tenancyPosture: 'group', audiencePosture:
'open', termsUrl: 'x' }` as `AuthFeaturesConfig` compiles (exit 0), and
`{ deviceAuthorization: 'yes' }` is refused TS2322 (exit 2). Probe
deleted.
- No other workspace consumer: `git grep` for `AuthFeaturesConfig` /
`GetAuthConfigResponse` outside `packages/spec` finds
`packages/client/src/index.ts` (re-export) and
`ui/packages/auth/src/types.ts` (a hand-written mirror that does not
import the spec type).
- Ablation: `deviceAuthorization` renamed to
`deviceAuthorizationAblated` in the schema with
`scripts/ablation-replace.mjs` (anchor 1 to 0, blob `b237957a` to
`595c0b73`): `auth-endpoints.test.ts` exit 1, 3 failed (the set-equality
pin, that flag's boolean case, the whole-response parse). Restored: blob
equal to HEAD `b237957a`, `git diff HEAD` empty.
- Level axis, offline: see Why (`none` exit 1, `added` exit 0).
- Scoped lint: `pnpm exec eslint --no-inline-config` over the two
changed .ts files, exit 0. `check-nul-bytes` exit 0.
- Dogfood battery: n/a, no `!` in the title and no `(narrowing)` arm.
</details>
<details><summary>Gates (dispatch-gates --commands)</summary>
- At head `d68dd29f` (after merging `origin/main` `208d3e2e`): 103
derived commands, all 103 exit 0, each run with stdin closed and its
exit captured before any pipe. Before them, `turbo run build
--concurrency=2 --filter='./packages/**'` under os-verify-lock, VERDICT
0 (78 tasks).
- `dispatch-gates --ran`: 103 derived, 103 run, 0 NOT-MEASURED (a
derived zero), 0 UNRUN.
- At the earlier head `7d5a57a6` the same 103 ran; three first exited 3
(PREREQUISITE NOT MET) and passed once their inputs existed:
`check-plugin-teardown-shape.mjs --self-test` after `git fetch
--depth=1` of its pinned fixture commit `621a4876`,
`check:skill-examples` after building `@objectstack/client-react`, and
`check:dual-build-cjs-loads` after building every package.
- Offline PR-body checks with this body: `check-changeset-no-major.mjs
--base origin/main --event` exit 0 (level axis `yes (widening)` +
`added`, spec `minor`); the same with only the protocol line set to
`none` exit 1 (see Why); `check-partof-closing-keyword.mjs` exit 0.
- Local-only families: `check:skill-frame-freshness` exit 0;
`check:dev-prereqs` exit 1 because 35 of 106 workspace packages (the
`ui/` tree among them; only `packages/**` was built here) have no dist
in this container: a machine-state reading, not a finding of this diff
(`packages/spec` dist is fresh).
- CI: the seat reads the PR's check-runs.
</details>
## Acceptance notes
- `ui/packages/auth/src/types.ts` (lines 284 to 289) documents
`features.sso` as "wired; when falsy the `/sign-in/sso` route isn't
mounted", while the served value is "usable", so a served `false` can
mean wired with no provider yet and the route mounted. The type (`sso?:
boolean`) is right. Prose only, for the ui lane; not touched here (this
diff does not edit `ui/`). The same hand-written mirror is also where
the nine keys would be typed for the console, if the ui lane wants them
typed.
- `tenancyPosture` and `audiencePosture` are closed enums. A plain
object schema fails the whole parse on an unknown enum member, so a
server that later adds a fourth posture fails an older client's
`GetAuthConfigResponseSchema` parse instead of being stripped. That is
the contract-first reading (both vocabularies are the producer's own
types today); noted for the contract review.
- `termsUrl` / `privacyUrl` are not URL-validated, matching the
producer, which serves any trimmed non-empty `OS_TERMS_URL` /
`OS_PRIVACY_URL` value.
## Verification
See the readings above. The PR stays a draft; the seat runs the contract
review, and the nine key names plus the `devSeedAdmin` ruling go to the
maintainer for the nod.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (scoped to the changed
files locally; the repo-wide run is CI's).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
## Maintainer approval
Warren approved the nine served `features` keys (`multiOrgEnabled`,
`degradedTenancy`, `oidcProvider`, `deviceAuthorization`, `admin`,
`tenancyPosture`, `audiencePosture`, `termsUrl`, `privacyUrl`) as named,
with `devSeedAdmin` left undeclared: 「全部批准」 on the decision card
「六个命名/措辞全部按推荐批准吗?」 in the project thread "Decide pending spec name
approvals", 2026-11-03 00:08Z. Recorded on this PR in seat comment
6591810483.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbaRoocSDkgqwRAGseR)_
<!-- signed: domain:spec#3 · os-warren ·
session_01JQwxbahe4v7GBD94QeSnir -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 744211f commit 4b06a54
5 files changed
Lines changed: 192 additions & 0 deletions
File tree
- .changeset
- content/docs/references/api
- packages/spec
- authorable-surface
- src/api
| 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 | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
68 | 77 | | |
69 | 78 | | |
70 | 79 | | |
| |||
193 | 202 | | |
194 | 203 | | |
195 | 204 | | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
196 | 214 | | |
197 | 215 | | |
198 | 216 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
| 212 | + | |
| 213 | + | |
212 | 214 | | |
| 215 | + | |
| 216 | + | |
213 | 217 | | |
| 218 | + | |
| 219 | + | |
214 | 220 | | |
215 | 221 | | |
216 | 222 | | |
217 | 223 | | |
| 224 | + | |
218 | 225 | | |
219 | 226 | | |
| 227 | + | |
| 228 | + | |
220 | 229 | | |
221 | 230 | | |
222 | 231 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
14 | 19 | | |
15 | 20 | | |
16 | 21 | | |
| |||
176 | 181 | | |
177 | 182 | | |
178 | 183 | | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
179 | 278 | | |
180 | 279 | | |
181 | 280 | | |
| |||
0 commit comments