Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-auth): keep the logger receiver when selecting a log channel — audience refusals report their verdict instead of 500 null - #12803
Conversation
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0c5ac1f4244cbddef7df7ae7e00f6a73b9efcf2c && git checkout 0c5ac1f4244cbddef7df7ae7e00f6a73b9efcf2c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 96dc446c9c19063edfae26ae30ff75143ef0c5b7 8b7ac8336ad73943cedf53648759a0ca056e363b && git checkout -B drift-repro 96dc446c9c19063edfae26ae30ff75143ef0c5b7 && git merge --no-ff 8b7ac8336ad73943cedf53648759a0ca056e363b
node scripts/docs-audit/affected-docs.mjs --json 96dc446c9c19063edfae26ae30ff75143ef0c5b7
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12773
Selecting a log channel by extracting the method loses the receiver.
a.bin callposition passes
aas the receiver;(a.b ?? c.d)(...)evaluates to the bare functionfirst, so the call runs with
this === undefined. A plain-closure logger does not readthisand survives it — which is why no suite caught this.@objectstack/core'sObjectLoggeris a real class with prototype methods and no constructor binding, so itthrew and the audience refusal was delivered as
HTTP 500with a null body.All verification below was run at commit
8b7ac8336.Measured
Each claim names its file and line, and the command that produced it.
The defect site, re-located. The card says
auth-manager.ts:3499; the PM's dispatchsays
:3610. On this branch's merge baseead731756it is atpackages/plugins/plugin-auth/src/auth-manager.ts:3610— the PM's number, confirmed bygrep, not trusted from either source.The host logger really is receiver-sensitive.
packages/core/src/logger.ts:219declares
class ObjectLogger;:414iserror(...)callingthis.writeErrorLike(...),and
:401/:405/:409aredebug/info/warncallingthis.write(...). There is nobind(this)anywhere in the constructor. So every channel of the real host loggerdereferences
this, not justerror.Why the failure surfaced as
500 nullrather than a logged warning. InvalidateAudienceAdmissionthe call atauth-manager.ts:3724sits inside atry, andthat block's
catchat:3735calls the same helper again. The first throw lands inthe
catch, the second escapes the gate entirely — so the refusal reached neither thecaller nor the log. This is read from the control flow at those two lines.
Three live sites, all inside
plugin-auth, all fixed here:ead731756)src/auth-manager.ts(logger?.error ?? logger?.warn)?.(...)error, elsewarnsrc/reconcile-membership.tsconst log = deps.logger?.error ?? deps.logger?.warnerror, elsewarnsrc/adopt-membership.tsconst log = options.logger?.info ?? ...infoThe second and third are siblings the PM's sweep did not find, because they are the
two-step
const fn = obj.method; fn(...)form rather than the syntactic(a ?? b)(...)one. Triage's scope ("fix any siblings inside
plugin-authin the same PR") covers them.Each is now a property call that keeps its fallback.
Tests.
pnpm --filter @objectstack/plugin-auth test— its own judgment line:pnpm --filter @objectstack/plugin-auth typecheck— exit 0. Note it needs the package'sown
distbuilt first: on an unbuilt worktree its second program(
tsc -p tsconfig.examples.json) fails withTS2307onexamples/basic-usage.ts, whichis a prerequisite, not a regression.
Gate union, re-derived live on the actual changed set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack: 21 path-derivedfamilies plus 6 convention-triggered ones (this PR adds a test file). All run, all exit 0,
including
check:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:type-check-coverage,check:published-files,check:test-source-aliasandcheck:nul-bytes.pnpm lint(
eslint . --no-inline-config, whole repo) exits 0 — the full scan, not a narrowing.check:type-check-debt(the ratchet half, run against a fully built closure) exits 0;its own line:
OK — 31 ledger entr(ies) re-measured in 233.1s, 1570 raw tsc error(s) total, none above its recorded number.node scripts/pm/check-half-states.mjsreturned exit 3 =PREREQUISITE NOT MET—no GitHub credential in this container. Its own text: "Nothing was swept ... it is no
reading at all." Recorded as NOT MEASURED, neither pass nor fail. The lint.yml
wrapper
check:pm-half-statesexits 0.The ablation — proof the pin can go red
Three legs, one per fixed site, each mutating the file back to the detached form and
running the whole package suite so "nothing else fails" is measured across all 1695
tests. Every leg proves the mutation reached the disk by
git hash-objectagainst theHEAD blob (never an editor exit code, never a bare
git diff --stat), and proves therestore by hash equality plus empty
git diff HEADplus emptygit status --porcelain.The replacement asserts an exact hit count of 1 and aborts otherwise, so a zero-hit no-op
cannot be reported as a green ablation. A
trap ... EXIT INT TERMon an absolute pathfrom
git rev-parse --show-toplevelrestores on any kill.auth-manager.ts299bc028a3b5b5a8659creconcile-membership.ts1a5476f9d86c7c07b7e7adopt-membership.tsa96f3cfc6cff578d317bIn every leg the failures were exactly the cases pinning that site and nothing else,
and each failed with
TypeError: Cannot read properties of undefined (reading 'record')—structurally identical to the production
reading 'writeErrorLike'. All three restoresverified clean.
No rebuild is involved: the pin imports the sites relatively (
./auth-manager.js), sovitest resolves package source, not
dist. The mutation going red is itself the proofthat the source under test is the code that ran.
The pin, and why the double is class-based
src/logger-receiver-detach.test.ts. The double is a class whose channels dispatchthrough
this.record(...), mirroringObjectLogger'sthis.write/this.writeErrorLike.A bare-closure double passes against the broken code and pins nothing.
Case ⓪ asserts that directly, so the file cannot quietly become vacuous: it asserts the
class double throws when detached, and — beside it — that a closure double does
not, which records in executable form why the closure shape must never be substituted
here. Cases ①–② drive the real audience gate rather than the private helper, so what is
pinned is the user-visible contract: the refusal reaches the caller carrying
AUTH_CONFIG_ERRORand its message, and a host shipping onlywarnstill gets thefallback (#9754). Cases ③–④ cover the other two sites, each on both the
errorand thereduced-sink path where one exists.
The sweep — what I covered, and what I did not
Re-derived rather than taken from the dispatch. Every regex was validated against a
synthetic positive control before its zero-hit result was believed, and two of them
were wrong on the first try and were fixed:
(a ?? b)(...)regex initially matched nothing at all, including the knowndefect site — a broken instrument reading as a clean repo;
^with an unindented control, so itmissed every indented
const, which is every real site. Fixing that anchor is whatsurfaced
reconcile-membership.ts:161andadopt-membership.ts:239.Covered (packages + apps, source, excluding
dist/node_modules):(a ?? b)(...)and(a ?? b)?.(...)— 1 site inplugin-auth(fixed); 9 call sites inpackages/drivers/driver-sql/src/sql-driver.ts(:10575,:10744,:10791,:11408,:11415,:11426,:11482,:14162,:14515) plus 2 occurrences inside its docblockprose;
packages/cli/src/utils/format.ts:1033;packages/cli/src/utils/dev-restart.ts:275; one test file.(a || b)(...)— zero, control validated.(cond ? a.m : b.m)(...)— zero real sites; every hit was a regex literal.const f = o.m, including??/||chains) — the shapethe PM flagged as unsearched. Found the two
plugin-authsiblings fixed here.const { error } = logger) and method-as-callback(
arr.forEach(logger.error)) — swept; no live defect inplugin-authsource.Not covered — stated so nobody reads this as a clean bill:
driver-sqlsites actually crash is NOT established here, and this PRdeliberately does not touch that package. They are carried on [finding] driver-sql has 10 inline
(this.logger.error ?? this.logger.warn)(…)detach-then-call sites — the shape #12773 measured crashing, in a file that already contains the correct spelling #12792. I measured 9call sites where the dispatch brief said 10; the difference is not adjudicated here and
belongs to that card. My regex is single-line, so a fallback split across lines would
have been missed in either count.
plugin-auth, where I reviewed every hit byhand. Repo-wide they are dominated by false positives (value fields named
error,expect(logger.error)assertions), so I make no repo-wide zero claim for those twoshapes.
obj[name]lookup was notsearched at all.
.bind()sites found incidentally(
trigger-record-change/src/record-change-trigger.ts:289,trigger-schedule/src/schedule-trigger.ts:88,trigger-schedule/src/time-relative-trigger.ts:400,services/service-job/src/db-job-adapter.ts:253) are listed as evidence that thereceiver-preserving idiom already exists in this codebase — they need no change.
Inferred, not measured
plugin-authis takenfrom the issue's captured stack trace; I did not boot a composed deployment myself.
500 nullis reproduced here at the level of mechanism (theescaping second throw, and a
TypeErrorof the same shape), not by driving HTTP.Scope notes
No
securitylabel and notarget:v17, per triage: the gate still refuses and failsclosed; the cost is diagnostic. Clause ② stays
no— re-derived live on the actualchanged set, the union names no contract family, so the falsifiable condition the
dispatch set did not trigger.
One thing checked and deliberately not filed:
plugin-auth/tsconfig.jsonexcludes**/*.test.ts, so no tsc program compiles this package's 82 test files. That is not anundeclared gap — it is a declared, ratcheted
TEST_DEBTledger entry(
scripts/check-type-check-coverage.mjs:895,errors: 97), already governed by #6376.The new test file adds 0 errors to it: measured at 94 with this diff applied and 94
with the three source files reverted to
ead731756and the new test parked, so theledger's
-3drift is entirely pre-existing and none of it is mine.Generated by Claude Code