Repository navigation
fix(client): getActiveMember's three anonymous statements say 401 UNAUTHENTICATED on request one, and a pin holds them there - #18810
Merged
os-support-ai merged 2 commits intoSep 17, 2026
Conversation
…UTHENTICATED on request one, and a pin holds them there objectstack#17881 (374d9d3) landed plugin-auth's refuseAnonymousSession, which converts better-auth's 200 + the literal JSON null on GET /api/v1/auth/get-session into the declared ADR-0112 refusal envelope -- HTTP 401, code UNAUTHENTICATED -- before it leaves the process. Three present-tense statements in and around organizations.getActiveMember went on describing the retired shape, wrong twice over: the CODE (UNAUTHORIZED vs UNAUTHENTICATED) and the REQUEST the refusal arrives on (the second, list-members, vs the first, get-session itself). packages/client/src/index.ts changes COMMENTS ONLY -- verified mechanically: every changed line in that file, comment leader stripped, is a comment line. The three, located by SYMBOL and not by line number (the filing card's line numbers were already 15 lines stale by dispatch): 1. Step 1 of the two-request list, which taught the retired VALUE ("the literal null for an anonymous one"). The signed-in arm keeps its (measured) tag; the anonymous arm is now stated separately and anchored to the producer, including that step 2 never reaches the wire. 2. The anonymous bullet of the 2026-09-09 drive's delta list, which taught the retired CODE ("still gets 401 UNAUTHORIZED, thrown from the list-members request"). RE-ANCHORED rather than restamped, the disposition the sibling delivery used on the same family: the drive's own row is kept in the past tense, and today's answer is stated from the producer. 3. The inline comment on the userId read, which taught the retired VALUE and the wrong request. It now says an anonymous caller never reaches that line, and says why the "| null" annotation and the "?? ''" fallback stay. The pin is a source-text assertion over the getActiveMember region, located by symbol, with its matchers shown FIRING against a verbatim pre-repair control corpus -- a matcher that has never fired cannot tell "absent" from "unmatchable", which is how this family's own defect was nearly written off. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
…measured against the published files[] skip-changeset is refused here by measurement, not by instinct. Built dist at 13e09a5: the corrected sentence is present once in dist/index.d.ts, dist/index.d.mts, dist/index.js and dist/index.mjs; the retired sentence is absent from all four; getActiveMember was carried as the lit control and found in every one of them. The package's files[] is ["dist","README.md", "CHANGELOG.md"], so that prose ships. Recorded because the dispatch's fence named the wrong second artifact: this package emits NO .d.cts and NO .cjs. Its CJS pair is index.js + index.d.ts and its ESM pair is index.mjs + index.d.mts, so a *.d.cts probe here would have measured an absent file and reported a zero about nothing. Clause-② reads `no` through readClause2Line (kind=declared, value=no, arm=null). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-support-ai
marked this pull request as ready for review
September 17, 2026 22:54
os-support-ai
enabled auto-merge
September 17, 2026 22:54
os-support-ai
deleted the
claude/issue-18651-getactivemember-retired-anon-statements
branch
September 17, 2026 23:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18651
Clause-②: no
#17881(374d9d3afa) landedplugin-auth'srefuseAnonymousSession, which converts better-auth's200+ the literal JSONnullonGET /api/v1/auth/get-sessioninto the declared ADR-0112 refusal envelope — HTTP401,code: UNAUTHENTICATED— before it leaves the process. Three present-tense statements in and aroundorganizations.getActiveMemberstill described the retired shape, wrong on two axes at once: the CODE (UNAUTHORIZEDvsUNAUTHENTICATED) and the REQUEST the refusal arrives on (the second,list-members, vs the first,/get-sessionitself).packages/client/src/index.tschanges comments only — verified mechanically: of every line the diff touches in that file, zero are outside a comment.The three, located by SYMBOL
nullfor an anonymous one"(measured)tag; the anonymous answer is stated separately from the producer, including that step 2 never reaches the wire401 UNAUTHORIZED, thrown from thelist-membersrequest"userIdread| nullannotation and the?? ''fallback are explained as the defensive branch they always were⭐ The seat's "naming is not teaching" fence SURVIVED contact, and is load-bearing
The repaired prose still contains
200,nullandUNAUTHORIZED— it has to, because it names the retired convention as the thing that was CONVERTED and as the drive row that was SUPERSEDED. A "mentions both 200 and null" filter reads the repaired file as defective. So the pin does not count mentions: it matches the three retired SENTENCES and proves it can see them by running the same matchers over a verbatim pre-repair control corpus.The fence earned its keep on a real near-miss, below.
The complete-set question the card left open — answered with a reading
The card recorded that it had ⛔ not swept for sibling statements. Every
anonymous/UNAUTHORIZED/UNAUTHENTICATEDhit inpackages/client/srcwas opened, not counted:index.tsnormalizeSessionResponsedocblock andauth.me()— already repaired by fix(client): the anonymous /get-session statements say 401 UNAUTHENTICATED, and the test double stops modelling 200 null #18642. Correct today. Untouched.index.tssecurity-admin docblock ("Anonymous callers are denied unconditionally server-side") and the/ai/*docblock ("An anonymous caller is refused 401 first") — different routes, true under both wire answers. Untouched.index.tsadoptRotatedSessionToken—401 UNAUTHORIZED(client SDK: a bearer-modeObjectStackClientis silently signed out byauth.twoFactor.disable(), the enrolment-lanetwoFactor.verifyTotp()andchangePassword({ revokeOtherSessions: true })— the server rotates the session and the SDK stores neither the echoed token norset-auth-token#16534) is a DELETED session presenting a stale bearer token on a non-/get-sessionroute. Not this seam. Untouched.organization-get-active-member-addressing.test.tstranscript row200 null, markedSUPERSEDED, see belowin the file's own header, and thelist-members401 UNAUTHORIZEDarm that fix(client): the anonymous /get-session statements say 401 UNAUTHENTICATED, and the test double stops modelling 200 null #18642 deliberately KEPT because it discriminates on a DIFFERENT code. Naming, not teaching. Untouched.auth-rotated-session-token.test.tsprincipalFor— "nullfor anonymous, never a status code". This is the near-miss. A mention filter flags it; a reading clears it. It describesauth.api.getSession(), better-auth's JS API, andanonymous-session-refusal.ts's own module header rules that seam out in as many words: "⛔ It does not touch better-auth's JS API.auth.api.getSession()… still answersnullfor an anonymous caller, because that is a function return value and not an HTTP answer. Only the wire shape moves." TRUE today. Untouched.⇒ Three is three. Three statements TEACH the retired convention; every other candidate in the package either is already repaired, is true under both wire answers, or names the convention as retired.
The pin — FAILS before, passes after
packages/client/src/organization-get-active-member-anonymous-statements.test.ts. A source-text assertion over thegetActiveMemberregion, located by SYMBOL and never by line number, with its matchers shown FIRING against a verbatim pre-repair control corpus — a matcher that has never fired cannot tell "absent" from "unmatchable", which is how this family's own defect was nearly written off (the filing seat's grep forAnonymous -> nullmissed the file's Unicode arrow).Ablation — one-off, fix committed first, mutation proved on disk, restored and proved restored, under a
trap … EXIT INT TERMwith absolute paths. This pin readspackages/client/src/index.tsas TEXT throughreadFileSyncon./index.ts; it does not resolve its subject throughdist, so no dist preflight applies and the on-disk proof is the grep pair plus the blob hash.git checkout HEAD^ -- packages/client/src/index.ts. On-disk proof: corrected-sentence count1 -> 0, retired-sentence count0 -> 1, blob102b25a4d(HEAD)->2f920cf4f(mutated). Not a no-op.Tests 7 failed | 1 passed (8). It fails on all three retired matchers AND on all four "states today's answer" assertions. The one PASS is section 1, which only asserts the region was found — by design, so a red here cannot be mistaken for a broken locator.git checkout HEAD -- packages/client/src/index.ts(point-namedHEAD, never a baregit checkout --, which would take the mutation back out of the index). Restored blob102b25a4dequals the HEAD blob;git diff HEADon that path is empty.Tests 8 passed (8).Readings
Local runs, at⚠️ every wall-clock absolute below is a SHARED-BOX figure.
fdca8ef2beunless noted. Heavy runs went throughscripts/pm/os-verify-lock.sh;pnpm --filter '@objectstack/client^...' build --concurrency=2(dependency closure)pnpm --filter @objectstack/client buildcheck-dts-emitted: 1/1 declaration file(s) present)pnpm --filter @objectstack/client typecheck(tsc --noEmit+check:test-typecheck)pnpm --filter @objectstack/client test(whole package)Test Files 46 passed (46),Tests 545 passed (545). 46 is the whole on-disk population (tests/integration/**is excluded by this package's config; 1 file there); the new pin is one of the 467 failed | 1 passed) / exit 0 (8 passed)eslint . --no-inline-configrepo-wide--format json), 0 errors, 0 warnings. Both changed files are in that population. Ran atfdca8ef2be, the final commit; 101s wallscripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack--ranwith exit codes recordedEvery exit code above was captured BEFORE any pipe (
cmd > file 2>&1; EXIT=$?).Gate families — the two that are not a plain green
pnpm check:dual-build-cjs-loads— exit 3, its own PREREQUISITE-NOT-MET code: "Runpnpm buildfirst. ⛔ This is NOT a pass: nothing was measured." A repo-wide build is CI's run, not this card's. NOT MEASURED, declared.dispatch-gates --ranclassified it the same way from the recorded code.pnpm --filter @objectstack/spec run check:skill-examples— exit 1 on the sweep pass, exit 0 on a clean re-run (258 prose examples type-check across 3 surface(s)). Reported as observed rather than as a single number. This gate does read@objectstack/clientTSDoc examples; the changed docblocks carry no marked code block, so the population is unchanged.Tier reading, BOTH directions
The dispatch warned that
packages/cli/vitest-tiers.tstreatsnew ObjectQL(as a KERNEL signal and that a new pin can move a whole file out of its tier. Measured, and it does not reach this card — see the falsification below.packages/cli's tiers: its population is derived bytestFilesOnDisk(pkgRoot)walkingpackages/cli. Measured: 262 files, 49 integration / 213 unit. The new pin is absent; nopackages/clientpath is in that population at all. ⭐ Control lit — a realpackages/clifile (src/adr-0048-app-split.test.ts) IS in it, so the enumeration was pointed at something.packages/client: there is no tier partition to move within.packages/client/vitest.config.tshas 0 occurrences ofvitest-tiers,integrationTestFiles,unitTestFilesorprojects; its only split isexclude: ['tests/integration/**'], and the pin is undersrc/. Separately, the pin contains 0 occurrences ofnew ObjectQL(. ⭐ Control lit —new ObjectQL(DOES occur in three sibling files in that same directory (auth-get-session-envelope.test.tsamong them), so the grep can find it.pnpm check:tier-file-adoption— exit 0.The changeset question was MEASURED, not inferred
AGENTS.md:1064-1066—skip-changesetis for a diff that publishes nothing from any released package.@objectstack/client'sfiles[]is["dist","README.md","CHANGELOG.md"], andgetActiveMemberis a member of the exportedObjectStackClient, so its TSDoc is emitted into the shipped artifacts. Measured on the builtdistat13e09a5e3c:getActiveMember)dist/index.d.tsdist/index.d.mtsdist/index.jsdist/index.mjs⇒ this publishes ⇒
patchchangeset,skip-changesetrefused by measurement.:3818/:3847/:3880at filing are:3833/:3862/:3895atorigin/main— a uniform +15 shift. Everything here is located by symbol.:4494is not a fourth defect. Under that same +15 it resolves toauth.me()'s⚠️ Anonymous REJECTSdocblock — the statement PR fix(client): the anonymous /get-session statements say 401 UNAUTHENTICATED, and the test double stops modelling 200 null #18642 already repaired. It is correct today. The card's open question is closed, and nothing is owed there.*.d.ctsis the wrong second artifact for this package. The dispatch's fence said TSDoc ships intodist/*.d.tsand*.d.cts.@objectstack/clientemits no.d.ctsand no.cjs: its CJS pair isindex.js+index.d.tsand its ESM pair isindex.mjs+index.d.mts. A*.d.ctsprobe here would have measured an absent file and returned a zero about nothing. The measurement above names the four artifacts that exist.vitest-tiers.tsexists only inpackages/cli— it is the singlevitest-tiers*file in the repo outside that package's own fixtures and partition pin.packages/clienthas no tier system, so no pin placed there can move a file between tiers. Measured both directions above rather than argued.Acceptance notes — out of scope, noted, not filed
packages/client/src/auth-rotated-session-token.test.ts,principalFor's docblock describes anauth.api.getSession()call in HTTP wording ("a 200 and a JSONnull") for a call that never goes over HTTP. The claim it is making is TRUE — the JS API returnsnulland no status, which is exactly why a status assertion is blind there — so this is wording looseness in a test file that publishes nothing, ⛔ not an error and ⛔ not a defect class. Carrier: the next PR that touches that file, which is client SDK: a bearer-modeObjectStackClientis silently signed out byauth.twoFactor.disable(), the enrolment-lanetwoFactor.verifyTotp()andchangePassword({ revokeOtherSessions: true })— the server rotates the session and the SDK stores neither the echoed token norset-auth-token#16534's territory and where the wording lives.getActiveMember's@throwstag lists the falsy-id throw and the 200-with-no-row throw and does not list the anonymous rejection. That gap predates fix(plugin-auth)!: an anonymous get-session is refused with the declared 401 envelope, not answered 200 null #17881 — an anonymous caller threw before it too, through thelist-members401 and the same sharedfetchwrapper — so fix(plugin-auth)!: an anonymous get-session is refused with the declared 401 envelope, not answered 200 null #17881 did not falsify it. It is a missing member, ⛔ not an error, and adding to a published@throwstag is a contract statement this card's fence does not cover. Carrier: none known.Neither meets class (a), (b) or (c), so neither is filed.
Generated by Claude Code