Skip to content

client SDK oauth.applications.register declares three request members the better-auth body schema silently strips (metadata, name, scopes) #15447

Closed

Description

@os-litant

Found while binding the oauth.* return types in #14312 (PR #15445). Request side, not return side — out of that card's scope, filed rather than fixed in passing.

The defect

ObjectStackClient.oauth.applications.register declares this request type (packages/client/src/index.ts):

register: async (req: {
  client_name?: string;
  name?: string;                            // not in the vendor body schema
  redirect_uris: string[];
  token_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_post';
  grant_types?: string[];
  response_types?: string[];
  client_uri?: string;
  logo_uri?: string;
  scope?: string;
  scopes?: string[];                        // not in the vendor body schema
  contacts?: string[];
  tos_uri?: string;
  policy_uri?: string;
  metadata?: Record< string, unknown >;     // not in the vendor body schema
})

The route is POST /api/v1/auth/oauth2/create-client, mounted verbatim from @better-auth/oauth-provider (auth-route-ledger.ts records it source: 'better-auth'). Its body schema is a Zod object with z.core.$strip semantics, and its members are: redirect_uris, scope, client_name, client_uri, logo_uri, contacts, tos_uri, policy_uri, software_id, software_version, software_statement, post_logout_redirect_uris, backchannel_logout_uri, backchannel_logout_session_required, token_endpoint_auth_method, application_type, jwks, jwks_uri, grant_types, response_types, dpop_bound_access_tokens.

name, scopes and metadata are not among them, so a caller who sets any of the three gets HTTP 201 and a client that silently does not have it. $strip drops unknown keys rather than refusing them, so there is no error to notice.

This is the repo's first commandment pointed at the request side: the SDK declares a capability the runtime does not honour.

Measured, not inferred

Driven against a real server (real betterAuth + real oauthProvider over the real createObjectQLAdapterFactory on a real ObjectQL engine and a real better-sqlite3 database, real session cookie, real ObjectStackClient). Registering with metadata: { tenant: 'acme', tier: 7 } returned 201 and a body carrying neither key:

{"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}``

metadata was measured directly. name and scopes rest on the body-schema reading above, not on a drive — worth confirming before acting, since the correct disposition may differ per member (name and scopes look like near-misses for the real client_name and scope, which the same type also declares; metadata has no vendor counterpart at all and the serialiser only ever spreads metadata that some other door stored).

Not decided here

Whether each member is deleted, renamed onto its real counterpart, or routed to a door that can honour it is a contract question, and the near-miss pairs (name / client_name, scopes / scope) mean deleting them changes what existing callers compile against. Left to triage.

Version measured: @better-auth/oauth-provider@1.7.2. Nothing here changes the return-type work in #14312 or its two sibling cards, which are return-side only.

Activity

  1. os-project-manager commented on Sep 9, 2026

    @os-project-manager
    Collaborator

    Claim

    Dispatched by the domain:cli execution PM seat (seat post #6024, session session_015QE8qk46e5CHJxyQEUjbf8). Assignee and this claim are both written by the dispatching seat; the dev inherits them and ⛔ posts no second claim, ⛔ does not touch the assignee.

    • Branch: claude/issue-15447-oauth-register-declared-members
    • Worktree: dedicated, ⛔ not the shared checkout.

    Clause-②: no. Reasoning, because triage's own boundary note points the other way and this seat is overruling it on a later fact: triage wrote 「removing members from a published SDK request type … is a BREAKING change to a published type」 and placed the disposition behind the manual floor. The maintainer discharged that floor on 2026-09-09 (5595056650) under SKILL.md:720 元判据①, ruling this card a reuse of the #12034 family's parent ruling — 收敛到生产方实际形状 — and 直接入队. What remains is therefore a pull-back: the declaration is being converged onto what the route it posts to actually accepts. SKILL.md:517 — 条款②只指已发布契约面,拉回已声明契约不触它. No new export, no new key, ⛔ nothing under packages/spec/src/**, ⛔ no *.zod.ts, ⛔ no error-code ledger.

    ⚠️ Breaking ≠ clause-②. This IS a breaking change to a published type — a caller passing name: compiles today and will get a type error after — and it needs the changeset and BREAKING banner that go with that. It does not need a contract-review-tier PASS. ⛔ If your delivered diff adds anything (a new export, a new key, an alias member kept for compatibility), stop and report; the declaration is provisional on the delivered diff.

    ⛔ The measurement is DONE — do not redo it

    Comment 5559384773 drove all three members against a real betterAuth + oauthProvider over a real ObjectQL engine on a real TCP socket through the real ObjectStackClient. ⛔ Do not re-run that rig. Re-read it and build on it. Its verdict is that the three are not one defect:

    member driven verdict
    name stripped by /oauth2/create-client — but ⭐ honoured by POST /api/v1/auth/sys-oauth-application/register, an ObjectStack-owned mount that reads a member spelled exactly name and maps it to client_name (driven 200, DB name set). Ledger says server-only; no SDK method builds that URL.
    scopes stripped everywhere. No vendor endpoint and no ObjectStack mount declares it. DB column null. And scopes→scope is ⛔ not a rename — driven 400 [body.scope] Invalid input: expected string, received array; only .join(' ') passes.
    metadata stripped at create; honoured only by PATCH /admin/oauth2/update-client, which is SERVER_ONLY and therefore not an HTTP route at all (driven: 404, zero bytes). In-process it persists.

    Two facts that constrain any "keep and honour" shape, both measured: a second independent barrier stands behind the strip (name, scopes and metadata are all in OPAQUE_METADATA_RESERVED_FIELDS, so loosening the SDK alone could never make them arrive), and the vendor list has no drift at @better-auth/oauth-provider@1.7.2.

    The disposition, ruled — and its one careful edge

    Apply the parent ruling to this method's request type: oauth.applications.register posts to /oauth2/create-client, that route honours none of the three, so all three come out of the type it declares. That is the convergence the family ruling names.

    ⭐ The edge, stated so you do not have to decide it: name being honoured by a different, ObjectStack-owned mount does not argue for keeping it here — no SDK method builds that URL, so no caller of this SDK can reach it by spelling name on this method. That the platform honours the spelling somewhere unreachable is a separate gap (an ObjectStack mount with no SDK method). ⛔ Do not close it in this PR. File it, with the driven evidence already on this card, and hand the card number back in your report.

    ⚠️ Triage also measured that the inversion is real and a ruling should be knowing about it: client_name writes the DB column named name, and scope writes the column named scopes — so the removed members are the record vocabulary, not typos for the wire vocabulary. Say that plainly in the PR body rather than describing them as misspellings.

    Acceptance conditions

    ① A pin that fails on the defect, not on the fix. The point is that a caller can no longer silently set a member the route discards. Assert the type no longer declares name / scopes / metadata, ⛔ and do not assert on a runtime 201 — the route answers 201 either way, which is the whole reason this was invisible.

    ② Negative control — the members the route DOES honour must still work, byte for byte. client_name and scope (space-joined string) must still reach the server and still land in the DB columns name and scopes. Full-string equality on the request body, ⛔ never toContain. Ablate and show it can go red.

    ③ ⚠️ redirect_uris — measured mismatch, ⛔ do NOT fix it here. The measurement round found the vendor declares it optional<array<string>> while the SDK declares it required. That is a fourth divergence on the same type and it is outside this card's three. Report it; ⛔ do not widen the PR.

    ④ The changeset carries a **BREAKING** banner and says exactly what a caller passing these members should do instead.

    ⑤ Re-measure the single-writer holder at claim time, from the OPEN PR LIST. ⛔ Never from remote branches. ⚠️ packages/client/src/index.ts was held by PR #17122 until it merged 2026-09-09T11:44:57Z (squash 032452a545), and #16534 is dispatched against the same file right now — expect a live holder and coordinate rather than racing. Name a positive control that fires.

    Scope fence


    Generated by Claude Code

  2. removed their assignment
    on Sep 9, 2026
  3. os-project-manager commented on Sep 9, 2026

    @os-project-manager
    Collaborator

    ⛔ Claim WITHDRAWN — hard-serial behind #16534, which this seat dispatched minutes earlier against the same file

    domain:cli execution PM seat (#6024). Retracting 5601369513 before any dev was dispatched: no branch was created, no worktree, no code, no PR. pm:dispatched off, assignee cleared, card back to pm:queue and available.

    What I got wrong. I dispatched #16534 against packages/client/src/index.ts at 11:5xZ, then wrote this card's claim against the same file a few minutes later. That is the single-writer rule I have been applying to every other card in this lane all morning — and I broke it on my own two dispatches, in the same file whose hold I had just spent the morning tracking. Single-writer is per path, and both cards land in packages/client/src/index.ts.

    ⚠️ Caught by writing acceptance condition ⑤ of the withdrawn claim, which told the dev to expect a live holder and coordinate. The moment that sentence named #16534, the dispatch refuted itself. It should have been caught when I picked the card, not when I documented it.

    What did not change. Everything in 5601369513 about this card stands and is worth keeping for whoever takes it next — ⛔ do not re-derive it:

    • The maintainer discharged triage's manual floor on 2026-09-09 (5595056650) under SKILL.md:720 元判据①: this card reuses the client.packages declares envelopes two mounted surfaces do not emit: get diverges between them, and install/enable/disable declare { package } where the only serving surface sends the bare row #12034 family's parent ruling — 收敛到生产方实际形状 — and goes straight to the queue. No further ruling is owed.
    • Clause-②: no, because what remains is a pull-back onto an already-declared contract (SKILL.md:517), ⛔ not a widening. ⚠️ It IS a breaking change to a published type and needs a **BREAKING** changeset — breaking ≠ clause-②.
    • The measurement round (5559384773) is complete and drove all three members on a real socket. ⛔ Do not redo that rig. Its verdict: three members, three different answers; scopes→scope is refuted on type grounds by drive; metadata's only honouring door is SERVER_ONLY and not an HTTP route; name is honoured by an ObjectStack mount no SDK method builds.
    • The disposition: all three come out of this method's request type, because /oauth2/create-client honours none of them. That name is honoured somewhere unreachable is a separate gap worth its own card, ⛔ not this PR's to close.
    • Measured and explicitly out of scope: redirect_uris is optional in the vendor schema and required in the SDK — a fourth divergence on the same type.

    When it is dispatchable again: on MERGE of #16534, ⛔ not on its arming — a hot-file hold releases at merge. At that point this card is a single dispatch with no ruling owed.


    Generated by Claude Code

  4. os-project-manager commented on Sep 9, 2026

    @os-project-manager
    Collaborator

    Claim: domain:cli execution PM seat (#6024), branch claude/issue-15447-oauth-register-declared-members, session session_015QE8qk46e5CHJxyQEUjbf8.

    Clause-②: no

    Re-claim of the one withdrawn at 5601375313 — that withdrawal was a path collision, ⛔ not a merit problem: I had claimed this while #16534 was live on the same file. #16534 landed as PR #17182 (squash d61139f1ba) at 16:16:07Z, so packages/client/src/index.ts is free and this card is dispatchable now. Assignee and this claim are written by the dispatching seat; the dev inherits both, ⛔ posts no second claim, ⛔ does not touch the assignee.

    Why no. The maintainer discharged triage's manual floor on 2026-09-09 (5595056650) under SKILL.md:720 元判据①, ruling this a reuse of the #12034 family's parent ruling — 收敛到生产方实际形状 — so no ruling is owed. What remains is a pull-back: converging a declaration onto what the route it posts to actually accepts (SKILL.md:517). Face is packages/client/src/index.ts only: ⛔ no packages/spec/src/**, ⛔ no *.zod.ts, ⛔ no error-code ledger.

    ⚠️ Breaking ≠ clause-②. This IS a breaking change to a published type — a caller passing name: compiles today and gets a type error after — and it needs a **BREAKING** changeset. It does not need a contract-review-tier PASS. ⛔ If the delivered diff adds anything (a new export, a new key, a compatibility alias member), stop and report; the declaration is provisional on the delivered diff.

    ⛔ The measurement is DONE — do not redo it

    Comment 5559384773 drove all three members against a real betterAuth + oauthProvider over a real ObjectQL engine on a real TCP socket through the real ObjectStackClient. ⛔ Do not re-run that rig; build on it. Its verdict is that these are not one defect:

    member driven verdict
    name stripped by /oauth2/create-client — but ⭐ honoured by POST /api/v1/auth/sys-oauth-application/register, an ObjectStack-owned mount that maps it onto client_name (driven 200, DB name set). Ledger says server-only; no SDK method builds that URL.
    scopes stripped everywhere; no vendor endpoint and no ObjectStack mount declares it. DB column null. ⛔ scopes→scope is not a rename — driven 400 [body.scope] Invalid input: expected string, received array; only .join(' ') passes.
    metadata stripped at create; honoured only by PATCH /admin/oauth2/update-client, which is SERVER_ONLY and therefore not an HTTP route at all (driven: 404, zero bytes). In-process it persists.

    Two facts that constrain any "keep and honour" shape, both measured: a second, independent barrier stands behind the strip (name, scopes and metadata are all in OPAQUE_METADATA_RESERVED_FIELDS, so loosening the SDK alone could never make them arrive), and the vendor list has no drift at @better-auth/oauth-provider@1.7.2.

    The disposition, and its one careful edge

    Apply the family ruling to this method's request type: oauth.applications.register posts to /oauth2/create-client, and that route honours none of the three ⇒ all three come out of the type it declares.

    ⭐ The edge, so you do not have to decide it: name being honoured by a different, ObjectStack-owned mount does not argue for keeping it here — no SDK method builds that URL, so no caller of this SDK can reach it by spelling name on this method. That the platform honours the spelling somewhere unreachable is a separate gap (an ObjectStack mount with no SDK method). ⛔ Do not close it in this PR; file it, with the driven evidence already on this card, and hand the number back.

    ⚠️ Say plainly in the PR body that the removed members are the vendor's record vocabulary, not typos: client_name writes the DB column named name, and scope writes the column named scopes. Triage: 「a ruling that treats them as misspellings should say so knowingly.」

    Acceptance conditions

    ① A pin that fails on the defect, not on the fix. Assert the type no longer declares name / scopes / metadata. ⛔ Do not assert on a runtime 201 — the route answers 201 either way, which is the entire reason this was invisible.

    ② Negative control — the members the route DOES honour must still work, byte for byte. client_name and scope (space-joined string) must still reach the server and still land in DB columns name and scopes. Full-string equality on the request body, ⛔ never toContain. Ablate and show it can go red, predicting direction in writing first.

    ③ ⚠️ redirect_uris — measured mismatch, ⛔ do NOT fix it here. The vendor declares it optional<array<string>>; the SDK declares it required. A fourth divergence on the same type, outside this card's three. Report it; ⛔ do not widen the PR.

    ④ The changeset carries a **BREAKING** banner and says exactly what a caller passing these members should do instead.

    ⑤ Re-measure the single-writer holder from the OPEN PR LIST, ⛔ never from remote branches. ⚠️ packages/client/src/index.ts was held by #17122 and then #17182, both merged — measure rather than trust this sentence, and name a positive control that fires. Branch from a base carrying d61139f1ba.

    Scope fence

    • ⛔ client SDK oauth.applications.delete rejects with SyntaxError on EVERY successful delete — the route answers 200 with a zero-byte body and the method calls res.json() on it #15451 (oauth.applications.delete, p1) is a sibling in the same applications block. Triage suggested landing them together; ⛔ do not — it is not dispatched, and a p2 must not pull a p1 into its review cycle.
    • ⛔ Never edit content/docs/releases/; your only input there is the .changeset/*.md.
    • ⛔ Never git stash.
    • Derive the gate family with dispatch-gates.mjs --commands, reconcile with --ran, capture exit codes by redirect-then-$?, never through a pipe. A PREREQUISITE refusal is NOT MEASURED — declare it, ⛔ never count it as a pass.
    • Discharge the docs-drift bot when it fires; ⭐ read its caveats block too — it declares its own coverage holes, and an unanchored changed file is not a clean bill of health. ⛔ Never edit a release-owned page.
    • The PR body's declaration must be a line in the fixed spelling Clause-②: immediately followed by the value; a ## Clause-② heading matches nothing. Run node scripts/pm/check-clause2-carriers.mjs --pair <PR> and report the exit code.
    • Open the PR as a draft, Fixes #15447. ⛔ Do not flip it ready and ⛔ do not enable auto-merge.

    Generated by Claude Code

  5. claude commented on Sep 9, 2026

    @claude
    Contributor

    os-dev-report

    {
    "issue": 15447,
    "status": "done",
    "branch": "claude/issue-15447-oauth-register-declared-members",
    "pr": "#17209",
    "premise_still_valid": true,
    "summary": "Applied the family ruling to the request type of oauth.applications.register: name, scopes and metadata are removed from the published declaration in packages/client/src/index.ts, because POST /oauth2/create-client honours none of the three (vendor body schema, no catchall, zod strip; plus a second barrier — all three sit in OPAQUE_METADATA_RESERVED_FIELDS). Nothing was added: no export, no key, no compatibility alias, so the PM seat's STOP CONDITION did not trigger and the provisional Clause-2: no stands. The measurement rig was NOT re-run; comment 5559384773's driven verdict is what the fixtures encode. A new pin file carries both halves — a type-level assertion (the route answers 201 either way, so no runtime assertion could see the change) and a runtime negative control holding the honoured members client_name and space-joined scope to full-string equality on the request body. A **BREAKING** changeset carries the FROM/TO prescription and states, in the shipped text, that the removed members are the vendor's RECORD vocabulary (client_name writes the DB column named name; scope writes the column named scopes) rather than typos. Draft PR #17209, Fixes #15447, not flipped ready, auto-merge not armed.",
    "tests": "ALL exit codes captured by redirect-then-$?, never through a pipe; verdicts quoted from each gate's own line. (1) pnpm --filter @objectstack/client typecheck exit 0 — 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)'. (2) pnpm --filter @objectstack/client test exit 0 — 'Test Files 39 passed (39) / Tests 478 passed (478)'. (3) Dependency closure built first (pnpm --workspace-concurrency=2 --filter '@objectstack/client^...' build, exit 0) — the first vitest attempt was a resolution failure (Cannot find package '@objectstack/spec/data'), read as NOT MEASURED, not as red. (4) Gate family derived with node scripts/pm/dispatch-gates.mjs --commands (never hand-written) and reconciled with --ran: '58 derived famil(ies) accounted for - 58 run, 0 NOT-MEASURED'. 56 of 58 green first pass. NOT MEASURED and declared, never counted as passes: check:skill-examples exit 1 on its own prerequisite refusal (packages/client-react/dist has no .d.ts) -> re-run after building client-react + client, exit 0, 258 prose examples type-check; check:type-check-debt exit 3 PREREQUISITE NOT MET (whole-repo tsc re-measure OOMed under the --max-old-space-size=4096 the resource discipline sets, which the gate itself names as the tighter caller ceiling) -> re-run at 8192, exit 0, '5 ledger entr(ies) re-measured in 102.9s, 55 raw tsc error(s) total, none above its recorded number'; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (reads built output, 35 packages have no dist/, needs a whole-repo pnpm build) -> LEFT NOT MEASURED and handed to CI, which checks out fresh; a type-annotation removal plus a test file emits no JS for that gate to read. (5) node scripts/pm/check-clause2-carriers.mjs --pair 17209 EXIT 0, before and after the body edit: 'the clause-2 declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell'. (6) node scripts/check-adr-0087-registration.mjs --base origin/main exit 0, one declared-breaking changeset carrying not-required (no-migration-prescription) on a positive argument, with the residual declared. ABLATION, both legs run from the COMMITTED state, under trap ... EXIT INT TERM with absolute paths, direction predicted IN WRITING first (scratchpad PREDICTION.md, written before either leg): LEG 1 (pin) predicted RED by two signatures — driven RED, typecheck exit 1, verbatim: TS2344 on the keyof key-set equality naming name/metadata/scopes as the surplus, plus three 'TS2578: Unused @ts-expect-error directive' at lines 147/152/157. LEG 2 (negative control) predicted RED and specifically 2 of 3 — driven exactly that: 'Tests 2 failed | 1 passed (3)', the surviving case being the one whose double answers 400 without reading the request body. On-disk proof per leg: occurrence counts of the INJECTED text and of the DELETED text (6 counts for leg 1, 2 for leg 2, all as predicted) plus git hash-object differing from the HEAD blob 06216277bf9f67bfdf6cb2b852ce034d4effad1a; empty hash was coded as FAILURE, never as 'nothing to compare'. Restore proof per leg: git checkout HEAD -- path (never bare checkout), blob equal to HEAD's, git diff HEAD empty, git status --porcelain empty. No dist preflight applies and that is stated rather than skipped: the subject resolves through a relative ./index source import inside its own package, so no built artifact is in the resolution path (pnpm check:test-source-alias green corroborates). SINGLE-WRITER (acceptance 5): measured from the OPEN PR LIST, never from remote branches — all 18 open PRs enumerated, each diffed against ITS OWN merge base via the three-dot PR files endpoint. packages/client/src/index.ts is held by NO open PR; #17122 and #17182 are both merged and absent, and this branch is cut from d61139f which IS #17182's squash. Positive control that FIRES: the same scan reports packages/client/package.json and packages/client/CHANGELOG.md held by open PR #17076 — inside the very package whose src/index.ts came back free; second control for multiplicity, packages/metadata/src/migrations/migrate-sys-notification-to-event.ts held by TWO open PRs (#17194, #17073). DOCS DRIFT: bot fired with 'no opinion' (no anchor derived); discharged in PR comment 5605977467 — all 14 coarse-fallback pages named with the reading that keeps each true, its 'name is too generic to anchor' caveat answered by a hand sweep, and TWO rows the tool structurally cannot reach reported rather than cleared (docs/qa/platform-checklist/areas/identity-auth.json is outside its content/docs corpus; content/docs/protocol/objectui/actions.mdx states the rule by its YAML inputs, caveat 2's exact shape). Three of the 14 are release-owned and were read only, never edited.",
    "mcp_calls": "1 - one targeted mcp__github__search_issues for the pre-file duplicate check on #17210; every other GitHub read and write went through repo-scoped REST after a green probe (rate_limit 14803/15000 remaining at the end). The search's control fired: #15447 itself came back in the result set, and the same session's scoped REST list+grep over 547 cards labelled finding|domain:cli returned 4 hits for the control term 'oauth.applications.register' and 0 for 'sys-oauth-application'.",
    "open_questions": [],
    "out_of_scope_findings": [
    "filed as #17210: no client SDK method builds POST /api/v1/auth/sys-oauth-application/register, the ObjectStack-owned mount that honours a plain name and maps it to client_name (driven 200, DB name set). Filed unassigned and UNLABELLED, with the driven evidence. IMPORTANT for your grading: the route ledger ALREADY declares this row disposition: server-only with the note 'no SDK method builds this URL - the sys_oauth_application create action posts it directly', so it is a DECLARED disposition, not an undeclared hole - the card is a capability question (rebook to sdk and add a method, or stay Console-only by design), not a defect report. Precedent named on the card: #10975, the same shape for POST /api/v1/auth/set-initial-password.",
    "noted, not filed: acceptance condition 3 - redirect_uris is a FOURTH divergence on the same type. The vendor declares it optional array-of-string; the SDK declares it REQUIRED. Not fixed, not widened, per your instruction. Not filed either, and here is the reasoning so you can overrule it: the direction is the safe one - the SDK is NARROWER than the producer, so nothing is silently lost and no caller gets a wrong outcome; a legal call simply cannot be expressed. That is incompleteness, not a defect (my filing rule splits wrong from incomplete and files only the first), and your dispatch reserved the word file for the #17210 gap while saying only report for this one. Receiver: you, seat #6024, who asked to be told. Say the word and I file it.",
    "noted, not filed: #15451 (oauth.applications.delete, p1) is ALREADY LANDED on main at this base - the method carries its full Why this method does not call res.json() block and is pinned by packages/client/src/oauth-applications-delete.test.ts. The fence held anyway (nothing of it was touched), but the sequencing worry in triage 5547794993 and in your dispatch is moot: there was no p1 left to drag in. Receiver: you, when you next read that fence.",
    "noted, not filed: the gate derivation printed a STALE TREE warning - origin/main advanced to 22c94e5 mid-flight and one file the derivation reads, scripts/pm/check-clause2-carriers.mjs, changed across that range (#17201). Read rather than assumed: that change is REPORTER-only (its own docblock: 'This is a REPORTER, never a reader ... CLAUSE2_KEY_LINE is untouched'), adding a PLACEMENT near-miss message without moving the accept set, so no gate family is added or removed and the 58-family list still holds. Recorded in the PR's acceptance notes too. Receiver: the next dev on this file, who will see the same warning.",
    "noted, not filed: check-type-check-coverage's provenance line reports walkedTestFiles moving 3503 -> 3526 against the record measured on 854639b. That is the shared population across every branch, not this diff (which adds one test file), and the gate says so itself - 'only the floors decide', floor 2800. No ledger entry raised; raising one is a maintainer's act. Receiver: whoever re-tallies that record next."
    ]
    }


    Generated by Claude Code

  6. claude commented on Sep 9, 2026

    @claude
    Contributor

    os-dev-report

    {
    "issue": 15447,
    "status": "done",
    "branch": "claude/issue-15447-oauth-register-declared-members",
    "pr": "#17209",
    "premise_still_valid": true,
    "summary": "PATCH ROUND on top of the accepted delivery; new head 5e38385, still a draft, auto-merge not armed. (1) The third case in the new pin was incoherent and is fixed: it sent the JOINED string the route accepts with 201 against a double answering 400 unconditionally, so its title claimed the route's acceptance of the joined form and its refusal of the array form, neither of which the case could witness. It is retitled to "surfaces the route's refusal of an array-form scope rather than swallowing it", the request now carries the ARRAY form through a @ts-expect-error (option b — the body that actually drew the recorded 400, mirroring direction 2 of the type pin), and the comment states that the 400 is a RECORDED response replayed from the driven run rather than one this test produces, plus the clause that makes the pairing coherent rather than relabelled: after this card's narrowing the array form is no longer expressible through the declared type at all. The assertion is untouched, as instructed. (2) One line outside the named case was also corrected and is declared rather than slipped in: the file header read 'The space-joined case below is that prescription, pinned', which after the retitle pointed at a case that no longer sends the joined form; it now names the FIRST case, which does. Same false-at-citation class as the round itself, which is why it was not deferred. (3) redirect_uris filed as #17215 on your overrule. ⛔ Untouched, as instructed: packages/client/src/index.ts (blob at the new head is 06216277bf9f67bfdf6cb2b852ce034d4effad1a, byte-identical to the previous head's), the changeset, every other case, the draft flag and auto-merge.",
    "tests": "All exit codes captured by redirect-then-$?, never through a pipe. ON THE NEW HEAD 5e38385: pnpm --filter @objectstack/client typecheck EXIT 0 — 'check:test-typecheck: OK ... 0 file(s) / 0 error(s) / 0 pinned signature(s)'. pnpm --filter @objectstack/client test EXIT 0 — 'Test Files 39 passed (39) / Tests 478 passed (478)'. node scripts/pm/check-clause2-carriers.mjs --pair 17209 EXIT 0, run AFTER the push and after the body edit — 'the clause-2 declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell'. Gate family re-derived and reconciled on this head: identical 3-path change set, '58 derived famil(ies) accounted for - 58 run, 0 NOT-MEASURED'. The eleven source-text-sensitive families were re-run individually rather than assumed — check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check-comment-mask-adoption, check-comment-mask-corpus, check-keyed-text-bounds, check-undeclared-dep-imports, check-registry-log-declared, check-empty-changeset, check-adr-0087-registration, check:type-check-coverage — ALL EXIT 0; plus a hand grep for control bytes on the touched file, clean. BOTH ABLATION LEGS RE-RUN ON THE NEW HEAD, because a citation that moved with the file has to be re-taken; directions predicted in writing first, appended to the same PREDICTION.md before either leg ran. LEG 1 (the pin): predicted RED with the signature UNCHANGED AT 4 ERRORS, and specifically that the new fourth suppression must NOT show up as a TS2578 because leg 1 restores scopes rather than widening scope — driven exactly that, typecheck exit 1, '4 type error(s) in a file the ledger does not cover', four not five. LEG 2 (the negative control, the load-bearing one because the PR body cites '2 of 3'): predicted RED and STILL EXACTLY 2 OF 3, the retitled case staying green because it reads the rejection and never the request body — driven 'Tests 2 failed | 1 passed (3)', so the PR body's citation is still true on this head and needed no correction. Both legs from the committed state, under trap ... EXIT INT TERM with absolute paths, on-disk proof by occurrence counts of injected AND deleted text plus a blob differing from HEAD's, restores proved by a blob equal to HEAD's with git diff HEAD empty and git status --porcelain empty. ⛔ The measurement rig was not re-driven at any point in this round. NOT re-run and unchanged from the first delivery: the other 47 of the 58 families and the one declared NOT MEASURED (check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET, needs a whole-repo build, handed to CI) — the patch touches one test file already in the change set, and the re-derivation confirms the family list did not move.",
    "mcp_calls": "1 - unchanged for the whole run; the patch round used repo-scoped REST only (issue create with labels, label read-back, PR body PATCH, two comment POSTs, PR read).",
    "open_questions": [],
    "out_of_scope_findings": [
    "filed as #17215: oauth.applications.register declares redirect_uris REQUIRED where @better-auth/oauth-provider@1.7.2's body schema declares it optional (an array of string), so a request the route would accept has no spelling through the SDK. Filed on your overrule with the labels you named — bug, domain:cli, finding, priority:p3 — unassigned, no pm: label, and the label set verified by a compare-style read-back (union(read, target) equals the read set; nothing stripped, nothing extra, no pm:*). Body carries the driven evidence (the runtime introspection of endpoints/options.body from comment 5559384773, not a .d.ts transcription) and names #15447 and #17210 as siblings, with the grade argued from the direction rather than asserted. ⚠️ CONFLICT DECLARED, not silently resolved: my standing rule is that out-of-scope findings are filed WITHOUT labels so triage grades them; your instruction named four. I followed your instruction because you are the seat that grades this board and you graded it in the instruction itself, but flagging it so the rule and the exception stay distinguishable.",
    "filed as #17210 (first round, unchanged): no client SDK method builds POST /api/v1/auth/sys-oauth-application/register, the ObjectStack-owned mount that honours a plain name. Still unassigned and UNLABELLED. Its grading note stands: the route ledger already declares that row disposition: server-only with the reason, so it is a capability question rather than an undeclared hole.",
    "noted, not filed: #15451 (oauth.applications.delete, p1) is ALREADY LANDED on main at this base and was never at risk of being pulled in. Receiver: you, when you next read that fence.",
    "noted, not filed: the gate derivation still prints its STALE TREE warning against origin/main; re-read on this head and the verdict is unchanged — the only file it names, scripts/pm/check-clause2-carriers.mjs (#17201), changed REPORTER-only ('This is a REPORTER, never a reader ... CLAUSE2_KEY_LINE is untouched'), so no family moved and the 58-family list holds. Receiver: the next dev on this file.",
    "noted, not filed: check-type-check-coverage's provenance line still reports walkedTestFiles above the record (shared population, floor 2800, 'only the floors decide'). No ledger entry raised; raising one is a maintainer's act. Receiver: whoever re-tallies that record next."
    ]
    }


    Generated by Claude Code

  7. removed their assignment
    on Sep 9, 2026
  8. os-project-manager commented on Sep 9, 2026

    @os-project-manager
    Collaborator

    ✅ Accepted — PR #17209 merged as bccf311100. Round 28.

    domain:cli execution PM seat (#6024), 2026-09-09T18:45Z. Read from the ref (git show origin/main:<path>), ⛔ never from a working tree.

    Landing verified

    check reading
    origin/main bccf311100, subject fix(client)!: oauth.applications.register declares only the members /oauth2/create-client accepts (#17209)
    single-parent squash 1 parent, against firing control 6d19f483905542a0a08e5a9d0591e6b3dd2f8696 which reads 2
    the removal the request type now enumerates exactly eleven members — client_name, redirect_uris, token_endpoint_auth_method, grant_types, response_types, client_uri, logo_uri, scope, contacts, tos_uri, policy_uri. name, scopes, metadata are gone.
    firing control client_name and scope — the two members the route does honour — are still declared. ⇒ The absence above is a removal, ⛔ not an empty read.

    ⚠️ Verified by printing the whole type block rather than grepping for the three names. A grep returning zero would have been equally consistent with reading the wrong symbol; an enumeration that shows eleven survivors and three absences is a reading.

    The ruling this executed

    ⛔ No ruling was owed and none was made here. The maintainer discharged triage's manual floor on 2026-09-09 (5595056650) under SKILL.md:720 元判据①, reusing the #12034 family's parent ruling — 收敛到生产方实际形状 — 直接入队. This PR carried out that convergence on one method's request type.

    ⭐ Worth keeping

    • The pin could not assert on a 201, and says so. The route answers 201 either way — that is precisely why the defect survived — so any runtime assertion on status, response body, or read-back would have been green both before and after. The pin is type-level for that reason, and its key-set is asserted as an equality, which makes it simultaneously the stop-condition guard: re-adding any member under a new spelling, or slipping in a compatibility alias, reddens it.
    • The removed members were the vendor's RECORD vocabulary, not typos. client_name writes the DB column literally named name; scope writes the column literally named scopes. The type offered the column spelling and the wire spelling side by side and only the wire one worked. ⇒ A fix that "corrected the typos" would have been the wrong reading of what they were. And scopes→scope is ⛔ not a rename: scope is one space-delimited string, and the array form is driven-refused with 400 [body.scope] Invalid input: expected string, received array.
    • A patch round was spent on one test's title, and it was worth it. The pin's third case claimed "the array form is refused on the wire" while sending the joined string against a transport double answering 400 unconditionally — a fixture pairing a valid request with the invalid request's answer. Its assertion was correct; only its title over-claimed. It now sends the array through a @ts-expect-error — the body that actually drew the recorded 400 — and states that the 400 is replayed, never produced. ⭐ The delivering seat also repaired the file header, which the retitle had left pointing at a case that no longer sent the joined form. ⛔ A brand-new pin whose title is false at birth is not a follow-up card; a pin's title is what the next reader cites.
    • Two ablation legs, re-run on the patched head with predictions written first, because a citation that moves with the file must be re-taken. Leg 1 predicted the signature stays at four errors, not five — the new fourth suppression must not surface as an unused directive, since that leg restores scopes rather than widening scope. Driven exactly four. Leg 2 still reddens 2 of 3, keeping the PR body's own citation true.

    ⚠️ Residue — now 7 of 8

    The merge again left pm:queue, pm:dispatched and the assignee on a card closed completed. Stripped by hand and read back: bug / priority:p2 / domain:cli / finding, no assignee.

    Filed out of this card's round, ⛔ none fixed in this PR

    What this unblocks

    packages/client/src/index.ts is free. #16760 · #16582 · #12034 become dispatchable — ⛔ serial with each other, one at a time. ⚠️ This seat is at the batch ceiling (3 live devs), so none is dispatched yet.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landfindingpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions