Repository navigation
fix(client)!: bind the oauth.* family to the wire shapes better-auth sends - #15445
Conversation
…sends Four methods of the `oauth.*` namespace ended `return res.json()` with no return annotation, so `lib.dom`'s `Response.json(): Promise<any>` was their published type. Each now declares the shape its route actually serves, and its `exported-any-returns.json` entry is deleted in the same change: oauth.applications.register -> OAuthApplicationRegistration oauth.applications.get -> OAuthApplication oauth.applications.getPublic -> OAuthApplicationPublic oauth.consent -> OAuthConsentResult The shapes were read off the wire against a real server, not off better-auth's own `.d.ts` — twice the vendor's declaration was the wider, wrong answer: `getPublic` is declared as the full client row but its handler hand-picks seven columns, and `user_id` / `application_type` are declared nullable while the serialiser folds a null column to `undefined`. Timestamps are RFC 7591 numbers (Unix epoch seconds), not `Date` and not ISO-8601 strings: the provider converts its stored `Date` to a number before serialising, so no `Date` reaches the wire and no revival layer exists. `oauth.applications.delete` is deliberately NOT bound and keeps its ledger entry: its route answers HTTP 200 with a zero-byte body, so its `res.json()` rejects with a SyntaxError on every successful delete. No annotation can be honest while that call stands, and binding it needs a behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 14 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 3e522119655fdc37d07255b7e96d90286810c488 && git checkout 3e522119655fdc37d07255b7e96d90286810c488
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ba426b0f091b8d5901acf10bb8e649b514349f49 747b9e670c6bda153e0b12fb814f4d4eecd8b1fa && git checkout -B drift-repro ba426b0f091b8d5901acf10bb8e649b514349f49 && git merge --no-ff 747b9e670c6bda153e0b12fb814f4d4eecd8b1fa
node scripts/docs-audit/affected-docs.mjs --json ba426b0f091b8d5901acf10bb8e649b514349f49
|
…owing The changeset declares `**BREAKING**` (and a title bang) and carried no `adr-0087:` disposition, so `check-adr-0087-registration.mjs` exited 1 and Check Changeset has been red on this PR since 16:01Z. This adds the one disposition line the gate requires. Category: `type-surface-only` (#13080). All four of its predicates were checked against this diff rather than assumed: 1 published @objectstack/client has no `private` field in packages/client/package.json. 2 no-spec-diff the four changed paths carry no `packages/spec/` prefix. 3 no-metadata-surface-diff `metadataSurfaceKind` is null for all four. 4 narrowed-from-erased read at both revs with the gate's own `readDeclaredTypeSurface`: `register`, `getPublic` and `consent` are unannotated at the merge base and concrete at HEAD. Predicate 4 is NOT claimed for the fourth narrowed member. The reference `packages/client/src/index.ts#get` does not address `oauth.applications.get`: the file declares 13 members named `get` and the predicate reads the FIRST (line 1928), which is an unrelated member, unannotated at both revs. Naming it would assert a verified fact about the wrong member, so it is named in the disposition's prose instead and the ambiguity is filed as its own card. No annotation, exported type, method body or ledger row is touched: the contract settled by the at-tier review is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Status correction — this PR is GREEN, and the REJECT's sole blocking finding is clearedThis seat has reported this PR's colour wrongly in both directions, so here is the measurement rather than a recollection. What I measured, just now, at head
|
⚖️ CONTRACT-REVIEW VERDICT (re-review) — ACCEPT WITH FINDINGS. Blocking: none.At-tier contract reviewer ( 1. CI, read by me — at 2026-09-05T00:45:42Z, head unchanged33 check runs, all
This reading expires with the next push or re-run; it is a measurement with a timestamp, not a property of the PR. 2. The prior REJECT's sole blocking ground — confirmed cleared, independently
I ran both gates myself in the worktree at 2026-09-05T00:38:13Z, against the merge base 3. The ADR-0087 marker, on its merits —
|
Contract re-review: ACCEPT WITH FINDINGS — no blocking finding. Flipping ready and enqueueing.Verdict at ⛔ First, correcting myself againIn my previous comment I wrote It changes no conclusion — the check is green either way — but a seat whose central rule is "a CI reading is a fact with a timestamp" does not get to be sloppy with the timestamp. The reviewer caught it, which is what an independent reviewer is for. What the review established, independently of me
|
| body says | truth |
|---|---|
"Final commit c0dbf802c9, clean tree." |
Head is 747b9e670c6, 2 commits. |
"the PR is draft and carries needs:contract-review. It waits for an at-tier contract reviewer; auto-merge is not armed and it is not enqueued." |
Superseded by this comment: the review is done, the label is removed, and the PR is being flipped and enqueued. |
| "The #13080 BREAKING-token gate is not addressed here" | Contradicted by the changeset, which carries the BREAKING banner. |
| gate-union claim ("44 commands, 41 pass") | Predates the ADR-0087 run; the reviewer's own gate runs supersede it. |
One finding I am NOT acting on, and one card that now exists
- ⭐ The previous verdict's finding 2 is refuted on the code — the server sets the
acceptheader itself beforeauthorize, and noredirect: falseexists anywhere in the dist. ⛔ The card I had planned to file for it will not be filed; filing it would have put a false claim in the backlog. - The marker promised a card for the
#getambiguity and none existed. The reviewer filed check-adr-0087-registration: atype-surface-onlyreference is a bare identifier resolved to the FIRST same-named definition in the file, so a nested SDK member with a common name (oauth.applications.get) cannot be named — PR #15445 had to leave its fourth narrowed member out of its marker #15627 (bare, unassigned). It will recur on family cards 2 and 3 (client SDKauth.*family: bind the 14return res.json()methods (auth 7 · sessions 3 · twoFactor 3 · accounts.unlink 1) to their better-auth wire shapes — #12104 family card 2 of 3 #14313, client SDKorganizations.*family: bind the 19return res.json()methods (organizations 11 · invitations 3 · teams 5) to their better-auth wire shapes — #12104 family card 3 of 3 #14314). - Non-blocking prose nits in the marker (says 13
getmembers, measured 14; cites line numbers that rot) are not worth a push on their own.
NOT MEASURED, recorded as such
The wire bytes end-to-end (producer code verified; the end-to-end read was the dev's), resources element type and jwks shape, optional unions beyond spot checks, and the sibling repos. ⛔ None of these is a pass.
⇒ Green, mergeable_state: clean, reviewed at tier, no blocking finding, no governed surface touched. Flipping ready and enqueueing through the merge queue.
Generated by Claude Code
…ts route answers
`ObjectStackClient.oauth.applications.delete` ended `return res.json()` on a
route that answers HTTP 200 with a ZERO-BYTE body, so it rejected with
`SyntaxError: Unexpected end of JSON input` on EVERY successful delete — after
the row had already been committed away server-side. The method had no success
path a caller could observe, and the obvious recovery (retry) failed
DIFFERENTLY, with the route's 404 `not_found`.
Measured end to end, not inherited: real betterAuth + real oauthProvider over
the real ObjectQL adapter, a real signed-up user and session, driven through the
real client with only the socket stood in for.
POST /oauth2/delete-client -> 200 · 0 bytes · content-type application/json
· NO content-length header
through the client, before -> REJECTED: SyntaxError
the row, server-side -> ALREADY GONE (get-client answers 404)
through the client, after -> RESOLVED | undefined
Emptiness is detected by READING the body. Both shortcuts were measured and both
are unusable here: the status is 200, not the 204 five other delete surfaces in
this file key off, and the response carries no `content-length` header at all.
A non-empty body is still parsed and its failure still thrown, so the ONLY
behaviour that moves is the zero-byte case. `void` is the wire fact: "deleted"
and "was already gone" are distinguished on the ERROR channel (404 `not_found`,
raised by `this.fetch` before any success value exists), so a synthesised
`{ deleted: true }` would be a shape the wire never sends.
`exported-any-returns.json` loses this method's entry in the same change — the
ledger is shrink-only, so the entry goes WITH the binding. Its last `oauth.*`
entry is now gone; 35 sites remain open. The `toEqualTypeOf<any>()` pin PR
#15445 left behind for exactly this moment is replaced by
`returnTypePrecisionPins15451`, and the reject/resolve flip — which no
compile-time assertion can observe — is pinned in a new runtime suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
….delete` `**BREAKING**` on two independent axes: the declared return moves off an erased `any` onto `void` (a compile break, though only for reads of a value the promise never produced), and the runtime flips from always-rejecting to resolving, so a caller's `catch` stops firing on success.⚠️ The ADR-0087 disposition is NOT claimed, and the gate is expected to red on this changeset until a maintainer settles it. Both legs were measured rather than guessed: type-surface-only REFUSED at predicate 4. A reference is a bare identifier resolved to the FIRST same-named definition, and index.ts declares TEN members named `delete`; the first (line 2397) is unannotated at both revs, so the gate reports "still UNANNOTATED" about a member this diff never touched. Issue #15627, filed off PR #15445 where the same ambiguity cost the `get` member its place in the marker — here it blocks the only member there is. no-migration-prescription mechanically ACCEPTED, and deliberately not taken. ADR-0087's D7 records that #8277 held this exemption on a detector MISS rather than a positive finding, and names that as the pattern the sixth category exists to stop. Taking it here, with the measurement in hand, would repeat it knowingly. Dropping the `**BREAKING**` token is the third exit and ADR-0087's addendum closes it for this class in as many words. So the honest state is a loud red on one gate with the reasoning written down, rather than a green held by a category that does not describe this diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…00 its route answers, instead of rejecting on every successful delete (objectstack-ai#15675) * fix(client)!: bind `oauth.applications.delete` to the zero-byte 200 its route answers `ObjectStackClient.oauth.applications.delete` ended `return res.json()` on a route that answers HTTP 200 with a ZERO-BYTE body, so it rejected with `SyntaxError: Unexpected end of JSON input` on EVERY successful delete — after the row had already been committed away server-side. The method had no success path a caller could observe, and the obvious recovery (retry) failed DIFFERENTLY, with the route's 404 `not_found`. Measured end to end, not inherited: real betterAuth + real oauthProvider over the real ObjectQL adapter, a real signed-up user and session, driven through the real client with only the socket stood in for. POST /oauth2/delete-client -> 200 · 0 bytes · content-type application/json · NO content-length header through the client, before -> REJECTED: SyntaxError the row, server-side -> ALREADY GONE (get-client answers 404) through the client, after -> RESOLVED | undefined Emptiness is detected by READING the body. Both shortcuts were measured and both are unusable here: the status is 200, not the 204 five other delete surfaces in this file key off, and the response carries no `content-length` header at all. A non-empty body is still parsed and its failure still thrown, so the ONLY behaviour that moves is the zero-byte case. `void` is the wire fact: "deleted" and "was already gone" are distinguished on the ERROR channel (404 `not_found`, raised by `this.fetch` before any success value exists), so a synthesised `{ deleted: true }` would be a shape the wire never sends. `exported-any-returns.json` loses this method's entry in the same change — the ledger is shrink-only, so the entry goes WITH the binding. Its last `oauth.*` entry is now gone; 35 sites remain open. The `toEqualTypeOf<any>()` pin PR objectstack-ai#15445 left behind for exactly this moment is replaced by `returnTypePrecisionPins15451`, and the reject/resolve flip — which no compile-time assertion can observe — is pinned in a new runtime suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * chore(changeset): declare the BREAKING binding of `oauth.applications.delete` `**BREAKING**` on two independent axes: the declared return moves off an erased `any` onto `void` (a compile break, though only for reads of a value the promise never produced), and the runtime flips from always-rejecting to resolving, so a caller's `catch` stops firing on success.⚠️ The ADR-0087 disposition is NOT claimed, and the gate is expected to red on this changeset until a maintainer settles it. Both legs were measured rather than guessed: type-surface-only REFUSED at predicate 4. A reference is a bare identifier resolved to the FIRST same-named definition, and index.ts declares TEN members named `delete`; the first (line 2397) is unannotated at both revs, so the gate reports "still UNANNOTATED" about a member this diff never touched. Issue objectstack-ai#15627, filed off PR objectstack-ai#15445 where the same ambiguity cost the `get` member its place in the marker — here it blocks the only member there is. no-migration-prescription mechanically ACCEPTED, and deliberately not taken. ADR-0087's D7 records that objectstack-ai#8277 held this exemption on a detector MISS rather than a positive finding, and names that as the pattern the sixth category exists to stop. Taking it here, with the measurement in hand, would repeat it knowingly. Dropping the `**BREAKING**` token is the third exit and ADR-0087's addendum closes it for this class in as many words. So the honest state is a loud red on one gate with the reasoning written down, rather than a green held by a category that does not describe this diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * feat(spec): register the oauth.applications.delete binding in the ADR-0087 ledger The changeset for this branch declared a breaking change and carried no `adr-0087:` disposition marker, so `check-adr-0087-registration` refused it. The maintainer ruling on objectstack-ai#15674 (comment 5549251533, 2026-09-05) settled the disposition: this class routes through `registered`, not through any `not-required` category, and the sixth category's text stays as written. - `entries/semantic/18.client-oauth-applications-delete-void.ts` — one `SemanticMigration` for major 18, the open window (`PROTOCOL_VERSION` is 17.0.0). surface: both halves of what a caller of `client.oauth.applications.delete` observes — the declared return, `any` to `void`, and the settle behaviour, reject-on-every-successful-delete to resolve. replacement: no value; the migration is on the settle path, the `catch` that fired on every successful delete now fires only on a real failure. reason: the wire is byte-identical and no `packages/spec` declaration moves, so the ledger is the only channel — and the half that actually ran has NO diagnostic, since a `try`/`catch` around the call compiles identically before and after while its `catch` stops executing. The TS2339 the type move produces names only code that was unreachable. acceptanceCriteria: every `catch` around the call re-read by hand, and the measured populations (this repo: zero production call sites; objectui at the pinned `.objectui-sha`: zero; cloud: NOT MEASURED). - `migrations/registry.ts` — regenerated by `gen:migration-registry` (187 semantic, 161 retired-key, 116 retired-def); never hand-edited. - The changeset's non-claim note becomes `adr-0087: registered client-oauth-applications-delete-void`; the rest of the file is byte-identical and keeps its `**BREAKING**` declaration. Anchors live in a source comment above the type import rather than in the entry's strings: the strings are projected into `spec-changes.json` and `docs/protocol-upgrade-guide.md` when 18 becomes current, and this repo's issue numbers do not resolve for the consumers who read those. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * docs(spec): the ledger entry names the flat better-auth error body, not an ADR-0112 envelope `entries/semantic/18.client-oauth-applications-delete-void.ts` listed "same ADR-0112 error envelope" among the things this change leaves untouched on the wire. The operative claim is true — the error bodies do not move — but the label is wrong for this route. `POST {auth}/oauth2/delete-client` is better-auth's, and its 404 answers the vendor's FLAT shape, `{"error_description":"client not found","error":"not_found"}`, not ObjectStack's nested ADR-0112 envelope. The repo states the distinction verbatim at `packages/plugins/plugin-auth/src/admin-remove-user-gate-ordering.test.ts:86`: "ObjectStack's ADR-0112 envelope nests it; better-auth's flat shape does not". Worth a correction rather than a follow-up because the string is consumer-facing: entries prefixed `18.` project into `spec-changes.json` and `docs/protocol-upgrade-guide.md` when major 18 becomes current, and a reader told "same ADR-0112 error envelope" would write a nested read against a flat body. What the entry asserts is unchanged; only the mislabel moves. `migrations/registry.ts` is regenerated by `gen:migration-registry`, never hand-edited, and a re-run at this head is a byte no-op — the entry is indexed, not merely present. This branch squashes, so its commit messages enter `main` verbatim and this one carries the current state of the disposition question. The ADR-0087 disposition IS claimed: the changeset carries `adr-0087: registered client-oauth-applications-delete-void`, and `check-adr-0087-registration` exits 0 at this head, printing "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition" against that marker. An earlier message on this branch states the disposition is not claimed and that the gate is expected to red until a maintainer settles it; that expectation is superseded — the maintainer settled it as ruling D on objectstack-ai#15674 (2026-09-05), and this class routes through ADR-0087 `registered`. Measured at this head, exit codes captured by redirect-then-capture: `gen:migration-registry` re-run byte no-op (`git hash-object` unchanged), `check:spec-changes` "spec-changes.json is up to date.", `check:upgrade-guide` "protocol-upgrade-guide.md is up to date.", `check:adr-0087-registration` self-test (332 assertions) and main both exit 0, the spec migrations suite 117 passed, `@objectstack/spec` typecheck exit 0, and `check:nul-bytes`, `check:doc-authoring`, `check:spec-parsed-alias` exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --------- Co-authored-by: Claude <noreply@anthropic.com>
…s to the commits that decided them (stage 10) (objectstack-ai#20750) Part of objectstack-ai#20234 Clause-②: no Stage 10 of the dead-citation sweep: the migration registry's hand-written entries. Every tracker number in `packages/spec/src/migrations/entries/**` that no longer exists on the board now cites the commit that decided it, in ruling C+D form C (ruling `5749154545` on objectstack-ai#19123). Where the number alone carried the meaning, the line now says what was decided. That is 131 sites over 21 numbers. All of them are comments; the entries' string literals carry no dead number. `migrations/registry.ts` moves only by `gen:migration-registry`. No entry id, literal, order, `conversionIds` or code token moves, and no live citation is removed. ## Boundary - **In:** all of `entries/**`. The gate's census reads 117 sites there, under the claim's ~120 slicing threshold, so there is no slice. My raw walk finds 14 more comment sites that the census does not count (see *Census* below), which gives 131 in total. They are the same dead card on the line after a counted site, plus one README line, so they are rewritten with it. - **Regenerated only:** `migrations/registry.ts`, 128 lines. `spec-changes.json` and `docs/protocol-upgrade-guide.md` do not move, because entry comments are never projected into them. Both `check:` scripts pass with no regeneration. - **Out, per the claim:** `registry.ts`'s hand-written parts. They still hold 13 dead sites, listed under Acceptance notes. The six `18.*-unit-in-key.ts` entries that PR objectstack-ai#20706 edits carry no dead site, and I did not touch them. I did not touch `conversions/` or other lanes' sites. ## The anchors (one per number, reused from earlier stages where they anchored the same number) | number | sites | anchor | what the rewritten line says it decided | |---|---|---|---| | objectstack-ai#13135 | 26 (13 are `re-charter objectstack-ai#13135`) | commit 9e0ba21 | retires the paper metadata-customization protocol; re-charter of objectstack-ai#12057, which stays | | objectstack-ai#8495 | 23 | commit 4bfe1a5 (PR objectstack-ai#8666 kept) | the precedent: the first 17.x-line narrowing registered under protocol 18 (its own second commit says so) | | objectstack-ai#8715 | 16 | commit 2c86fe3 | the ApiKeySchema retirement; its message names the "route 3" kit (no carrier key, no tombstone, no D2) | | objectstack-ai#14691 | 11 | commit b3a63d3 | retires the ten inert `RestServerConfig` keys | | objectstack-ai#10724 | 11 | commit be21955 | retires the nine dead `contributes` members | | objectstack-ai#14369 | 10 | commit a3d5724 | the liveness census that recorded the 15 `dead` rows | | objectstack-ai#10485 | 6 | commit 35ad101 | retires the `themes` carrier and `ThemeSchema` | | objectstack-ai#11846 | 5 | commit 0c2334f | retires preview mode; its own text carries the ruling record (objectstack-ai#12428 kept) | | objectstack-ai#14676 | 5 | commit 13c48c2 | retires `connector.errorMapping` | | objectstack-ai#11332 | 3 | commit dce5cd4 | retires the manifest's three dead containers | | objectstack-ai#6361 | 2 | commit 90bbf25 | retires the notification-list `cursor` on both halves | | objectstack-ai#10627 | 2 | commit be21955 | that commit records the controlled monorepo census | | objectstack-ai#14365 | 2 | commit f60ab90 | the open `z.partialRecord` proposal that commit's changeset recorded; the retirement leaves no record to reshape | | objectstack-ai#14526 | 2 | commit db16b94 | the landing of the client envelope convergence (anchor block, and the README's measured case) | | objectstack-ai#6363 | 1 | commit 17d0954 | "ruled jointly with the `unreadCount` fix" | | objectstack-ai#6239 | 1 | commit f549a0d | the ViewProtocol retirement sweep | | objectstack-ai#10726 | 1 | commit bc56e18 | retires `contributes.routes` (Option B) | | objectstack-ai#10812 | 1 | commit be21955 | "the cloud census", whose 2026-08-24 reading @5b5925a that commit's message records | | objectstack-ai#9041 | 1 | commit d491625 | the url-branch refinement (objectstack-ai#9147, live, stays) | | objectstack-ai#14996 | 1 | commit db16b94 | the ADR-0087 registration, which landed in the same squash (its body: "Registration requested on objectstack-ai#14996") | | objectstack-ai#14312 | 1 | commit e944fdb | the `oauth.*` binding, which left `applications.delete` out as a behaviour change (PR objectstack-ai#15445 kept) | Two lines change without a number, so the sentence still reads: `patterns`' follow-on line and the `oauth` anchor block's continuation line. In total 133 lines are removed and 133 added in 86 files, and every file is balanced. ## Verification record (final head `1ee5841c09`; base `fbec216e2d`) **Census** (the gate's own `check-issue-citations.mjs --census --json`, board enumerated, 186 pages): | subtree | base (00:21Z, frontier objectstack-ai#20740) | head (01:18Z, frontier objectstack-ai#20743) | |---|---|---| | `entries/retired-keys` | 73 | 0 | | `entries/retired-defs` | 40 | 0 | | `entries/semantic` | 4 | 0 | | `registry.ts`, generated regions | 114 | 0 | | `registry.ts`, hand-written parts | 2 | 2 | | `chain.ts`, `types.ts`, `index.ts`, `spec-changes.ts`, tests | 0 | 0 | | **`migrations/`** | **233** | **2** | | `packages/spec/src` | 235 | 4 | - **Site-set difference:** 254 sites are only at base. 231 are this diff's (117 plus 114 copies). The other 23 are `plugin-audit`'s, from `main`'s objectstack-ai#20737. 0 sites are only at head. - **Kind** (every census site is a comment): a TypeScript 6.0.3 walker classes all 131 entry sites as comments. It also finds 13 dead string sites, all in `registry.ts`'s hand-written rationale (see Acceptance notes). - **Probe:** REST `issues/N` without following redirects, over all 302 distinct in-repo numbers of 100 or more in `migrations/`. 277 answer 200 and 25 answer 404. Controls were read at start, every 50 and end: lit objectstack-ai#20234 200 (8/8), dead objectstack-ai#8710 404 (8/8). - **Blind spots:** the census does not count 14 of the 131 entry sites. - 13 are `re-charter objectstack-ai#13135`, because `NON_CITATION_HEADS` reads `re-charter #N` as an ordinal. - 1 is in `entries/README.md`, which is outside the gate's `packages/**/src/**/*.ts` surface. - **Numbers:** no tracker number is added. The only numbers on added lines are the live ones kept from the same lines: objectstack-ai#8666, objectstack-ai#12057, objectstack-ai#8586, objectstack-ai#12428, objectstack-ai#9147 and objectstack-ai#15445. The diff-scoped gate judges them: "every citation this change adds resolves". **Residue** (scratch walker over the TypeScript parser, per file, base vs head, 86 `.ts` files): - The file with every comment range cut out, everything else byte for byte, is IDENTICAL. - The leaf-token stream is IDENTICAL: 38,417 tokens, aggregate `67c1f6db944e93ed`. - **Controls** mutate the head text in memory only, 259 of 259 as expected: - Expected identical: a comment insertion in every file. - Expected to differ: a string-literal edit, a template-literal edit, a regex-literal edit (where the file has one) and an appended declaration. **Regeneration** (`gen:migration-registry`): - `check:migration-registry` exits 1 before and 0 after. - The registry diff is 128 lines out and 128 in. As (old, new) pairs they equal the entry diff's pairs, re-indented by four spaces. - 0 changed lines fall outside the generated regions. - 5 entry pairs are deliberately not carried: the README line, and the semantic "Anchors" header lines, which sit above a blank line and so are not in the carried comment run. - `check:spec-changes` and `check:upgrade-guide` exit 0 with no regeneration. **Build and tests** (under `os-verify-lock`): - Build: `turbo run build` over `./packages/*` and `./packages/*/*`, 71 of 71, at `845e90fea4` and again at `1ee5841c09`. - `check:generated`: all 15 artifacts up to date, at both heads. - spec `local` project: 576 files, 16,991 passed and 1 todo, at both heads. - spec `repo` project: 41 of its 45 files, 666 passed, at both heads. - spec `typecheck`: exit 0; 53 files / 251 errors / 138 pinned signatures held. **Gates:** `dispatch-gates --commands` derives 82 gates, and the derivation is identical before and after the second merge. At `1ee5841c09` all 82 exit 0. `--ran` reconciles: 82 derived, 82 run, 0 NOT-MEASURED, a derived zero. **Lint** (a proven narrowing): - `eslint --no-inline-config --format json` over the 86 touched `.ts` files: 86 files, 0 errors, 0 warnings. - `isPathIgnored` is false for all 86. - `eslint.config.mjs:327-328` states that type-aware linting is never enabled, so a comment edit cannot move an untouched file's verdict. **Changeset:** `patch`. - In the built `dist`, the new wording (for example "the precedent of commit 4bfe1a5, PR objectstack-ai#8666") appears in `dist/migrations/index.js` and `.mjs`, and the old wording appears in 0 files. - Control: an unchanged phrase appears in the same 2 files. - So the published `@objectstack/spec/migrations` entry carries these comments. **Merges:** `origin/main` was merged twice through `scripts/pm/os-regen-merge.sh`. Neither merge left a regeneration to commit. - Since then, `main` has advanced to `97005aed04`, and none of those commits touches `packages/spec`. - Driver-free `merge-tree` probes (from a bare `--shared` scratch clone with no `merge.*` config) exit 0 against that `main` and against PR objectstack-ai#20706's head `d8bac3877d`. ## Acceptance notes - **The next stage for this card: 13 dead sites left in `registry.ts`'s hand-written parts.** They are outside this claim, which says `registry.ts` is regenerated only. - **Comments (form C), 2:** - `:5940` objectstack-ai#8495, the step-18 doc comment on the persistence placeholder refusal. - `:22819` objectstack-ai#6239, the `RETIRED_DEFS_BY_MAJOR[17]` doc comment. - **Author-shown rationale strings (form D), 11:** - Step 17's `:344` objectstack-ai#6345. It projects into `docs/protocol-upgrade-guide.md:68`, so that stage regenerates the guide. - Step 18's fragments: `:5121` objectstack-ai#14676, `:5455` objectstack-ai#12868, `:5526` objectstack-ai#10329, `:5544` objectstack-ai#8495, `:5555` objectstack-ai#13135, `:5742` objectstack-ai#10724, `:5745` objectstack-ai#10627, `:5752` objectstack-ai#10726, `:5799` objectstack-ai#10485, `:5816` objectstack-ai#10926. These are not projected into either artifact yet. - **Census blind spots, measured here and not filed** (a coverage boundary, not one of the three filing classes; new gates default to no): - `NON_CITATION_HEADS` skips `re-charter #N` even where N is a tracker card. - `.md` files under `packages/**/src` are outside the surface. - **NOT MEASURED locally, left to CI:** - `scripts/build-schemas-check-mode.test.ts` hit the foreground cap alone (exit 124 after 560 s, no output). It parses `registry.ts`'s source; the residue check above shows that source's code and string tokens are unchanged. - The other three heavy `repo`-project files (`def-key-collisions`, `publish-smoke-boot-failure`, `publish-smoke-port-collision`) were not run, as in stages 8 and 9. - **Hypothesis 6's baseline half is falsified by construction.** `scripts/doc-authoring-prose-id.baseline.json` has no `packages/spec` row, because its leg excludes `packages/spec`. `check:doc-authoring` reads strings only, so a comment-only diff cannot move it. It read 808 sites across 229 files at `845e90fea4` and 794 across 227 at `1ee5841c09`. The difference is `main`'s own re-anchor commits; this diff touches no row. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #14312 — card 1 of 3 of the #12104 family. Merging this does not complete that card: four of its five methods are bound here, and the fifth is an open question stated below.
Clause-② is yes (this narrows a published return type), so the PR is draft and carries
needs:contract-review. It waits for an at-tier contract reviewer; auto-merge is not armed and it is not enqueued.What changed
Four
oauth.*methods endedreturn res.json()with no return annotation, solib.dom'sResponse.json(): Promise< any >was their published type. Each now declares the shape its route serves, and itsexported-any-returns.jsonentry is deleted in the same change.oauth.applications.registeranyOAuthApplicationRegistrationoauth.applications.getanyOAuthApplicationoauth.applications.getPublicanyOAuthApplicationPublicoauth.consentanyOAuthConsentResultLedger: 40 entries before, 36 after — exactly these four deleted, nothing else touched.
The shapes were read off the WIRE
The ruling is explicit that better-auth's own types are the pre-serialization server shape and the wire is the only source of truth. So these were measured, not transcribed: a real
betterAuthwith the realoauthProviderplugin over the realcreateObjectQLAdapterFactoryon a real ObjectQL engine and a real better-sqlite3 database, a real signed-up user and a real session cookie, driven through the realObjectStackClientwith only the socket stood in for.register— POST /oauth2/create-client, HTTP 201:{"client_id":"pWhEhQXRmdLVGVZkgiobMJfxEmsgVCoB","client_secret":"EPsjhqMCoPzoNPiQdMRLDPfcUIWDnFmp","client_secret_expires_at":0,"scope":"openid profile email","user_id":"t3IvR7eGC7IHhZnZUy4ftuLgeesecJ4a","client_id_issued_at":1788534576,"client_name":"Probe App","client_uri":"````''https://app.example.com","logo_uri":"https://app.example.com/logo.png","contacts":["ops@example.com"],"tos_uri":"https://app.example.com/tos","policy_uri":"https://app.example.com/privacy","redirect_uris":["https://app.example.com/cb"],"token_endpoint_auth_method":"client_secret_basic","grant_types":["authorization_code","refresh_token"],"response_types":["code"],"application_type":"web","disabled":false,"dpop_bound_access_tokens":false}''````get— GET /oauth2/get-client, HTTP 200: the same row withclient_secretstripped.getPublic— GET /oauth2/public-client, HTTP 200:{"client_id":"pWhEhQXRmdLVGVZkgiobMJfxEmsgVCoB","client_name":"Probe App","client_uri":"````''https://app.example.com","logo_uri":"https://app.example.com/logo.png","contacts":["ops@example.com"],"tos_uri":"https://app.example.com/tos","policy_uri":"https://app.example.com/privacy","redirect_uris":[]}''````consent— POST /oauth2/consent, HTTP 200 — accept, then deny:{"redirect":true,"url":"https://app.example.com/cb?code=B5icJNiIgnGcmb8iHusBf2ehnAvxGHQ2&state=st4te&iss=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fv1%2Fauth"} {"redirect":true,"url":"https://app.example.com/cb?error=access_denied&error_description=User+denied+access&state=st4te&iss=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fv1%2Fauth"}Both decisions answer the same shape and the same 200; the outcome rides in
url. Soredirectis not the accept/deny signal, and the type says so.Twice the vendor's
.d.tswas the wider, wrong answergetPublicis declaredOAuthClient— the full row — but its handler hand-picks seven columns (clientId,name,uri,contacts,icon,tos,policy).OAuthApplicationPublicis that projection, derived withPick< … >from its parent so the two cannot drift. Itsredirect_urisis always[]on this route: the projection does not pass the stored URIs through and the serialiser defaults the missing value — the member carries no information here, and the JSDoc says so.user_idandapplication_typeare declared nullable by the vendor, but the serialiser folds a null column toundefined, sonullis unreachable on the wire and is not declared.Timestamps:
number. The ruling's ISO-8601 clause has no site in this family.The ruling ordered every
Date-typed field declared as an ISOstring, forbade aDatedeclaration, and forbade a revival layer. There is noDatefield here to convert. RFC 7591 carriesclient_id_issued_atandclient_secret_expires_atas Unix-epoch seconds, and the provider converts its storedDateto a number before serialising:So the wire sends neither a
Datenor an ISO string — it sends anumber, measured above as1788534576. Both are declarednumberand a type-level pin holds them there. The ruling's prohibitions are satisfied: nothing declares aDate, and no revival layer exists. Flagging this explicitly because it is the one place this card's outcome differs from what the ruling's wording anticipated — the policy is intact, it simply has no site to apply to.oauth.applications.deleteis NOT bound — an open question for the reviewerIts route answers HTTP 200 with a zero-byte body under
content-type: application/json(its handler returns nothing; the vendor declares itvoid). Driven through the real client:Every successful delete rejects — the row is already gone server-side by then. No declared return type can be honest while that
res.json()call stands, so binding it needs a behaviour change, which is a decision beyond the type-narrowing this family was scoped to. Its ledger entry therefore stays open, which is the shrink-only ledger working rather than being relaxed. The two readings, and what each costs, are in the dev report on #14312. I did not pick one.Verification
Final commit
c0dbf802c9, clean tree.pnpm --filter @objectstack/client check:exported-any-returns—✅ no NEW exported callable of @objectstack/client resolves to any: 317 callables reached (52 caller-supplied generics, not counted as erasure), 36 ledgered site(s) still open.Its--self-testalso reportsLedger is exact in both directions.automation.trigger(bound by fix(client): bind the five in-reporeturn res.json()methods erased toPromise< any >, and measure the 38 third-party ones #13082) is absent from the ledger and resolves toPromise< AutomationResult >in the builtdist, and the baseline run reported 40 open sites — so the instrument was known to see this world before its output was read.pnpm --filter @objectstack/client typecheck—tsc --noEmitclean, thencheck:test-typecheck: OK — 0 file(s) / 0 error(s). The pin file's presence in that program is proved bytsc -p tsconfig.test.json --listFiles, not assumed.pnpm --filter @objectstack/client test— 33 files, 431 tests, all passing.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 44 commands, 41 pass. The other three arePREREQUISITE NOT METrefusals that need a full-workspace build (check:skill-exampleswantspackages/client-react/dist;check:dual-build-cjs-loadsandcheck:type-check-debtwant the built closure) — not measured, and reported as such rather than as passes. The five roster gates whose baseline sits under a directory my paths are in were run explicitly and all pass....@objectstack/resttest sweep). Rather than hold the box, the check is narrowed and declared: CI builds the farm and runs all three regardless, and none of them reads a file this diff touches.git grepfinds zero call sites of the four bound methods anywhere outside the pin file — the other references are theAUTH_ROUTE_LEDGERrows (strings) and docs.Ablation — direction predicted first, then measured
Predicted: reverting
getPublic's annotation gives two independent reds. Both landed.The mutation was proved on disk before anything was read (removed-text count 0, injected-text count 1, source blob moved), then rebuilt and proved to have reached
dist/withablation-dist-preflight.mjs --absent. The first attempt was a no-op — a\Q…\Equoting bug madeperlmatch nothing and exit 0 — and the on-disk check is what caught it; it is recorded here rather than quietly re-run.check:exported-any-returnsexit 1:❌ 1 exported callable(s) … resolve to any and are not ledgered: ObjectStackClient.oauth.applications.getPublic resolves to Promise< any >check:test-typecheckexit 1:3 type error(s)inreturn-type-precision.test.ts— the equality pin plus the two now-unused@ts-expect-errordirectives (TS2578), which is what makes those suppressions evidence rather than decoration.Restored with
git checkout HEAD --naming the absolute path and proved by whole-treegit status --porcelainempty, blob hash equal to the HEAD blob (d38d5db31d…),git diff HEADempty, then rebuilt and the marker proved present again.Scope
Only the five methods this card names, on
packages/client/src/index.ts. Theauth.*andorganizations.*cards are serialized behind this one on the same hot file and are untouched. The #13080 BREAKING-token gate is not addressed here — the ruling says that card is independent and is not to be done in passing.Generated by Claude Code