Skip to content

docs(qa): re-point identity-auth's get-session clauses at the 401 refusal envelope, and pin them to it - #18742

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-18650-identity-auth-checklist-retired-200-null
Sep 17, 2026
Merged

os-support-ai merged 4 commits into
mainfrom
claude/issue-18650-identity-auth-checklist-retired-200-null

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #18650

docs/qa/platform-checklist/areas/identity-auth.json still taught better-auth's
retired no-session convention — HTTP 200 with a JSON null body — and
instructed a runner accordingly. Since #17881 landed refuseAnonymousSession,
the live answer is 401 + UNAUTHENTICATED in the ADR-0112 refusal envelope, so
the file asserted the opposite of the truth: it said a 401 expectation
"misdescribes a correct implementation", when an implementation answering 401
is today the correct one. A runner following it recorded correct behaviour as a
defect, and the remedy its negative pointed at was undoing an auth tightening.

Clause-②: no

The two premises, measured here rather than relayed

① The count. Re-derived on today's origin/main by a structural walk of
every docs/qa/platform-checklist/areas/*.json (by JSON node, not by line), with
controls in both directions: positive get-session = 35 occurrences across the
area files, negative nonsense token = 0. The result is five sites, all in
identity-auth.json, all inside one item:

node path field
items[4].steps[7] the runner instruction
items[4].acceptance[4].verify clause 5's oracle
items[4].negative[2] the negative
items[4].source[6] the evidence row
items[4].history[2].change revision 3

Five is what the card said and five is what the sweep found — reported because it
was measured, not because it was expected. The card's declared-unswept question
is answered in the same pass: 0 other area files carry the convention, and
grep over RUNNER.md / README.md / SWEEP.md / FOLLOW-UPS.md finds none
either, so nothing else needs filing.

Deliberately not counted among the five: get-session statements at other
sites that describe behaviour without teaching the convention (a post-sign-out
read "no longer returns the user"; a pre-2FA read "does not return an
authenticated user"; the post-remove avatar read). Those stay true under both
wire answers.

② The behaviour. Driven on this checkout through a real AuthManager over
the in-memory engine this package's better-auth suites use, because the card's
first declared act was to drive the revoked path specifically — #17881 is
described in terms of an anonymous caller and nobody had checked they were the
same door:

sign-up                          -> 200  cookie issued
GET /get-session   (live)        -> 200  {"user":{...},"session":{...}}
POST /revoke-sessions            -> 200  {"status":true}
GET /get-session   (revoked)     -> 401  {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Sign in first"}}
GET /get-session   (fresh mgr)   -> 200      [control, on its own engine]

They are the same door. refuseAnonymousSession keys on the answer shape —
the /get-session path, status 200, and a body that is exactly the literal
null — never on how the caller became anonymous, so "never signed in",
"unknown cookie" and "revoked session" all convert. The control on a separate
manager rules out the revoke having poisoned the harness. The probe was a
throwaway; it is not in this diff.

What changed

Four instructional sites re-pointed at the live answer. The substantive
advice is kept and re-founded rather than deleted: get-session is still not
this clause's oracle, but no longer because its status cannot discriminate (it
now can) — because the clause's contract is an immediate kill on a PROTECTED
request, and one auth-route seam's status is not proof the authorization layer
refuses the target's in-flight protected reads. That reason does not move with
the wire.

The negative was the most dangerous of the five and is now explicit in both
directions: a 401 after the revoke is the correct answer and must never be
filed as the defect, while a 200 carrying a user after the revoke is no longer
the benign convention this line once described — it is the live-session answer,
so it IS a real signal the kill did not land.

revision 3 is left standing, unedited. It was right when it was written;
its cited authority (session-of-record.test.ts) was inverted underneath it
afterwards and now pins the very 401 it was cited for denying. Rewriting or
deleting it would erase what the August judgement rested on, which is the whole
point of a revision history. A new revision 6 says so, and the item's
revision field moves 5 to 6 (check:platform-checklist holds those equal).

The pin, and the proof it can go red

packages/plugins/plugin-auth/src/checklist-refusal-envelope-consistency.test.ts
holds the checklist equal to the runtime's refusal envelope. The status comes
from ANONYMOUS_SESSION_REFUSAL_STATUS and the code is derived from it
through ADR-0112's own status map — neither is spelled a second time, so the pin
cannot agree with a runtime that has moved.

It lives in plugin-auth on purpose. The next behaviour change is a diff in that
package, which puts it in the affected set; check:platform-checklist is
deliberately not a per-PR gate (its own header records that ruling), so a pin
sitting beside the docs would be judged by where the docs live. The escaping read
into docs/qa/platform-checklist/areas/ is declared in
scripts/cross-package-test-inputs.mjs with matching turbo.json inputs, so
neither of CI's scoping layers replays a cached green over it.

Three legs, because any one alone is satisfiable by a file that says nothing:
ALIGNMENT (every site naming the seam states the runtime's status and code),
ABSENCE (no instructional string in any area file teaches the retired
convention), FLOOR (each instructional family still carries an aligned
statement). history is exempt from the absence leg by design — a revision entry
has to be free to quote what it corrected, which is exactly what this card was
told to preserve.

Ablation, two legs, mutation shown on disk before each run and the restore
verified by blob hash, never by exit code:

HEAD blob  checklist = 4d00eca34e8ba0dbb622d17cfa42e148312b84e3
HEAD blob  seam      = ba3aeb861171543e87d4841766bc84e6a0fe346e

LEG A  simulate the next behaviour change (401 -> 403 at the seam)
  on disk BEFORE: 'ANONYMOUS_SESSION_REFUSAL_STATUS = 401' x1
  on disk AFTER : 401 x0 · 403 x1
  pin exit = 1   FAIL "every site naming the refusal seam states the status and code the RUNTIME emits"
      + "items[4].steps[7]: missing status 403"
      + "items[4].steps[7]: missing code PERMISSION_DENIED"
      + "items[4].acceptance[4].verify: missing status 403"   (and the rest)
  restored, blob matches HEAD: ba3aeb861171543e87d4841766bc84e6a0fe346e

LEG B  revert the checklist to the RETIRED assertion
  on disk BEFORE: '200-with-null-body' x0
  on disk AFTER : '200-with-null-body' x1
  pin exit = 1   FAIL "no instructional string teaches the retired 200-plus-null convention"
      + "items[4].negative[2]"
  restored, blob matches HEAD: 4d00eca34e8ba0dbb622d17cfa42e148312b84e3

RESTORED-TREE CONTROL  pin exit = 0 ; git diff HEAD over both paths empty

Leg A also settles the resolution question the dist-preflight rule exists for:
the mutated constant is reached through a relative source import
(./anonymous-session-refusal), not through a dependency's exports, so no
rebuild sits between the mutation and the red — and the red arriving without one
is the evidence, not an assumption. Leg B's subject is a JSON file read at
runtime, with no build in its path at all.

Tier

Measured, not assumed: packages/plugins/plugin-auth runs vitest run with no
project partition and has no vitest-tiers.ts (the only one in the repo is
packages/cli's). The new pin therefore lands in the package's single tier and
is covered by the ordinary pnpm --filter @objectstack/plugin-auth test run
below — nothing moved out of a tier, and no existing pin changed tier.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
    derived 72 families; reconciled with --ran carrying every exit code:
    72 derived, 71 run, 1 NOT-MEASURED, 0 UNRUN.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3 = PREREQUISITE NOT
    MET (reads built output; 40 packages have no dist/ in this worktree). Not a
    pass and not a red — CI's Build Core job owns it.
  • Two gates went red or refused on first contact and were repaired inside this
    diff rather than argued with: check:cross-package-test-inputs exit 1 naming
    scripts/cross-package-test-inputs.mjs as a path the test names in prose
    with no glob covering it (the "named rather than read" class this entry already
    carries for two sibling scripts — declared, per its own note that declaring is
    cheaper than rewording prose to dodge the collector), now exit 0; and
    check:type-check-debt exit 3 until its three unbuilt workspace dependencies
    were built, then exit 0 with 4 ledger entries re-measured, 53 raw tsc errors, none above its recorded number.
  • pnpm lint repo-wide (eslint . --no-inline-config, full population, no
    narrowing): exit 0. Run although dispatch-gates does not name this family.
  • pnpm --filter @objectstack/plugin-auth test: 112 files, 2365 tests passed.
  • pnpm --filter @objectstack/plugin-auth typecheck: exit 0 —
    test layer compiles under tsconfig.test.json; 10 files / 94 errors / 23 pinned signatures held (unchanged ledger).
  • pnpm check:platform-checklist: exit 0 — 15 areas, 264 items; symbol anchors 577/633 resolved, 17 file floors held. The evidence row's anchor was swapped
    one-for-one (session-of-record.test.ts#body out,
    anonymous-session-refusal.ts#ANONYMOUS_SESSION_REFUSAL_STATUS in), so the
    per-file floor is untouched.
  • Dependency closure built first (pnpm --filter '@objectstack/plugin-auth^...' build), so nothing here was judged against a stale dist.

Landing surface and changeset

check-governed-merges.mjs --test on the final four-path file list: 0 of 4
hit the register — NOT governed
, ordinary queue landing. Control in the other
direction: the same tool answers 1 of 1 GOVERNED for
.claude/skills/pm-dispatch/SKILL.md.

skip-changeset, measured rather than assumed. plugin-auth publishes
files: ["dist","README.md","CHANGELOG.md"]; after building it, the new test's
symbols (checklist-refusal-envelope-consistency, RETIRED_CONVENTION,
instructionalStrings) hit 0 across all three, while the positive control
ANONYMOUS_SESSION_REFUSAL_STATUS hits dist/index.js and dist/index.mjs.
Across the 70 published packages, 0 name scripts in files[] and exactly
one names src (packages/spec, which this diff does not touch); turbo.json
is named by none. Positive control: all 70 name dist.

Acceptance notes

  • noted, not filed: the revoke leaves one sys_session row behind — the
    deliberate tombstone session-tombstone.ts writes, not a leak. Named here
    because the probe printed the count and a later reader would otherwise have to
    re-derive it.
  • noted, not filed: check:platform-checklist is not a per-PR gate. That is a
    recorded maintainer decision, stated in the script's own header, and it is the
    reason this card's pin had to live in a package rather than beside the docs —
    not a gap to file. Successor: whoever next moves a checklist invariant.
  • noted, not filed: five further get-session statements in this file describe
    behaviour without teaching the convention and stay true under both wire
    answers, so they were left alone rather than swept along. Successor: whoever
    next re-points this item.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3


Generated by Claude Code

…usal envelope

The five sites in `docs/qa/platform-checklist/areas/identity-auth.json` that
taught better-auth's retired no-session convention (HTTP 200 + a JSON `null`
body) were inverted by #17238/#17881: `refuseAnonymousSession` now converts
that answer into `401` + `UNAUTHENTICATED` in the ADR-0112 refusal envelope.
A runner following the file scored the correct implementation as defective,
and the remedy the negative pointed at was undoing an auth tightening.

Four instructional sites re-pointed (the step, clause 5's verify, the third
negative, the evidence row). `revision 3` is left standing and unedited --
it records what the August judgement rested on -- and a new `revision 6`
states that its cited authority was inverted afterwards.

Measured on this checkout rather than relayed: live session 200 with
{ user, session }; after revoke-sessions, 401 UNAUTHENTICATED; fresh-manager
control still 200. The refusal covers the REVOKED path, not only the
never-signed-in one, because the seam keys on the answer shape.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
…ssion refusal envelope

`checklist-refusal-envelope-consistency.test.ts` holds
`docs/qa/platform-checklist/areas/*.json` equal to this package's own
`anonymous-session-refusal.ts`: the status is read from
`ANONYMOUS_SESSION_REFUSAL_STATUS` and the code is derived from it through
ADR-0112's status map, so neither is spelled a second time.

Three legs: ALIGNMENT (every site naming the seam states the status and code
the runtime emits), ABSENCE (no instructional string teaches the retired
`200` + JSON `null` convention, tree-scoped over every area file), and FLOOR
(each instructional family still carries an aligned statement, so the first
two cannot be satisfied by deleting the guidance). `history` is exempt from
the absence leg by design -- a revision entry has to be able to quote what it
corrected.

It lives in `plugin-auth` because the next behaviour change is a diff here,
which puts this package in the affected set; `check:platform-checklist` is
deliberately not a per-PR gate. The escaping read is declared in
`scripts/cross-package-test-inputs.mjs` with matching `turbo.json` inputs so
neither scoping layer replays a cached green over it.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
`check:cross-package-test-inputs` went red naming
`scripts/cross-package-test-inputs.mjs` as a path
`checklist-refusal-envelope-consistency.test.ts` names but no glob covers.
Same "named rather than read" class the plugin-auth entry already carries for
`check-published-files.mjs` and `check-cross-package-test-inputs.mjs`, and
settled the same way its own note prescribes: declare the file rather than
reword the prose to dodge the collector. Mirrored into `turbo.json`.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
@os-support-ai os-support-ai added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

…back tally

`check-ci-filter-parity --self-test` holds a per-card ledger of the globs a
rollback of `crosspkg` to its pre-#10015 list leaves uncovered, judged over
the LIVE declaration table — the assertion's own note says a declaration
under a new root "moves it and is recorded here by name". This branch added
one, so the tally moves 21 -> 22 and gains its entry.

Which of this branch's two new declarations moved it was MEASURED, not
inferred: rebuilding the uncovered set against `origin/main`'s table and
against this branch's gives 21 vs 22, and the single added member is
`docs/qa/platform-checklist/areas/*.json`. The sibling declaration
`scripts/cross-package-test-inputs.mjs` moves nothing — the rollback keeps
`scripts/**`, which covers it. The by-name assertion therefore names the
QA-checklist glob.

Before: 1 of 47 assertions failed, verdict never reached.
After: 48 assertions, verdict reached, success line printed.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants