Skip to content

Commit cf3a5dd

Browse files
committed
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 #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
1 parent c895b12 commit cf3a5dd

3 files changed

Lines changed: 199 additions & 15 deletions

File tree

.changeset/client-oauth-delete-zero-byte-200.md

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,7 @@ fix(client)!: `oauth.applications.delete` resolves on the zero-byte 200 its rout
66

77
**BREAKING** on two independent axes, and it makes a published method usable for the first time. Before this change `client.oauth.applications.delete(id)` **rejected on every successful delete** — there was no success path a caller could observe. It ships as `minor` under the lockstep launch-window convention (`scripts/check-changeset-no-major.mjs`); the version number is not the migration signal here, this entry is.
88

9-
<!-- ADR-0087 disposition: BLOCKED ON A DECISION, and deliberately not claimed. This is
10-
NOT a disposition marker and must not be read as one; the gate is expected to red on
11-
this changeset until a maintainer settles which category applies. Measured, both legs:
12-
(1) `type-surface-only packages/client/src/index.ts#delete` is REFUSED at predicate 4
13-
-- a reference is a bare identifier resolved to the FIRST same-named definition, and
14-
this file declares TEN members named `delete`; the first (line 2397) is unannotated at
15-
both revs, so the gate reads "still UNANNOTATED" about a member this diff never
16-
touched. That is issue #15627, filed off PR #15445 where the same ambiguity cost the
17-
`get` member its place in the marker -- here it blocks the ONLY member there is.
18-
(2) `no-migration-prescription` is mechanically ACCEPTED (the detector finds no
19-
prescription in this body) and is not claimed for that reason: ADR-0087's own D7
20-
records that #8277 held this exemption on a detector MISS rather than a positive
21-
finding, and names that as the pattern the sixth category was created to stop.
22-
Claiming it here, with the measurement in hand, would repeat it knowingly.
23-
Neighbouring open card: #14502. Full analysis in this PR's body. -->
9+
<!-- adr-0087: registered client-oauth-applications-delete-void -->
2410

2511
The fifth and last method of the `oauth.*` family, and the one #14312 / PR #15445 deliberately could not close: its ruling fenced that card to *narrowing published return types*, and no declared return type could be true while the `res.json()` call stood.
2612

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// Anchors for this entry, kept in source rather than in the strings below: the
4+
// entry's prose is projected into `packages/spec/spec-changes.json` and
5+
// `docs/protocol-upgrade-guide.md`, which are read by consumers who cannot
6+
// resolve this repo's internal issue numbers.
7+
//
8+
// card objectstack-ai/objectstack#15451
9+
// landing PR objectstack-ai/objectstack#15675
10+
// disposition objectstack-ai/objectstack#15674 (ruled D, 2026-09-05: this
11+
// class routes through ADR-0087 `registered`)
12+
// precedents objectstack-ai/objectstack#13023, #13079 (the three sibling
13+
// entries this is shaped on: `client-delete-result-success`,
14+
// `client-meta-reset-result-reset`,
15+
// `client-envelope-convergence-analytics-automation`)
16+
// family objectstack-ai/objectstack#14312 (the `oauth.*` binding card
17+
// whose ruling fenced this method out, PR #15445)
18+
// pins `packages/client/src/oauth-applications-delete.test.ts`,
19+
// `packages/client/src/return-type-precision.test.ts`
20+
// vendor `StrictEndpoint<'/oauth2/delete-client', ..., void>` in
21+
// `@better-auth/oauth-provider`
22+
23+
import type { SemanticMigration } from '../../types.js';
24+
25+
export const entry: SemanticMigration = {
26+
id: 'client-oauth-applications-delete-void',
27+
surface:
28+
'client.oauth.applications.delete(clientId) — both halves of what a caller of this '
29+
+ 'published `@objectstack/client` method observes: the DECLARED return, '
30+
+ '`Promise<any>` before and `Promise<void>` after, and the SETTLE BEHAVIOUR, which '
31+
+ 'rejected with `SyntaxError: Unexpected end of JSON input` on every successful '
32+
+ 'delete before and resolves after',
33+
replacement:
34+
'no value — `void`. There is nothing to move a read TO, because the promise never '
35+
+ 'resolved for a caller to read anything off it. The migration is on the settle '
36+
+ 'path instead: `try { await client.oauth.applications.delete(id); } catch { '
37+
+ '/* it probably worked */ }` → drop the workaround, the `catch` was executing on '
38+
+ 'EVERY successful delete and now executes only on a real failure. A read off the '
39+
+ 'resolved value — `(await client.oauth.applications.delete(id)).deleted` — was '
40+
+ 'unreachable code that has never executed and now stops compiling (TS2339). Same '
41+
+ 'call, same request, same wire body',
42+
reason:
43+
'The route answers HTTP 200 with a ZERO-BYTE body: `POST {auth}/oauth2/delete-client` '
44+
+ 'returns nothing from its handler, the vendor declares the endpoint `void`, and the '
45+
+ 'response carries `content-type: application/json` with NO `content-length` header '
46+
+ 'at all. The method ended `return res.json()`, so it rejected `SyntaxError: '
47+
+ 'Unexpected end of JSON input` on every successful delete — after the row had '
48+
+ 'already been removed server-side. There was no success path a caller could '
49+
+ 'observe, and the obvious recovery made it worse: the retry failed DIFFERENTLY, '
50+
+ 'with the route\'s 404 `not_found`, because the client was already gone. The method '
51+
+ 'now reads the body as text, returns on the empty case, and still parses (and still '
52+
+ 'throws on) a non-empty one — so the ONLY behaviour that moved is the zero-byte '
53+
+ 'case, which is the defect itself. THE WIRE IS BYTE-IDENTICAL: same route, same '
54+
+ 'request body, same status codes, same ADR-0112 error envelope; no Zod schema and '
55+
+ 'no `packages/spec` declaration moves, no authorable key and no stored '
56+
+ 'representation is involved, so a raw-HTTP caller is unaffected and '
57+
+ '`objectstack migrate meta` has nothing to rewrite. This is registered rather than '
58+
+ 'exempted because the change is NOT compiler-delivered where it matters, and the '
59+
+ 'gap is exact rather than theoretical. The change has two halves and only one of '
60+
+ 'them has a diagnostic. (1) The declared return moves from a ledgered `any` to '
61+
+ '`void`, so a typed caller that read a property off the resolved value now gets '
62+
+ '`error TS2339` — but that read was UNREACHABLE, since the promise never resolved, '
63+
+ 'so the compiler names only code that has never run. (2) The half that DID run on '
64+
+ 'every call — a `try`/`catch` wrapped around the delete — compiles identically '
65+
+ 'before and after, with no diagnostic anywhere, while its `catch` block stops '
66+
+ 'executing. So for the only behaviour that was ever observable, `tsc` names ZERO '
67+
+ 'sites; and for an untyped JS caller there is no constrained channel at all. That '
68+
+ 'is why the ledger entry is the only notification that reaches an upgrader — the '
69+
+ 'same argument the three sibling entries on this package make '
70+
+ '(`client-delete-result-success`, `client-meta-reset-result-reset`, '
71+
+ '`client-envelope-convergence-analytics-automation`). ⚠️ Note the DIRECTION, which '
72+
+ 'is the inverse of the usual break: this does not stop working code from working, '
73+
+ 'it makes a method that could never succeed succeed. The hazard is therefore '
74+
+ 'inverted too — code written to survive a permanent failure is now inert, and any '
75+
+ 'alerting or error budget fed by this method\'s rejections goes quiet. ⛔ Do not '
76+
+ 'keep the old behaviour behind a flag or a wrapper that re-throws: there is one '
77+
+ 'producer shape, and the rejection was never a contract, it was a parse of an empty '
78+
+ 'string. ⛔ Do not synthesise `{ deleted: true }` either: the 200 carries zero bytes '
79+
+ 'and therefore zero information, and "it was already gone" is distinguished on the '
80+
+ 'ERROR channel — a client that is not there answers 404 `{ error: \'not_found\' }`, '
81+
+ 'which `ObjectStackClient.fetch` raises as a throw before the body reader runs — so '
82+
+ 'a synthesised success value would be a shape the wire never sends and strictly '
83+
+ 'less informative than the 404 the caller already receives. ADR-0087 D3.',
84+
acceptanceCriteria:
85+
'⚠️ The real work is behavioural and NOTHING will report it: every `try`/`catch` '
86+
+ 'wrapped around `client.oauth.applications.delete()` has to be re-read one by one, '
87+
+ 'because it compiles identically before and after while its `catch` block goes from '
88+
+ 'running on every successful delete to running only on a real failure. Anything '
89+
+ 'that block did — treating the delete as failed, retrying it (the retry answered '
90+
+ '404 `not_found`, which may itself have been swallowed), skipping post-delete '
91+
+ 'cleanup, cache invalidation, audit writes or a UI refresh, or reporting the delete '
92+
+ 'to a user as failed — is now on the other branch, and the cleanup paths that were '
93+
+ 'skipped run for the first time. Verify that is what you want rather than assuming '
94+
+ 'it restores prior behaviour. Alerting, error budgets and dashboards fed by '
95+
+ '`SyntaxError` rejections from this method drop to zero: that is the fix landing, '
96+
+ 'not an outage. Any test that passed while asserting this call rejects on a '
97+
+ 'successful delete was asserting on the defect and needs rewriting, not renaming. '
98+
+ 'On the type side, no code reads a property off the resolved value; `tsc` names '
99+
+ 'those sites for a typed caller (TS2339), but every one of them was unreachable, so '
100+
+ 'a clean type-check is NOT evidence that the sweep above was done. An untyped JS '
101+
+ 'caller gets no report at all. Nothing about the request, the route, the status '
102+
+ 'codes or the thrown error shapes changes and no server needs upgrading — the '
103+
+ 'server has always answered this way; only the client stopped mis-reading it. '
104+
+ 'Populations, measured at this landing: in the ObjectStack repo, ZERO production '
105+
+ 'call sites — the only references are the pins that ship with this change '
106+
+ '(`oauth-applications-delete.test.ts`, `return-type-precision.test.ts`); in '
107+
+ 'objectui at the pinned `.objectui-sha`, ZERO — neither `oauth.applications` nor '
108+
+ '`delete-client` appears anywhere in that tree; `objectstack-ai/cloud` is NOT '
109+
+ 'MEASURED, and a `catch` there that swallowed this method\'s rejection is now dead '
110+
+ 'code that this entry is the only notice of.',
111+
};

packages/spec/src/migrations/registry.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6151,6 +6151,93 @@ const step18: MigrationStep = {
61516151
+ 'that passed while asserting on `deleted` was asserting on `undefined` and needs '
61526152
+ 'rewriting, not renaming.',
61536153
},
6154+
{
6155+
id: 'client-oauth-applications-delete-void',
6156+
surface:
6157+
'client.oauth.applications.delete(clientId) — both halves of what a caller of this '
6158+
+ 'published `@objectstack/client` method observes: the DECLARED return, '
6159+
+ '`Promise<any>` before and `Promise<void>` after, and the SETTLE BEHAVIOUR, which '
6160+
+ 'rejected with `SyntaxError: Unexpected end of JSON input` on every successful '
6161+
+ 'delete before and resolves after',
6162+
replacement:
6163+
'no value — `void`. There is nothing to move a read TO, because the promise never '
6164+
+ 'resolved for a caller to read anything off it. The migration is on the settle '
6165+
+ 'path instead: `try { await client.oauth.applications.delete(id); } catch { '
6166+
+ '/* it probably worked */ }` → drop the workaround, the `catch` was executing on '
6167+
+ 'EVERY successful delete and now executes only on a real failure. A read off the '
6168+
+ 'resolved value — `(await client.oauth.applications.delete(id)).deleted` — was '
6169+
+ 'unreachable code that has never executed and now stops compiling (TS2339). Same '
6170+
+ 'call, same request, same wire body',
6171+
reason:
6172+
'The route answers HTTP 200 with a ZERO-BYTE body: `POST {auth}/oauth2/delete-client` '
6173+
+ 'returns nothing from its handler, the vendor declares the endpoint `void`, and the '
6174+
+ 'response carries `content-type: application/json` with NO `content-length` header '
6175+
+ 'at all. The method ended `return res.json()`, so it rejected `SyntaxError: '
6176+
+ 'Unexpected end of JSON input` on every successful delete — after the row had '
6177+
+ 'already been removed server-side. There was no success path a caller could '
6178+
+ 'observe, and the obvious recovery made it worse: the retry failed DIFFERENTLY, '
6179+
+ 'with the route\'s 404 `not_found`, because the client was already gone. The method '
6180+
+ 'now reads the body as text, returns on the empty case, and still parses (and still '
6181+
+ 'throws on) a non-empty one — so the ONLY behaviour that moved is the zero-byte '
6182+
+ 'case, which is the defect itself. THE WIRE IS BYTE-IDENTICAL: same route, same '
6183+
+ 'request body, same status codes, same ADR-0112 error envelope; no Zod schema and '
6184+
+ 'no `packages/spec` declaration moves, no authorable key and no stored '
6185+
+ 'representation is involved, so a raw-HTTP caller is unaffected and '
6186+
+ '`objectstack migrate meta` has nothing to rewrite. This is registered rather than '
6187+
+ 'exempted because the change is NOT compiler-delivered where it matters, and the '
6188+
+ 'gap is exact rather than theoretical. The change has two halves and only one of '
6189+
+ 'them has a diagnostic. (1) The declared return moves from a ledgered `any` to '
6190+
+ '`void`, so a typed caller that read a property off the resolved value now gets '
6191+
+ '`error TS2339` — but that read was UNREACHABLE, since the promise never resolved, '
6192+
+ 'so the compiler names only code that has never run. (2) The half that DID run on '
6193+
+ 'every call — a `try`/`catch` wrapped around the delete — compiles identically '
6194+
+ 'before and after, with no diagnostic anywhere, while its `catch` block stops '
6195+
+ 'executing. So for the only behaviour that was ever observable, `tsc` names ZERO '
6196+
+ 'sites; and for an untyped JS caller there is no constrained channel at all. That '
6197+
+ 'is why the ledger entry is the only notification that reaches an upgrader — the '
6198+
+ 'same argument the three sibling entries on this package make '
6199+
+ '(`client-delete-result-success`, `client-meta-reset-result-reset`, '
6200+
+ '`client-envelope-convergence-analytics-automation`). ⚠️ Note the DIRECTION, which '
6201+
+ 'is the inverse of the usual break: this does not stop working code from working, '
6202+
+ 'it makes a method that could never succeed succeed. The hazard is therefore '
6203+
+ 'inverted too — code written to survive a permanent failure is now inert, and any '
6204+
+ 'alerting or error budget fed by this method\'s rejections goes quiet. ⛔ Do not '
6205+
+ 'keep the old behaviour behind a flag or a wrapper that re-throws: there is one '
6206+
+ 'producer shape, and the rejection was never a contract, it was a parse of an empty '
6207+
+ 'string. ⛔ Do not synthesise `{ deleted: true }` either: the 200 carries zero bytes '
6208+
+ 'and therefore zero information, and "it was already gone" is distinguished on the '
6209+
+ 'ERROR channel — a client that is not there answers 404 `{ error: \'not_found\' }`, '
6210+
+ 'which `ObjectStackClient.fetch` raises as a throw before the body reader runs — so '
6211+
+ 'a synthesised success value would be a shape the wire never sends and strictly '
6212+
+ 'less informative than the 404 the caller already receives. ADR-0087 D3.',
6213+
acceptanceCriteria:
6214+
'⚠️ The real work is behavioural and NOTHING will report it: every `try`/`catch` '
6215+
+ 'wrapped around `client.oauth.applications.delete()` has to be re-read one by one, '
6216+
+ 'because it compiles identically before and after while its `catch` block goes from '
6217+
+ 'running on every successful delete to running only on a real failure. Anything '
6218+
+ 'that block did — treating the delete as failed, retrying it (the retry answered '
6219+
+ '404 `not_found`, which may itself have been swallowed), skipping post-delete '
6220+
+ 'cleanup, cache invalidation, audit writes or a UI refresh, or reporting the delete '
6221+
+ 'to a user as failed — is now on the other branch, and the cleanup paths that were '
6222+
+ 'skipped run for the first time. Verify that is what you want rather than assuming '
6223+
+ 'it restores prior behaviour. Alerting, error budgets and dashboards fed by '
6224+
+ '`SyntaxError` rejections from this method drop to zero: that is the fix landing, '
6225+
+ 'not an outage. Any test that passed while asserting this call rejects on a '
6226+
+ 'successful delete was asserting on the defect and needs rewriting, not renaming. '
6227+
+ 'On the type side, no code reads a property off the resolved value; `tsc` names '
6228+
+ 'those sites for a typed caller (TS2339), but every one of them was unreachable, so '
6229+
+ 'a clean type-check is NOT evidence that the sweep above was done. An untyped JS '
6230+
+ 'caller gets no report at all. Nothing about the request, the route, the status '
6231+
+ 'codes or the thrown error shapes changes and no server needs upgrading — the '
6232+
+ 'server has always answered this way; only the client stopped mis-reading it. '
6233+
+ 'Populations, measured at this landing: in the ObjectStack repo, ZERO production '
6234+
+ 'call sites — the only references are the pins that ship with this change '
6235+
+ '(`oauth-applications-delete.test.ts`, `return-type-precision.test.ts`); in '
6236+
+ 'objectui at the pinned `.objectui-sha`, ZERO — neither `oauth.applications` nor '
6237+
+ '`delete-client` appears anywhere in that tree; `objectstack-ai/cloud` is NOT '
6238+
+ 'MEASURED, and a `catch` there that swallowed this method\'s rejection is now dead '
6239+
+ 'code that this entry is the only notice of.',
6240+
},
61546241
{
61556242
id: 'cluster-driver-dangling-values-removed',
61566243
surface: 'kernel.cluster.driver (ClusterDriverSchema, kernel/cluster.zod.ts) '

0 commit comments

Comments
 (0)