Skip to content

feat(client,plugin-auth): bind set-initial-password into the SDK and ledger it as an sdk mount - #11360

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10974-sdk-set-initial-password
Aug 23, 2026
Merged

feat(client,plugin-auth): bind set-initial-password into the SDK and ledger it as an sdk mount#11360
os-zhuang merged 2 commits into
mainfrom
claude/issue-10974-sdk-set-initial-password

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#10974
Fixes#10975

One statement in one PR: the method exists · the ledger declares it · the URL resolves to an enumerated route.

POST /api/v1/auth/set-initial-password has been mounted by AuthPlugin for as long as the SSO-onboarding flow has existed, with no SDK method and no ledger row. This binds it into ObjectStackClient as auth.setInitialPassword and records the exact AUTH_ROUTE_LEDGER row that names that method.

The ruling this PR carries

Maintainer's option C (2026-08-22) settled the disposition: bind the route into the SDK, then ledger it sdk. It was filed as two cards, #10974 then #10975, and the SDK half landed here first — at which point the split turned out to be measurably unsatisfiable. client-url-conformance.test.ts bounds wildcard-only matches at zero ("ZERO IS THE POINT: … Raising this bound reintroduces the one kind of evidence this audit family has caught being wrong"), so the method matching through the dispatcher's * /auth/** row was a hard failure, not the weak pass #10974's body predicted. The only thing that turns it green is the exact row — which #10974's own fence excluded.

Maintainer ruling A — combine (2026-08-23): both halves land here. That changes the packaging, not the surface — option C had already authorised exactly one method and exactly one row.

⚠️Clause-② stands unchanged. This widens published SDK surface. needs:contract-review stays hung, the PR stays draft, no ready-flip, no auto-merge, no merge-queue enqueue on the dispatching seat's authority, and the reviewing seat must not be the dispatching seat.

The matched pattern, named before and after

A green suite either way would not show the displacement; the pattern name does. Both readings come from the conformance test's ownmatches(), via a temporary probe that was restored by trap after each run, so no probe is in the diff. Each probe's injection was confirmed on disk before its run (anchor and marker counts, byte delta) and the tree was verified clean again afterwards.

BEFORE — at f9f809d2, from the unmodified test's own failing assertion, no probe needed:

auth.setInitialPassword → POST /api/v1/auth/set-initial-password (via * /auth/**): expected 1 to be +0

and, naming the compiled pattern's source:

OSPROBE auth.setInitialPassword -> POST /api/v1/auth/set-initial-password (via * /auth/**) [pattern source: dispatcher]

AFTER — at fbd9af2d, printed by vitest itself from a probe assertion:

OSPROBE matched-pattern-name for auth.setInitialPassword POST /api/v1/auth/set-initial-password;
pattern source=auth: expected 'POST /api/v1/auth/set-initial-password' to be 'OSPROBE-SHOW-ME'

* /auth/** (compiled from the dispatcher ledger, source: 'dispatcher') → POST /api/v1/auth/set-initial-password (compiled from AUTH_ROUTE_LEDGER, source: 'auth'). The sort at client-url-conformance.test.ts:132 put the exact row ahead of the wildcard family exactly as its comment says it would. wildcardOnly is now empty and its bound is untouched at toBe(0).

The :171 pin — extended by ADDITION, argued on its own terms

the objectstack-mounted rows are the ones auth-plugin.ts serves itself pins the source: 'objectstack' set to a hand-written literal list. The card said 3 → 4; re-derived on this branch it is 11 → 12 — the pin grew through #10534's follow-ups after #10975 was written. The assertion, its shape and the live.has(route) loop are unchanged; one string literal was added, in the position .sort() puts it.

The pin's own two terms, both measured:

1. auth-plugin.ts mounts it directly. Re-derived rather than trusted — #10975 measured line 1716, it is now 1741:

// packages/plugins/plugin-auth/src/auth-plugin.ts:1741
rawApp.post(`${basePath}/set-initial-password`, async (c: any) => {

A raw-app mount ahead of the catch-all, so better-auth's wire table cannot publish it and the enumeration (which reads .path) never sees it.

2. live.has(route) is false. The assertion's own reading, printed by vitest, with a positive control so a false from an empty set cannot pass for a measurement:

pin=12 · live.size=131 · live.has('POST /api/v1/auth/set-initial-password')=false
· CONTROL live.has('POST /api/v1/auth/sign-in/email')=true

131 live routes enumerated, the control route present, ours absent. It does not come from better-auth.

⛔ Not reached by loosening the assertion, deleting the pin, or replacing the literal list with a computed one.

The five gates, individually — all at fbd9af2d

GateResult
auth-route-ledger.conformance.test.ts:152 every sdk row names a client methodevery 'sdk' entry names its client method; every non-sdk entry carries a rationale
:160 no route ledgered twice, every row under the auth base pathno route is ledgered twice, and every row is under the auth base path
:171 the objectstack pinthe objectstack-mounted rows are the ones auth-plugin.ts serves itself
:189gap and mismatch only shrinkgap and mismatch counts only shrinkboth still 0; no gap row was added, and no ratchet moved
client-url-conformance.test.ts:363 every client method classified✓ the method drives; not parked in NON_HTTP, no DRIVE override
client-url-conformance.test.ts:373 every URL matches a mounted route✓ now green on the exact row (see above)

Whole files: auth-route-ledger.conformance.test.ts10 passed (10); @objectstack/client23 files / 314 tests passed (was 22/1-failed and 313/1-failed at f9f809d2 — the one failure was :373).

One file outside the two halves, and why it is not scope creep

scripts/check-auth-mount-ledger.mjs carried a PENDING_DISPOSITION entry for this exact route, whose own text reads "This entry is deleted by #10975." The gate reconciles that list in both directions and raises resolved-pending when an entry's route acquires a ledger row — so landing the row without deleting the entry is a red gate, by design. The entry is deleted; PENDING_MAX is not changed (it stays 1, and lowering it would break the gate's own self-test case that exercises a one-entry list). Two sentences of prose that the deletion made false were corrected in the same file. The ratchet coming down, in the gate's own words:

check-auth-mount-ledger --self-test: 42 assertions OK (right boundary, lane exclusion, rationale, pending ratchet).
check-auth-mount-ledger: OK -- 17 ObjectStack auth mount(s), all accounted for
(13 by a reviewed ledger row, 4 shadowing a vendor-declared path, 0 pending a disposition).

Nothing was weakened to get here

⛔ The wildcardOnly bound stays at 0 (option D was presented and not taken). ⛔ The method is not in NON_HTTP. ⛔ gap and mismatch stay at 0. ⛔ The route's accept/reject behaviour, its admit set and its server-side guards are untouched — this binds a client and records a row; it does not change what the mount allows. ⛔ Nothing else in packages/plugins/plugin-auth/** is touched.

Verification — all at fbd9af2d, clean tree

Gate families derived from the real change set with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-built path list; the script read the merge-base change set of 5 paths and asserted the repo against this checkout's origin). 18 path-matched families plus the 6 convention-triggered ones for a touched test file, every exit code captured before any pipe:

  • suites: plugin-authauth-route-ledger.conformance.test.ts (10/10) · @objectstack/client full suite (23 files / 314 tests)
  • typecheck: @objectstack/client (incl. check:test-typecheck"0 file(s) / 0 error(s)") · @objectstack/plugin-auth
  • check:auth-mount-ledger · check:engine-double-contract · check:where-matcher · check:query-options-erasure · check:type-check-coverage · check:nul-bytes · check:ratchet-remedy-authority · check:cross-package-test-inputs · check:test-source-alias · check:type-source-resolution · check:published-files · check:slot-lookup · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:changeset-gate-self-tests · check:objectui-changeset
  • check-adr-0087-registration · check-changeset-no-major · check-ci-filter-parity · check-cross-package-test-inputs · check-empty-changeset · check-plugin-teardown-shape · check-affected-docs
  • repo-widepnpm lint (eslint . --no-inline-config) — run in full, not narrowed
  • check:type-check-debt --re-measure on a fully built workspace closure: "33 ledger entr(ies) re-measured … 1897 raw tsc error(s) total, none above its recorded number" (its first invocation refused with NOT MEASURED on an unbuilt worktree — its designed refusal; the closure was built and it was re-run)

Changeset present and extended to both packages (@objectstack/client minor, @objectstack/plugin-auth patch).

Refs

Part of #10534, which remains open until all 17 of its mounts are accounted for — its ledger leg for this route is now clear, and its docs leg is #10660's. Also #10050 · #3563 / #3642 (the conformance guards) · #3656 (the auth ledger's charter) · #9941 (the pin's add-member precedent) · #3528 (the coverage-lie failure the combined order avoids: the row and the method exist in the same commit).

Out-of-scope finding #11359 (the auth ledger's client names are never resolved against a real client, unlike every sibling ledger) is filed, unassigned, and is not fixed here.


Generated by Claude Code

…itial-password route
`AuthPlugin` mounts `POST /api/v1/auth/set-initial-password` on the raw Hono
app, but no `ObjectStackClient` method built the URL — measured zero for both
`setInitialPassword` and `set-initial-password` across `packages/client/src`,
against four sibling auth members returning non-zero on the same corpus.
The method is shaped like its namespace siblings (`getConfig`,
`changePassword`, `changeEmail`): `this.getRoute('auth')` + `this.fetch`, POST
with a JSON body, returning the parsed envelope.
The route's own accept/reject behaviour, admit set and server-side guards are
untouched. This binds a client to an already-mounted route.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
@github-actions

github-actionsBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/client, @objectstack/plugin-auth, touching 3 documentable anchor(s).

1 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/set-initial-password (route))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 23 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 9337cd8104cb4640b5616a7227c55b9fb5a1222epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 8ee1fceec3f103fd6f239c6858e6ad62ecb390bf — the merge of head fbd9af2da4b8d16ca92a466899526cba906e99ee into base 9337cd8104cb4640b5616a7227c55b9fb5a1222e, 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 8ee1fceec3f103fd6f239c6858e6ad62ecb390bf && git checkout 8ee1fceec3f103fd6f239c6858e6ad62ecb390bf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9337cd8104cb4640b5616a7227c55b9fb5a1222e fbd9af2da4b8d16ca92a466899526cba906e99ee && git checkout -B drift-repro 9337cd8104cb4640b5616a7227c55b9fb5a1222e && git merge --no-ff fbd9af2da4b8d16ca92a466899526cba906e99ee
node scripts/docs-audit/affected-docs.mjs --json 9337cd8104cb4640b5616a7227c55b9fb5a1222e

⚠️ 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 9337cd8104cb4640b5616a7227c55b9fb5a1222e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM: this red is known, diagnosed and ruled. Fix is in flight — no action needed from a reader.

Test Core (3/6) at f9f809d2, one failure out of 314:

AssertionError: methods matched only by a wildcard `**` family — weaker evidence than an exact
route, and demonstrably able to be wrong (#3718). Enumerate the family instead:
auth.setInitialPassword → POST /api/v1/auth/set-initial-password (via * /auth/**): expected 1 to be +0
❯ src/client-url-conformance.test.ts:473:7

Not a defect in this PR's code.#10974's body predicted this leg would go green on weak wildcard evidence and asked the dev to name the matched pattern. It does not: the assertion bounds wildcard-only matches at toBe(0)"ZERO IS THE POINT: … Raising this bound reintroduces the one kind of evidence this audit family has caught being wrong." So the card's acceptance clause ("both legs pass") and its fence ("⛔ not in this card: the AUTH_ROUTE_LEDGER row") are jointly unsatisfiable — the exact ledger row is the only thing that turns it green, and it was fenced out.

⭐ The dev did not reach for any of the ways to make this green cheaply: no NON_HTTP parking, no raising the bound, no ledger row against the fence. It reported instead. That is the correct outcome and the reason this comment exists rather than a quiet patch.

Maintainer ruling, 2026-08-23 — option A, combine. The exact AUTH_ROUTE_LEDGER row lands in this PR alongside the method, so :373 turns green on exact evidence rather than wildcard evidence. #10975 folds in; this PR will carry Fixes #10974 and Fixes #10975. Full four-axis analysis on #10974 (5386473767), ruling recorded at 5386490928, notice to the domain:services seat on #10975 (5386494321).

Gate status unchanged while that lands: Clause-②: yes, needs:contract-review hung, PR stays draft, ⛔ not enqueued, ⛔ not armed.


Generated by Claude Code

…k mount
Second half of the maintainer's option-C ruling, landed in the same PR as the
first half by the follow-up ruling of 2026-08-23 (combine).
`AUTH_ROUTE_LEDGER` gains the exact row for
`POST /api/v1/auth/set-initial-password` — `family: 'objectstack-mount'`,
`source: 'objectstack'`, `disposition: 'sdk'`,
`client: 'auth.setInitialPassword'` — shaped like the two ObjectStack `sdk`
mounts it sits beside.
The `:171` pin (`the objectstack-mounted rows are the ones auth-plugin.ts
serves itself`) goes 11 → 12 BY ADDITION: the assertion, the pin and the
`live.has(route)` loop are untouched. Both of the pin's own terms hold for the
new entry — auth-plugin.ts mounts it directly on the raw app ahead of the
catch-all, and better-auth does not publish it.
`scripts/check-auth-mount-ledger.mjs`'s PENDING_DISPOSITION entry for this
route is deleted, which is that shrink-only ratchet coming down: the gate fails
`resolved-pending` if an entry survives its disposition landing, and the entry
said so itself. PENDING_MAX is NOT changed.
⛔ No guard was weakened to reach this: the `wildcardOnly` bound stays at 0, the
method is not parked in `NON_HTTP`, `gap`/`mismatch` stay at 0, and the route's
accept/reject behaviour, admit set and server-side guards are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM note for the contract reviewer: scripts/check-auth-mount-ledger.mjs is in this diff, and it is the ratchet coming DOWN, not a gate weakening. Verified, not taken on trust.

A gate script inside a Clause-② PR should make a reviewer tense up, so here is the check I ran rather than the dev's word for it.

What changed: the single entry in PENDING_DISPOSITION — a shrink-only exemption list — was deleted, and the surrounding prose moved to past tense ("the list HAS held a legitimate entry", and the failure message now reads "is EMPTY today").

Why the deletion is required, from the gate's own source (origin/main, :506-513):

if(ledgered.has(p.route)||vendor.has(p.route)){findings.push({kind: 'resolved-pending',text: `PENDING_DISPOSITION entry ${p.route} now HAS a ledger row (${p.issue} landed). `+`Delete the entry -- that is how the ratchet comes down.`,});}

Once the sdk row lands, leaving the entry is a finding. So the deletion is what the gate demands, not an accommodation of it — and the gate says so in its own words.

⭐ Stronger still: the gate's self-test at :754-758 was written against this exact route and this exact disposition

[{route: p[0].route,source: 'objectstack',disposition: 'sdk',client: 'auth.setInitialPassword'}],'a pending entry whose route is now ledgered passed -- the ratchet cannot come down'

— so today's landing is the scripted outcome the gate rehearsed in advance. The deleted entry's own text also said "This entry is deleted by #10975."

Direction check: shrink-only lists are weakened by growing. This one shrank 1 → 0. Nothing was exempted, no threshold moved, no assertion loosened.

The :171 pin was widened the ruled way too — by addition: 'POST /api/v1/auth/set-initial-password' now sits alongside the three existing source: 'objectstack' rows, 3 → 4. ⛔ Not by loosening the assertion, deleting the pin, or replacing the literal list with a computed one, which were the failure modes fenced against at dispatch.

Lane crossings, named rather than glossed: this PR now reaches packages/plugins/plugin-auth/** (domain:services) and scripts/** (domain:devx) from the domain:cli seat. Both are under the maintainer's 2026-08-23 combine ruling; notice was posted to the services side on #10975 (5386494321). The scripts/** touch is the landing half of a ruling recorded inside the deleted entry itself, not a lane land-grab.

Gates unchanged: Clause-②: yes, needs:contract-review hung on both cards and this PR, still draft, ⛔ not enqueued, ⛔ not armed.

Out of scope and expected: the docs-drift advisory names content/docs/permissions/authentication.mdx via the /api/v1/auth/set-initial-password route. That is #10534's docs leg, which #10975's body assigns to #10660 — this PR clears the ledger leg only.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM: green at fbd9af2d. The combine ruling is validated by measurement, not by argument.

Every job completed: success; only Console Pin Gate and Build Docs skipped, both legitimately. All six Test Core shards pass — including the one that was red at f9f809d2.

That green is itself the proof the displacement happened. The assertion is expect(wildcardOnly.length).toBe(0). At f9f809d2 it read expected 1 to be +0, the 1 being auth.setInitialPassword matched only by the dispatcher's * /auth/** family. It now reads 0, which can only be true if the method is matched by an exact enumerated route — i.e. the AUTH_ROUTE_LEDGER row this PR added. The wildcard could not have been displaced by anything else in the diff.

So maintainer ruling A (combine) is confirmed the right call on evidence: the two halves were jointly unsatisfiable apart, and together they close on exact evidence rather than weak.

Still owed by the dev, and I will hold the review note until it lands: the matched-pattern name before and after, which the dispatch demanded as a top-level field. The ratchet reaching 0 establishes that an exact route matched; the pattern name establishes which, and a card whose whole subject is "weak evidence is not evidence" should not settle for the inference when the reading is available.

Gates unchanged and this is not a hand-off to merge: Clause-②: yes, needs:contract-review hung on #10974, #10975 and this PR, still draft, ⛔ not enqueued, ⛔ not armed. Review routing is not this seat's to short-circuit — it dispatched this card.

Prior PM notes on this PR stand: the scripts/check-auth-mount-ledger.mjs touch is the ratchet coming down (5386773210, verified against the gate's own source and self-test), and the :171 pin was widened 3 → 4 by addition.


Generated by Claude Code

@os-zhuangos-zhuang changed the title feat(client): add auth.setInitialPassword, binding the mounted set-initial-password routefeat(client,plugin-auth): bind set-initial-password into the SDK and ledger it as an sdk mountAug 23, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 15:59
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit d3bee87Aug 23, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10974-sdk-set-initial-password branch August 23, 2026 16:17
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

2 participants

@os-zhuang@claude