Repository navigation
feat(spec): declare the nine served features keys on the auth config response and pin the registry to the schema - #29583
Conversation
…registry to the schema Claude-Session: https://claude.ai/code/session_01JQwxbaRoocSDkgqwRAGseR Co-authored-by: Claude <noreply@anthropic.com>
… reference Claude-Session: https://claude.ai/code/session_01JQwxbaRoocSDkgqwRAGseR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbaRoocSDkgqwRAGseR Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b8c078a61aab6b8c1175ac28cb0a5869338a486e && git checkout b8c078a61aab6b8c1175ac28cb0a5869338a486e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 107be283f030242a7803848ae31a1ec11e9891dc a4a79ea320ddb1df2876917b63848aa94a79bd8f && git checkout -B drift-repro 107be283f030242a7803848ae31a1ec11e9891dc && git merge --no-ff a4a79ea320ddb1df2876917b63848aa94a79bd8f
node scripts/docs-audit/affected-docs.mjs --json 107be283f030242a7803848ae31a1ec11e9891dc |
|
domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
…link too Claude-Session: https://claude.ai/code/session_01JQwxbaRoocSDkgqwRAGseR Co-authored-by: Claude <noreply@anthropic.com>
|
domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir Contract review — deltaServed-tier: Closure of the prior should-fix
New findings
Check-runs on
|
|
domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir 维护者速读
席位核对ACCEPT ·
Generated by Claude Code |
|
domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir Maintainer approval · 2026-11-03T00:10ZWarren approved this PR's open item at 2026-11-03T00:08Z in the project thread "Decide pending spec name approvals". The card asked 「六个命名/措辞全部按推荐批准吗?」 and he picked 「全部批准」. The post above that card listed this PR's item as follows.
With this, the gate the seat ACCEPT named is met. The head is unchanged since the ACCEPT. The seat now readies this PR and arms auto-merge, and Generated by Claude Code |
Requested by Warren · project thread
Fixes #29473
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none —
git grepunderskills/finds noauth/configreference, and the one hit for any of the nine key names isskills/objectui/guides/auth-permissions.mdusingfeatures.multiOrgEnabledas an expression example, which stays true; the API reference page is regenerated in this diff维护者速读
GET /api/v1/auth/config响应契约AuthFeaturesConfigSchema新增九个可选键(服务端早就在发、控制台早就在读,只是契约没声明),并加一条测试把「已分类的键」与「契约声明的键」钉成同一集合。sso、cloudManaged都是这样),这条钉子让下一个漏声明的键在 CI 直接变红。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
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.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'ssemantics.PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS:tenancyPosturereusesTenancyPostureSchema(security/tenancy-posture.ts, the vocabulary the producer'sTenancyPosturetype is),audiencePosturereusesAUDIENCE_POSTURES(system/auth-config.zod.ts; the producer reports an off-vocabulary configured value asinvite_only), andtermsUrl/privacyUrlare plain strings, because the producer passes the trimmedOS_TERMS_URL/OS_PRIVACY_URLvalue through without URL validation (a relative path is served as-is). Both imported modules import onlyzodandlazySchema, so no cycle.devSeedAdminis ruled out as dev-only, not declared. plugin-auth adds it at the top level of the response only underNODE_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 onGetAuthConfigResponseSchema.packages/spec/src/api/auth-endpoints.test.ts, blockAuthFeaturesConfig declares every classified served key): the schema's declared keys, minus the retiredPUBLIC_AUTH_FEATURES_NOT_ADVERTISEDtombstones (passkeys,magicLink), equalPUBLIC_AUTH_FEATURE_NAMESplusPUBLIC_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.packages/spec/authorable-surface/api.json(nineapi/AuthFeaturesConfig:*rows, written by the build) andcontent/docs/references/api/auth-endpoints.mdx(gen:docs, the one artifactcheck:generatednamed stale).api-surfacedid not move: no export was added..changeset/29473-auth-config-features-served-keys.md:@objectstack/specminor, both declaration lines.Producer readings (origin/main
390bd2ae,packages/plugins/plugin-auth/src/auth-manager.tsgetPublicConfig()): thefeaturesliteral servestwoFactor,organization,multiOrgEnabled,tenancyPosture,audiencePosture,degradedTenancy,oidcProvider,sso,ssoEnforced,deviceAuthorization,admin,phoneNumber,phoneNumberOtp,cloudManaged, and conditionallytermsUrl,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.devSeedAdminsits besidefeatures, not in it.Why
GET /api/v1/auth/configserves these nine keys and the console reads several of them (for exampledeviceAuthorizationinui/apps/console/src/pages/auth/DeviceAuthPage.tsx), but the contract did not declare them, so a client parsing throughGetAuthConfigResponseSchemalost all nine: a plain object schema strips unknown keys. Nothing tied the registry to the schema, which is howssoandcloudManagedeach needed their own card.Why
Metadata-protocol: added, not the claim'snone. The claim recordednone("a wire response, not authorable metadata") and asked the dev to confirm against AGENTS.md Post-Task Checklist step 3. Step 3 definesaddedas "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 thessorow PR #29455 added underadded), and this diff adds nine rows toapi.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 --eventexits 1 undernone("metadata-protocol declaresnonewhile an authorable-surface or authorable-defaults baseline moved … Moved: packages/spec/authorable-surface/api.json") and exits 0 underadded("LEVEL AXIS …yes (widening)and metadata-protocoladded"). Soadded, with@objectstack/specatminor, is the only declaration the gate accepts, and it matches thessoprecedent on the same schema.Tests
packages/spec/src/api/auth-endpoints.test.ts, new block, 38 cases:trueandfalseand refuses'yes'with exactly oneinvalid_typeissue at its own path, expectedboolean;GetAuthConfigResponseSchemaparse unchanged;tenancyPosture/audiencePostureis refused with oneinvalid_valueissue at its path; a non-stringtermsUrl/privacyUrlis refused with oneinvalid_typeissue.Local readings (head d68dd29)
pnpm --filter @objectstack/spec buildunder os-verify-lock, VERDICT 0, thencheck:generatedexit 1 naming one stale artifact (content/docs/references/**);gen:docsVERDICT 0;check:generatedagain 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).turbo run build --concurrency=2 --filter=@objectstack/client...thenpnpm --filter @objectstack/client typecheck(it re-exportsAuthFeaturesConfig/GetAuthConfigResponse): VERDICT 0. Reverse check that it reads the rebuilt declarations: a probe inpackages/client/srctyping{ deviceAuthorization: true, tenancyPosture: 'group', audiencePosture: 'open', termsUrl: 'x' }asAuthFeaturesConfigcompiles (exit 0), and{ deviceAuthorization: 'yes' }is refused TS2322 (exit 2). Probe deleted.git grepforAuthFeaturesConfig/GetAuthConfigResponseoutsidepackages/specfindspackages/client/src/index.ts(re-export) andui/packages/auth/src/types.ts(a hand-written mirror that does not import the spec type).deviceAuthorizationrenamed todeviceAuthorizationAblatedin the schema withscripts/ablation-replace.mjs(anchor 1 to 0, blobb237957ato595c0b73):auth-endpoints.test.tsexit 1, 3 failed (the set-equality pin, that flag's boolean case, the whole-response parse). Restored: blob equal to HEADb237957a,git diff HEADempty.noneexit 1,addedexit 0).pnpm exec eslint --no-inline-configover the two changed .ts files, exit 0.check-nul-bytesexit 0.!in the title and no(narrowing)arm.Gates (dispatch-gates --commands)
d68dd29f(after mergingorigin/main208d3e2e): 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.7d5a57a6the same 103 ran; three first exited 3 (PREREQUISITE NOT MET) and passed once their inputs existed:check-plugin-teardown-shape.mjs --self-testaftergit fetch --depth=1of its pinned fixture commit621a4876,check:skill-examplesafter building@objectstack/client-react, andcheck:dual-build-cjs-loadsafter building every package.check-changeset-no-major.mjs --base origin/main --eventexit 0 (level axisyes (widening)+added, specminor); the same with only the protocol line set tononeexit 1 (see Why);check-partof-closing-keyword.mjsexit 0.check:skill-frame-freshnessexit 0;check:dev-prereqsexit 1 because 35 of 106 workspace packages (theui/tree among them; onlypackages/**was built here) have no dist in this container: a machine-state reading, not a finding of this diff (packages/specdist is fresh).Acceptance notes
ui/packages/auth/src/types.ts(lines 284 to 289) documentsfeatures.ssoas "wired; when falsy the/sign-in/ssoroute isn't mounted", while the served value is "usable", so a servedfalsecan 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 editui/). The same hand-written mirror is also where the nine keys would be typed for the console, if the ui lane wants them typed.tenancyPostureandaudiencePostureare 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'sGetAuthConfigResponseSchemaparse 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/privacyUrlare not URL-validated, matching the producer, which serves any trimmed non-emptyOS_TERMS_URL/OS_PRIVACY_URLvalue.Verification
See the readings above. The PR stays a draft; the seat runs the contract review, and the nine key names plus the
devSeedAdminruling go to the maintainer for the nod.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses (scoped to the changed files locally; the repo-wide run is CI's).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Maintainer approval
Warren approved the nine served
featureskeys (multiOrgEnabled,degradedTenancy,oidcProvider,deviceAuthorization,admin,tenancyPosture,audiencePosture,termsUrl,privacyUrl) as named, withdevSeedAdminleft 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