Repository navigation
Commit d6137fd
fix(client): the anonymous /get-session statements say 401 UNAUTHENTICATED, and the test double stops modelling 200 null (#18642)
Fixes #18139
`#17881` (`374d9d3afa`) moved the anonymous `GET
/api/v1/auth/get-session` wire answer from `200` + the literal JSON
`null` to the declared ADR-0112 refusal envelope — HTTP `401`, `code:
UNAUTHENTICATED`. `@objectstack/client` reaches the server over the
wire, so that is exactly what it sees. This PR pulls the SDK's own
statements and one test double back onto it. No behaviour moves:
`packages/client/src/index.ts` changes **comments only** (verified
mechanically — every changed line in that file is inside a comment).
## Half 1 — the SDK's statements
Three present-tense statements, none carrying a rev or a date, so none
read as history:
1. **The `/auth/*` wire table** on `normalizeSessionResponse` (located
by symbol, not by line). Its anonymous `-> 200 null` row is removed from
the list of bodies the helper is handed — it is not handed that body at
all now, because the shared `fetch` wrapper throws on the non-2xx one
frame out. The current answer is stated separately, anchored to the
producer.
2. **The closing `!body`-guard paragraph** in the same docblock, which
claimed that guard "carries the anonymous answer" and that closing the
gap "needs the published return annotation to widen". `#17238` ruled the
opposite: the producer moved and `SessionResponseSchema` is untouched.
The guard's own code is byte-identical; it is now described as the
defensive branch it is.
3. **`auth.me()`'s docblock**, which said the route "serves the literal
`null` at 200 and it is returned as-is". It now says the anonymous call
REJECTS, with the code and status a caller catches.
## Half 2 — the phantom, which is the half that mattered
Triage's grading is quoted verbatim and is what this half answers:
> ⭐ 更值钱的是第二半:`organization-get-active-member-addressing.test.ts` 的
`signedIn: false` 腿建模了一个**运行时已不再产生**的答案 ⇒
它**不可能因为它存在的那个理由而失败**(AGENTS.md 的 phantom-check 形状)。**⛔ 只改 docblock
不算修完这张卡。**
**The disposition chosen: assert the refusal, not drop the leg —
justified from the tree.** The file's own header states its charter:
*"Only the socket is faked — every status, code and row shape below is a
transcript line from the drive"*. Under that charter the anonymous row
is not optional, it is stale, so it is re-anchored rather than deleted.
Dropping the leg would delete the only coverage that `getActiveMember`
does not invent a client-side diagnostic for an anonymous caller — a
property that is still real and is now sharper, because the refusal
arrives on request ONE instead of request TWO.
What changed in the fixture:
- The anonymous `/get-session` arm serves `401
{"success":false,"error":{"code":"UNAUTHENTICATED","message":"Sign in
first"}}` — shaped from the producer (`refuseAnonymousSession` derives
the code through `standardErrorCodeForHttpStatus(401)` and takes the
message from `PLATFORM_ADMIN_REFUSAL_MESSAGES[401]`), not restamped onto
a drive that was never re-run. The header says so in as many words;
every other row is still the 2026-09-08 drive, untouched.
- Case ⑥ now asserts `code: UNAUTHENTICATED` + `httpStatus: 401` and
pins `urls` as the WHOLE list `[AUTH + '/get-session']`, so a silent
extra request cannot hide behind a length check.
- The organisation-route `401 UNAUTHORIZED` arm is KEPT although the SDK
no longer reaches it, and that is load-bearing: it answers a **different
code** from the `/get-session` seam, so a regression that swallowed the
first refusal and walked on fails on the VALUE, not merely on a request
count. Case ⑥ drives it directly as a guard-the-guard, in the file's
existing ⑦/⑧ style.
### Ablation — the proof it can now fail for the reason it exists
One-off, committed first, mutation proved on disk, restored and proved
restored.
- Mutation: the fixture's anonymous arm reverted to `return json(200,
null);`. On-disk proof: refusal-arm grep count `1 -> 0`, retired-arm
grep count `0 -> 1`, blob `48459424a` (HEAD) vs `19eb96dcb` (mutated) —
not a no-op.
- RED leg: `Tests 1 failed | 7 passed (8)` — `⑥ ... AssertionError:
expected 'UNAUTHORIZED' to be 'UNAUTHENTICATED'`. It fails on the CODE,
which is the discrimination this design is for.
- Restore leg: `git checkout HEAD -- path` under an `EXIT INT TERM`
trap; restored blob `48459424a` equals the HEAD blob, `git diff HEAD`
empty, `git status --porcelain` empty.
- GREEN leg: `Test Files 1 passed (1) · Tests 8 passed (8)`.
## The premise the dispatch asked me to falsify — FALSIFIED
The card's dedupe was a recent-window search and said so. Swept
repo-wide for `get-session`, `200 null`, "literal `null`" and "literal
JSON `null`" with controls (a token known present returned 8 hits; a
nonsense token returned 0 at exit 1). The `:1474` table was **not** the
only present-tense statement left. Six more were found; three are fixed
here (the two above plus the guard paragraph) and three are listed in
Acceptance notes because they are out of this PR's face.
## The card's declared-unmeasured item — now MEASURED
The card wrote: *"Whether `auth.me()`'s rejection path is covered
anywhere today. I did not measure it."* Measured: **it IS covered,
twice.** `packages/client/src/client.test.ts` carries *"me() rejects the
anonymous refusal rather than resolving outside its type"* asserting
`code: UNAUTHENTICATED` / `httpStatus: 401`, and
`packages/client/src/auth-get-session-envelope.test.ts` block ③
(*"anonymous is REFUSED"*) carries the real-server twin. Both were
reversed by `#17238` and say so in their own comments. Nothing in this
PR depends on that coverage; the reading is reported because the card
asked for it.
## Clause-②
**Clause-②: no.** Three readings, from the measured diff:
- **New exports:** none. `packages/client/src/index.ts` changes comments
only; the test file and the changeset export nothing. The package's
public surface is byte-identical.
- **`ERROR_CODE_LEDGER` / `StandardErrorCode`:** untouched — the diff
names neither, at zero occurrences. `UNAUTHENTICATED` is an existing
STANDARD member that `#17881` already derives via
`standardErrorCodeForHttpStatus`; nothing is minted here.
- **Accept-set changes:** none. No `.zod.ts` in the diff (zero paths),
no schema key moves, nothing previously admitted is refused and nothing
new is admitted.
The expected direction held: this is a **pull-back** — the runtime
already answers 401 and the SDK's self-description was lagging. Nothing
widens.
## Tests
All readings taken in a dedicated worktree cut from `origin/main` at
`681ebe424`; the union below was re-run at final HEAD `81a05648d`.
- `pnpm --filter @objectstack/client typecheck` — exit 0. Note the two
halves: the root `tsc --noEmit` program does NOT include the edited test
file (`--listFiles` count 0, with `src/index.ts` at 1 as the lit
control); the test layer is covered by the script's second half,
`check:test-typecheck`, which reports *"@objectstack/client's test layer
compiles under packages/client/tsconfig.test.json; 0 file(s) / 0
error(s)"*.
- `pnpm --filter @objectstack/client test` — `Test Files 43 passed (43)
· Tests 524 passed (524)`.
- `pnpm --filter '@objectstack/client^...' build` — exit 0 (the
dependency closure; the first test run was a false red from an unbuilt
`@objectstack/spec`, not from this diff).
- **Gate derivation, rerun in this worktree** — `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 59 families. Reconciled with
`--ran`: **59 derived, 58 run, 1 NOT-MEASURED, 0 UNRUN.**
- NOT MEASURED: `pnpm check:dual-build-cjs-loads` — exit 3,
`PREREQUISITE NOT MET`, 34 packages have no `dist`; it needs a full
`pnpm build`, which does not fit a foreground turn here. Its own text
says *"This is NOT a pass: nothing was measured."* CI's `Build Core`
covers it.
- `pnpm --filter @objectstack/spec run check:skill-examples` first
exited 1 on the same prerequisite shape (`packages/client-react`
unbuilt). Built that package and re-ran: **exit 0**, 258 prose examples
across 3 surfaces.
- `pnpm check:cross-package-test-inputs` — exit 1, and it is **NOT from
this diff**. It flags
`packages/cli/test/init-created-files-summary.e2e.test.ts` descending
from `packages/spec/dist/`. Control: that file and
`scripts/check-cross-package-test-inputs.mjs` and `turbo.json` are all
byte-identical to `origin/main` (blob-for-blob), while a file this PR
did change differs — so the verdict is a property of `origin/main`, not
of these two paths. The walk is a static source property
(`join(SPEC_PKG, 'dist')` at that file's line 115). Filed below rather
than fixed here.
- `pnpm lint` repo-wide (`eslint . --no-inline-config`) — **exit 0**.
Run whole, not narrowed, so no narrowing needs proving.
- Ratchet and changeset families re-run at final HEAD `81a05648d`:
`check-empty-changeset`, `check-changeset-no-major`,
`check-adr-0087-registration`, `check:objectui-changeset`,
`check:pm-changeset-deadline-census`, `check:type-check-coverage`,
`check:type-check-debt`, `check:published-files`,
`check:tier-file-adoption`, `check:nul-bytes`, `check:test-source-alias`
— all exit 0.
### Changeset, measured rather than assumed
`skip-changeset` does **not** apply and was not requested.
`@objectstack/client`'s published `files[]` is
`["dist","README.md","CHANGELOG.md"]`, and `auth.me()` is a member of
the exported `ObjectStackClient`, so its TSDoc is emitted into the
shipped declarations. Measured on the built artifact: the corrected
sentence is present in `dist/index.d.ts`, `dist/index.d.mts`,
`dist/index.js` and `dist/index.mjs`; the retired sentence is absent
from `dist` afterwards; `getActiveMember` carried as the lit control,
found in the same four files. Hence a `patch` changeset.
## Acceptance notes
**Neighbour PR.** `#18429` (card `#17274`) also edits
`packages/client/src/index.ts` and was open and unmerged when this
branch was cut. Its measured hunks are at 3671-3689, 3962-3967,
3976-3983 (`organizations.invitations.resend`); this PR's edits are the
`/auth/*` docblock near 1474 and the `auth.me()` docblock near 4437. The
regions do not intersect — which guarantees textual mergeability and
nothing more.
**Three same-defect statements deliberately NOT fixed here, because they
sit in the `organizations.*` region this PR is fenced out of.** All
three are in `getActiveMember`'s docblock and body in
`packages/client/src/index.ts`, and all three now misdescribe the
runtime:
- *"The body is the bare `{ user, session }` envelope for a signed-in
caller and the literal `null` for an anonymous one (measured)"* — step 1
of the docblock's two-request explanation.
- *"an anonymous caller still gets `401 UNAUTHORIZED`, thrown from the
`list-members` request by the same session middleware"* — the refusal
now comes from the `/get-session` request with `UNAUTHENTICATED`, one
request earlier.
- The inline comment *"Anonymous → `null`, and the request below is then
refused 401 by the session middleware before the filter is ever read."*
They are the same defect class as this card and the fix is mechanical,
but the in-place-fix criteria do not all hold:
`packages/client/src/index.ts` is held by another live claim (`#18429`),
and the dispatch fences this PR out of that region by name. Handed back
for filing with dedupe words `getActiveMember`, `get-session`,
`anonymous`, `UNAUTHENTICATED`, `#17881`. **Reviewer, read this PR's
case ⑥ together with that docblock: they now disagree, and the docblock
is the one that is wrong.**
**Two more, outside `packages/client`, handed back for filing:**
- `docs/qa/platform-checklist/areas/identity-auth.json` (two clauses)
instructs a QA runner that *"better-auth's no-session convention is HTTP
200 with a JSON null body, so a 401 expectation misdescribes a correct
implementation"* and that `get-session` is not a valid oracle. Since
`#17881` a 401 is exactly what a correct implementation answers, so a
runner following this verbatim scores a correct platform as defective.
It even cites `session-of-record.test.ts` as its authority — and that
file was updated by `#17238` and now says the opposite in its own helper
docblock. This is the highest-value item in the sweep. Dedupe words:
`identity-auth.json`, `get-session`, `oracle`, `JSON null body`,
`revoke-user-sessions`.
- `.changeset/client-get-session-envelope-and-refresh-read.md` (pending,
unreleased) closes with *"Two answers stay outside the declared type and
are not addressed here: the anonymous `null`, which would need the
published return annotation to widen"* — present-tense, and it ships
verbatim into `@objectstack/client`'s `CHANGELOG.md` at the next
release. Its transcript block above it is anchored to a dated
better-auth 1.7.2 drive and reads as history; only the closing paragraph
is the problem. Not touched here because it is another PR's release
input. Dedupe words: `changeset`, `get-session`, `anonymous null`,
`return annotation`, `#16760`.
**Noted, not filed** —
`packages/adapters/hono/src/hono-auth-mount-basepath.test.ts` carries a
`GET /api/v1/auth/get-session -> 200 null` row in an AFTER transcript.
It is anchored to a dated maintainer ruling and a named drive, so it
reads as a measurement record rather than a present-tense claim, and no
assertion in that file depends on it. Who would carry it: that adapter's
next `#16025`-area edit. If nobody touches that file, nobody is misled
by it, because the file's own cases assert a relation and never that
body.
**Cleanup:** the worktree used for this work is removed; no dev server
was started and no temporary artifact is left in the tree.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e1f4f06 commit d6137fd
3 files changed
Lines changed: 161 additions & 21 deletions
File tree
- .changeset
- packages/client/src
Lines changed: 58 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1471 | 1471 | | |
1472 | 1472 | | |
1473 | 1473 | | |
1474 | | - | |
1475 | 1474 | | |
1476 | 1475 | | |
1477 | 1476 | | |
1478 | 1477 | | |
| 1478 | + | |
| 1479 | + | |
| 1480 | + | |
| 1481 | + | |
| 1482 | + | |
| 1483 | + | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
1479 | 1491 | | |
1480 | 1492 | | |
1481 | 1493 | | |
| |||
1518 | 1530 | | |
1519 | 1531 | | |
1520 | 1532 | | |
1521 | | - | |
1522 | | - | |
1523 | | - | |
1524 | | - | |
1525 | | - | |
| 1533 | + | |
| 1534 | + | |
| 1535 | + | |
| 1536 | + | |
| 1537 | + | |
| 1538 | + | |
| 1539 | + | |
| 1540 | + | |
1526 | 1541 | | |
1527 | 1542 | | |
1528 | 1543 | | |
| |||
4491 | 4506 | | |
4492 | 4507 | | |
4493 | 4508 | | |
4494 | | - | |
4495 | | - | |
4496 | | - | |
| 4509 | + | |
| 4510 | + | |
| 4511 | + | |
| 4512 | + | |
| 4513 | + | |
| 4514 | + | |
| 4515 | + | |
| 4516 | + | |
4497 | 4517 | | |
4498 | 4518 | | |
4499 | 4519 | | |
| |||
Lines changed: 74 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
| 40 | + | |
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
47 | 73 | | |
48 | 74 | | |
49 | 75 | | |
| |||
54 | 80 | | |
55 | 81 | | |
56 | 82 | | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
57 | 87 | | |
58 | 88 | | |
59 | 89 | | |
| |||
85 | 115 | | |
86 | 116 | | |
87 | 117 | | |
88 | | - | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
89 | 123 | | |
90 | 124 | | |
91 | 125 | | |
| |||
116 | 150 | | |
117 | 151 | | |
118 | 152 | | |
119 | | - | |
120 | | - | |
121 | | - | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
122 | 163 | | |
123 | 164 | | |
124 | 165 | | |
| |||
127 | 168 | | |
128 | 169 | | |
129 | 170 | | |
130 | | - | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
131 | 176 | | |
132 | 177 | | |
133 | 178 | | |
| |||
243 | 288 | | |
244 | 289 | | |
245 | 290 | | |
246 | | - | |
| 291 | + | |
247 | 292 | | |
248 | 293 | | |
249 | 294 | | |
250 | 295 | | |
251 | 296 | | |
252 | 297 | | |
253 | | - | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
254 | 303 | | |
255 | | - | |
256 | | - | |
257 | | - | |
258 | | - | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
259 | 321 | | |
260 | 322 | | |
261 | 323 | | |
| |||
0 commit comments