Skip to content

docs(plugin-auth): ledger and document the unaccounted ObjectStack auth mounts - #10660

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10534-auth-mount-ledger-and-docs
Aug 22, 2026
Merged

docs(plugin-auth): ledger and document the unaccounted ObjectStack auth mounts#10660
os-zhuang merged 1 commit into
mainfrom
claude/issue-10534-auth-mount-ledger-and-docs

Conversation

@os-warren

@os-warrenos-warren commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Part of #10534

Scope is the card's follow-ups 1 and 2 only, per the triage ruling and the claim comment. Follow-up 3 (the REGISTRAR_FILE_RE bridge) and follow-up 4 (the proposed gate) are untouched — see Out of scope below, including a state correction on the reason follow-up 3 was fenced.

1. The census, re-run — and one number the card got wrong

Re-measured on origin/main at 47aff0938 (the card measured 4544be260), running the card's own expression rather than trusting it.

Population: still 17.rawApp. mounts spelled with a template literal on ${basePath} in auth-plugin.ts, excluding the rawApp.all catch-all, the rawApp.use IP-gate middleware, and the .well-known/* mounts that are not under basePath.

Leg 1 — documented? 6 undocumented, not 5

The card's test — does the literal wire path appear anywhere in the hand-written docs corpus — is a substring test, and one route is a prefix of another:

/api/v1/auth/admin/sso/register is a prefix of /api/v1/auth/admin/sso/register-saml

/admin/sso/register was credited as documented on the strength of the only occurrence in the corpus being register-saml's URL at content/docs/permissions/sso.mdx:222. Re-run with a right boundary (a negative lookahead for word characters and hyphen):

undocumented
naive substring (the card's method)5
boundary-aware6 — the 5 named, plus /admin/sso/register

So the card's "5 remaining" was one short, and the missing one was hidden by the measurement rather than by the corpus. All 6 are documented here.

Positive controls (leg 1), because two of the numbers above are zeros:

known-documented control /api/v1/auth/sign-in/email strict-present: true
impossible control /api/v1/auth/zzz-not-a-route present: false
add-member (documented by #10050's PR) strict-present: true

The card's own control — organization/add-member present in the undocumented set — now reports false, correctly: #10050's PR documented it. It is replaced above rather than dropped, since a control that has been fixed is no longer a control.

Leg 2 — ledgered? 9, exactly as the card found

9 of 17 in neitherAUTH_ROUTE_LEDGER nor BETTER_AUTH_MOUNTED_SURFACE, matching the card member-for-member. Positive control (leg 2): the same per-route grep returns 1 for /config and 1 for /organization/add-member.

After this PR

undocumented (boundary-aware): 0 of 17
in neither ledger half: 1 of 17 <- set-initial-password, deliberately (section 3)

2. Follow-up 1 — a verdict per route, not a bulk edit

The card permits documenting or recording a deliberate server-only, not user-facing verdict, and names bootstrap-status as a likely candidate. All six are documented; none earned the server-only verdict, and the reason differs per route:

routeverdictwhy
GET /bootstrap-statusdocumentThe card's hypothesis is falsifiable and false: client.auth.bootstrapStatus() is a shipped ObjectStackClient method, ledgered disposition: 'sdk'. A public SDK method IS user-facing surface; an undocumented one is the declared-is-not-documented gap, not a server internal.
POST /set-initial-passworddocumentSession-required, browser-facing; the Console's Profile → Password card and Set Password page drive it. Also the card's named trap — named in authentication.mdx ("offers set-initial-password") with no URL. That mention now carries the literal path and links to the new section.
POST /admin/unban-userdocumentIts five siblings (create-user, set-user-password, import-users, unlock-user, ban-user) are all documented. A ban that documents no way back is the gap.
POST /admin/sso/registerdocumentThe 6th, above. Its SAML counterpart is documented one line away; the OIDC one never was.
POST /admin/sso/request-domain-verificationdocumentThe whole opt-in flow (ADR-0024 second clause, OS_SSO_DOMAIN_VERIFICATION) had zero mentions anywhere in content/docs or docs — not the URLs, not the env var.
POST /admin/sso/verify-domaindocumentSame flow, step 2.

"Mentioned" is not "documented" was treated as the bar throughout: every route above gets its literal wire path plus what it actually does, refuses, and requires.

⚠️One behaviour claim I nearly wrote wrong. The first draft said both domain-verification halves answer 404 when the feature is off. Reading register-sso-provider.ts instead of the mount's comment: step 1 translates the inner 404 into 400 DOMAIN_VERIFICATION_DISABLED, step 2 passes the 404 through. The docs now state the asymmetry and tell readers to match on the code, not the status.

3. Follow-up 2 — 8 rows, and one deliberate refusal

Eight mounts gain reviewed source: 'objectstack' rows, all disposition: 'server-only'. Disposition was decided, not defaultedserver-only means "deliberately not SDK surface", which is a claim about intent. It was tested per route by measuring who builds the URL:

Positive control on the SDK grep (packages/client/src), so the zeros below are real:

bootstrap-status 2 organization/add-member 0
sign-in/email 2 admin/create-user 0
get-session 6 admin/set-user-password 0

Every one of the eight came back with zeroObjectStackClient callers and exactly one real caller that is a declarative surface — a metadata action target on sys_user / sys_sso_provider / sys_oauth_application, or the Console import wizard (objectuiapp-shell/src/views/identityImport.ts). That is the organization/add-member precedent exactly. Their peer routes are uniformly SDK-absent too, so "the SDK deliberately does not express platform-operator user administration" is the surface's real shape, not a story written to fit a row.

POST /api/v1/auth/set-initial-password is NOT ledgered — escalated instead

It is the ninth unledgered mount and the one place I stopped. It fails the test above in a way none of the other eight do:

  • Its caller is @object-ui/auth's createAuthClient, which builds exactly four auth URLs: /config, /get-session, /list-accounts, /set-initial-password.
  • The first three are all ledgered sdk with ObjectStackClient methods (auth.getConfig, auth.me, auth.accounts.list). Only this one has none.
  • Its own sibling branch in the same Console password card — changePassword — is ledgered sdk.

That is the shape of gap ("should be in the SDK and is not"), not server-only. And gap is ratcheted to zero by this file's own conformance suite. So the two available moves were: write server-only (a false declaration of intent, chosen to dodge a ratchet) or write gap (turns the suite red). Neither is mine to pick — whether ObjectStackClient should express setInitialPassword is a product decision. It is in the report's open_questions with options and a recommendation. The refusal is recorded in both the ledger and the conformance test so the next reader is not left wondering.

Contrast that with /admin/import-users, whose caller is also a hand-written objectui client rather than a metadata action: its peer group (create-user, ban-user, set-user-password) is uniformly SDK-absent, so it is consistent, and server-only is honest. The discriminator is the peer group, not convenience.

The conformance pin moves in the same edit

'the objectstack-mounted rows are the ones auth-plugin.ts serves itself' pins the source: 'objectstack' set exactly — it grows 3 to 11. That is the closest mechanical check that exists today for "an ObjectStack mount with no ledger row", and its comment now says so, along with what it still cannot catch (it is hand-written, so it sees a row that disappears, not a mount that never got one).

4. Ablation

Signature predicted before running, then observed:

Delete the POST /api/v1/auth/admin/unlock-user row → the objectstack-mounted pin fails, own comes back with 10 entries instead of 11, and the diff names the absent route.

Observed:

x the objectstack-mounted rows are the ones auth-plugin.ts serves itself
AssertionError: expected [ ...(10) ] to deeply equal [ ...(11) ]
- "POST /api/v1/auth/admin/unlock-user",
Test Files 1 failed (1) Tests 1 failed | 9 passed (10)

No rebuild is involved, and that is proven rather than asserted: the suite imports the ledger relativelyfrom './auth-route-ledger' at auth-route-ledger.conformance.test.ts:27 — so resolution cannot go through the package exports field and no dist/ sits between the edit and the run. Mutation confirmed on disk (the row grepped to 0 occurrences), not from an editor's exit code.

Restore proven byte-identical, not merely "looks right":

pre-ablation : 123e1a87dcedf63dc188cf680c2fc3ed08417913
restored : 123e1a87dcedf63dc188cf680c2fc3ed08417913
git diff --quiet -> exit 0

5. Gates

All run at the final commit 3115f7939 on a clean working tree. Family derived by node scripts/pm/dispatch-gates.mjs with no path arguments (5 paths, 20 matched families plus the convention-triggered set). Exit codes captured before any pipe (cmd > file 2>&1; EXIT=$?), never after a tail.

gateits own verdict line
plugin-auth full suiteTest Files 61 passed (61) · Tests 1359 passed (1359)
plugin-auth typechecktsc --noEmit, exit 0
check:affected-docs✓ affected-docs self-test: 281 cases pass.
check:docs-audit-scope✓ docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).
✓ release-owned pages are in scope and read-only: 9 page(s) ... review-only
check:doc-anchors✅ ... slug parity, custom ids, duplicate counters ... (277 live fragment links)
spec check:docs✅ 229 generated files in sync with packages/spec
check:nul-bytesOK (scanned 6212 text file(s) ... no raw ASCII control bytes).
check:route-envelope (+ --self-test)✓ Plugin-mounted Hono routes — 12 module(s) audited ... 8 conformant, 0 ratcheted
check:dispatcher-error-vocabulary (+ --self-test)8 shapes + 102 assertions OK
check:where-matcher✓ where-matcher conformance holds: 271 matcher(s) discovered, 271 answer the combinator battery correctly
check:cross-package-test-inputsOK: 13 package(s) read outside themselves, all declared
check:type-check-coverage✓ --self-test — 23 semantic + 24 observation + 25 re-measure + 28 built-closure + 9 auto-lowering case(s) hold.
check:engine-double-contract · check:query-options-erasure · check:slot-lookup · check:test-source-alias · check:type-source-resolution · check:role-word · check:docs-redirects · check:published-readme-links · check:objectui-changeset · check:changeset-gate-self-tests · check-changeset-no-major · check-empty-changeset · check-adr-0087-registration · spec check:empty-state / check:liveness / check:strictness-ledger / check:variant-docsall exit 0

⚠️Two gates the derivation did not name (class #10309) — check:route-envelope and check:dispatcher-error-vocabulary, both of which bite plugin-auth — were run explicitly. Confirmed: neither appears in the derivation's output for this change set. That is a live instance of the class, recorded here rather than only in the report.

One declared narrowing:check:type-check-debt --re-measure was not run. It refuses on an unbuilt worktree and needs the whole workspace closure built, a full-workspace build under a container-shared verification lock. In its place, pnpm --filter @objectstack/plugin-auth typecheck (tsc --noEmit) passes at exit 0, and the diff adds only string-literal array entries to an existing test file. CI runs the ratchet regardless.

⚠️@objectstack/plugin-auth TEST_DEBT (card #10615): untouched. The number was not edited, --lower was not run, and nothing here re-measures it. The edited test file is pre-existing (auth-route-ledger.conformance.test.ts), so no package gains or loses test-file coverage.

Out of scope

Follow-up 3 — a state correction. The card and the claim comment both fence it on the grounds that scripts/docs-audit/affected-docs.mjs is held by open PR #10501. Verified directly rather than taken on trust: #10501 is merged (closed 2026-08-21T02:46:52Z, merged: true, commit 4e7229a6). The stated blocker has therefore cleared. It is still not touched here, because the scope ruling admits follow-ups 1 and 2 only — but the reason it was deferred no longer holds, which is the PM's call to make.

Follow-up 4 — not built, as instructed. Concrete evidence about how such a gate should be written came out of this work and is in the report; the one worth repeating here is that a mount-vs-docs gate must match the wire path with a right boundary, or it silently credits a prefix route to its longer sibling — the exact artifact that made this card's leg 1 read 5 instead of 6.

Filed unassigned, not fixed:#10658check-error-code-casing prints ✓ no lowercase error codes in 4346 scanned file(s) while two ObjectStack-authored lowercase codes ship in register-sso-provider.ts, because a literal reached through an || fallback matches none of its four recognizers (verified with a positive control on the gate's own regex). Same "partial read reported as complete" class #10501 just closed for a different scanner. Not fixed here: different defect class.

No route's mounting, behaviour, or accept/reject set is changed by this PR (Clause-② no). Nothing under content/docs/releases/** or packages/spec is touched.


Generated by Claude Code

…th mounts
auth-plugin.ts mounts 17 routes directly on the raw Hono app ahead of the
better-auth catch-all. Nine were in neither half of auth-route-ledger.ts and
six carried no literal wire path in the hand-written docs.
Ledger: eight gain reviewed `source: 'objectstack'` rows, all `server-only`,
each measured to have zero ObjectStackClient callers and exactly one real
caller (a declarative metadata action target or a Console wizard). The
conformance pin over the objectstack-sourced set grows 3 -> 11 in the same
edit, so the enlarged set stays reviewable.
set-initial-password is deliberately left unledgered: its peer group makes it
read as `gap`, not `server-only`, and `gap` is ratcheted to zero — so a
disposition there would be a false declaration written to dodge a ratchet.
Escalated on the issue instead.
Docs: bootstrap-status, set-initial-password, admin/unban-user,
admin/sso/register and the two admin/sso domain-verification routes now carry
their literal wire paths. admin/sso/register was NOT documented before — the
census's substring test had credited it to its longer sibling register-saml.
No route's mounting, behaviour or accept/reject set changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 10 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authentication.mdx(via /api/v1/auth/admin/import-users (route), /api/v1/auth/admin/unlock-user (route), /api/v1/auth/set-initial-password (route))
  • content/docs/permissions/sso.mdx(via /api/v1/auth/admin/sso/register (route), /api/v1/auth/admin/sso/register-saml (route), /api/v1/auth/admin/sso/request-domain-verification (route), /api/v1/auth/admin/sso/verify-domain (route))
  • content/docs/protocol/objectui/actions.mdx(via /api/v1/auth/admin/oauth2/toggle-disabled (route), /api/v1/auth/sys-oauth-application/register (route))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via /api/v1/auth/admin/import-users (route))
  • content/docs/releases/v16.mdx(via /api/v1/auth/admin/import-users (route))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9dd192d48b17e1d60abc47f16bd9d3c0212d237bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 6a2e2d6df472be799b0eb8270396510465bd5ce9 — the merge of head 3115f7939f7517da5de14df074b15937f2493ff1 into base 9dd192d48b17e1d60abc47f16bd9d3c0212d237b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6a2e2d6df472be799b0eb8270396510465bd5ce9 && git checkout 6a2e2d6df472be799b0eb8270396510465bd5ce9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9dd192d48b17e1d60abc47f16bd9d3c0212d237b 3115f7939f7517da5de14df074b15937f2493ff1 && git checkout -B drift-repro 9dd192d48b17e1d60abc47f16bd9d3c0212d237b && git merge --no-ff 3115f7939f7517da5de14df074b15937f2493ff1
node scripts/docs-audit/affected-docs.mjs --json 9dd192d48b17e1d60abc47f16bd9d3c0212d237b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9dd192d48b17e1d60abc47f16bd9d3c0212d237b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32550339300 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.00s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 73 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 163a162Aug 22, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10534-auth-mount-ledger-and-docs branch August 22, 2026 04:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-warren@os-zhuang@claude