fix(plugin-audit): key the lost auth-event row report per CAUSE, and name the real cause in its first line - #18246
Conversation
…roduction, currently RED Drives two failures of DIFFERENT causes through `reportAuthEventWriteFailure` and shows the second degrades to `debug`, and shows the ADR-0057 §3.6 / `OS_TELEMETRY_DB` datasource hint printing for an organization refusal it is not the remedy for. 4 of 14 fail on the unfixed source. The three that pass — repeat-of-same-cause dedupe, code-keyed (never message-keyed) bounding, and the no-code fold — are the discriminating controls: a process-wide boolean satisfies them too, so they constrain the fix rather than demonstrate the defect. Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj Co-authored-by: Claude <noreply@anthropic.com>
…name the real cause in the first line `auth-event-audit.ts` carried a second, independent copy of both defects #15166 fixed in `audit-writers.ts`: its own process-level `failureReported` boolean, so after the first failure of ANY cause every later failure of every OTHER cause degraded to `debug` for the life of the process; and its own fixed message literal, printing the ADR-0057 §3.6 / `OS_TELEMETRY_DB` datasource guidance unconditionally, regardless of what actually failed. The dedupe key is now the failure's identity — the error `code` (or its absence) plus the object — and the helpers are `audit-writers.ts`'s, imported rather than re-spelled: a second copy of the key is how the defect reached this file, so a third spelling would be the same mistake again. A repeat of an already-reported cause still degrades to `debug`; a new cause reports at `error`, once. The datasource guidance is kept and made conditional on the missing-table cause it is the remedy for, asked through the shared `isMissingTableError` predicate for the one table `persistAuthEventAuditRow` writes. Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 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 0c7232a313ed46bc8a53ceb304d4eb29bd966f74 && git checkout 0c7232a313ed46bc8a53ceb304d4eb29bd966f74
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b d20bd87650185735e45342e52a63686bf7f94118 && git checkout -B drift-repro b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b && git merge --no-ff d20bd87650185735e45342e52a63686bf7f94118
node scripts/docs-audit/affected-docs.mjs --json b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b
|
Fixes #17452
Clause-②: no
What this is
packages/plugins/plugin-audit/src/auth-event-audit.ts— the writer behind thelogin/logoutrows of the compliance ledger — carried a second, independent copy of both defects that #15166 removed fromaudit-writers.ts:failureReportedboolean, so after the first failure of ANY cause, every later failure of every OTHER cause degraded todebugfor the life of the process;OS_TELEMETRY_DBdatasource guidance unconditionally, regardless of what actually failed.persistAuthEventAuditRowis registered inDURABILITY_CRITICAL_CALLEES(scripts/check-durability-degradation-log-level.mjs:361), whose entire purpose is that durability loss is reported aterror. That register is the declaration this restores — ⛔ it is not edited here.Separation re-verified before choosing the approach
The dispatch asked whether the two writers have converged since #15166. Measured on
origin/mainatb3b43b6ea(this branch's base), they had not:audit-writers.tsauth-event-audit.ts(before)reportAuditWriteFailurereportAuthEventWriteFailurereportedAuditFailureCauses(aSet)failureReported(a boolean)(engine as any).loggerAuthEventAuditLogger,errorOPTIONALsys_audit_log+sys_activitysys_audit_logonlySo the fix lands in place rather than merging two structurally different reporters. What is shared is the part the card called a port: the cause-key helpers.
What changed
audit-writers.ts—auditFailureCauseKeyandauditFailureCauseSummarybecome module exports so the auth-event sink can use them. ⛔ Not added tosrc/index.ts: the sharing is internal to the package. This mirrors the existingcreateFieldPresenceProbeimport that already crosses these two files.auth-event-audit.ts— the boolean becomesreportedAuthEventFailureCauses, aSetkeyed byauditFailureCauseKey(SESSION_OBJECT, err). The firsterrorline leads withauditFailureCauseSummary(err, detail)— the code and message that were already computed one line above the branch and passed only into thedebugpayload. The datasource guidance is kept and made conditional onisMissingTableError(err, 'sys_audit_log'), asked for the one table this writer writes.auth-event-audit.test.ts— 7 new cases (see below).patchchangeset.⛔ Importing the helpers rather than re-spelling them is deliberate: a second copy of this key is how the defect reached this file, so a third spelling would be the same mistake again.
Evidence
Reproduce first — the new block against the unfixed source
Commit
3985dd2bbis the pin block alone, on top of unmodified source.pnpm --filter @objectstack/plugin-audit exec vitest run src/auth-event-audit.test.ts:Both defects the card names, reproduced through the real reporter: a second cause silenced to
debug, and the datasource hint printed for a failure it is not the remedy for.After the fix
Tests 14 passed (14). Whole package:Test Files 23 passed (23) · Tests 341 passed (341);pnpm --filter @objectstack/plugin-audit typecheckexit 0 (check:test-typecheck: OK — 0 file(s) / 0 error(s)).Reverse verification — three legs, mutated on disk, each restored byte-identically
Every leg proves the mutation landed (
grep -con the anchor text and on the injected text, plus the on-disk blob hash moving), runs the suite, then restores withgit checkout HEAD -- pathand provesgit hash-objectequals the HEAD blob andgit diff HEADis empty. HEAD blobba07853195ca57faf56734d32e005a836cd0aae2before and after all three. No build/distleg is owed: the suite imports./auth-event-audit.jsrelatively andvitest.config.tsaliases@objectstack/typesto source, so nothing on the tested path resolves throughdist.31b8e4c…reports a SECOND, DIFFERENT cause at error,[#9657] the warn fallback is per-cause toomissingTableforced totrue(= the old unconditional hint)13edd5b…prints the datasource remedy for the cause it is the remedy FOR, and not for others7d998a1…reported at ERROR, oncecaseThe discriminating control for a per-cause dedupe
Leg C is the point. "A different cause now reports" is satisfied by simply deleting the boolean, which is the outcome AGENTS.md names as this rule's falsifier. Three cases hold the other half, and they stay green under leg A (12 of 14 passed there, and none of the three is in that leg's FAIL list) while going red under leg C:
still degrades a REPEAT of an already-reported cause to debug— 5 sign-ins, same cause ⇒ 1error+ 4debug, and thedebuglines carry the samecausekey.keys on the error CODE, never its message, so a per-row fault cannot flood error— 200 sign-ins, 200 distinct per-row messages, one code ⇒ 1error.folds a fault carrying NO code into ONE bucket rather than growing one— 200 sign-ins, no code at all ⇒ 1error.⇒ the delivered behaviour is cause-keyed dedupe, not no dedupe. The premise #15166's ruling hung on — that cause-keying does not reintroduce #4420's unreadable flood — holds on this seam too, and for a stronger reason: the object dimension is constant here (
sys_session), so the key reduces to the driver's own closed code vocabulary.Clause-② re-derived from the DELIVERED diff
src/index.tsis untouched (git diff origin/main --staton it is empty), andtsupbuilds the single entrysrc/index.ts..d.tsexport list of the published entry (dist/index.d.ts): 27 names, neitherauditFailureCauseKeynorauditFailureCauseSummaryamong them.name in await import('dist/index.mjs')(the pathexports["."]names):⇒
Clause-②: no, derived by reachability rather than by the wordexport. The changeset is gradedpatch.Gate denominator
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 65 families at HEADd20bd8765. All 65 were run with exit codes recorded before any pipe, and reconciled:Three first answered exit 3, PREREQUISITE NOT MET —
check:i18n,check:dual-build-cjs-loads,check:type-check-debt. ⛔ Not read as green: their prerequisite closures were built (turbo run buildover the i18n gate's named closure, then over./packages/*./packages/*/*) and all three re-run to exit 0 —check-i18n-bundles: OK (9 package(s) — all bundles in sync…)andcheck-type-check-coverage --re-measure: OK — 5 ledger entr(ies) re-measured, 55 raw tsc error(s) total, none above its recorded number.Outside the derived set and run anyway because it is the declaration this card restores:
pnpm check:durability-log-levelexit 0 —✓ durability-degradation log levels: 36 durability-critical catch seam(s), all loud….eslint — a declared narrowing, with its three readings
Repo-wide
pnpm lintis CI's run. Narrowed here to the three source files this diff touches, at HEADd20bd8765:① Population read from eslint's own
eslint.config.mjs, not guessed: the base block isfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']with furtherpackages/**/*.{ts,tsx,mts,cts}blocks — all three files are inside both.② Count read from
--format json: 3 files linted, 0 errors, 0 warnings, exit 0.③ Invariance over untouched files:
eslint.config.mjsstates at its own line 328 that this repo "runs oneeslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" — measured there with a positive control. With no cross-file type program, this diff cannot move the verdict on any file it does not touch.Acceptance notes
Out of scope for this PR, ⛔ not fixed here, and ⛔ not filed either — the dispatch reserved filing to the PM:
packages/plugins/plugin-audit/src/read-audit.ts(lines 484 onward).reportReadAuditWriteFailurehas its own process-levelfailureReportedboolean and its own fixed literal carrying the same unconditional ADR-0057 §3.6 /OS_TELEMETRY_DBguidance — and its calleepersistReadAuditRowsis registered inDURABILITY_CRITICAL_CALLEES(scripts/check-durability-degradation-log-level.mjs:357), exactly as the two already dealt with. Same declared invariant, same blast radius, on the record-view audit path. Now a cheap port: the helpers this PR exports are the whole shape it needs. Dedupe read: one targeted semantic search over this repo, 12 hits, no open duplicate — with [finding] plugin-audit:auth-event-audit.tscarries a SECOND copy of both #15166 defects — its own process-level dedupe boolean and its own unconditional datasource hint #17452 and finding(plugin-audit):reportAuditWriteFailurereports once per process then degrades todebug, and its first line blames the telemetry datasource for every repeated cause — an operator is misdirected on the second loss of any kind #15166 both returning as the firing control. Dedupe words for whoever files it:read-audit.ts·reportReadAuditWriteFailure·persistReadAuditRows·failureReported· third copy.packages/services/service-settings/src/config-change-audit.ts:157carries the same process-widefailureReportedshape, but it is not the same class: its callee is a bareeng.insertthat no register names, its first line already carriesCause:plus the real detail, and its remedy text is cause-agnostic (it explains thatplugin-auditis optional). An observation, not a contract violation. Successor: whoever takes theread-audit.tscard above, as the same sweep.Notes for review
DURABILITY_CRITICAL_CALLEESis untouched, per the card's fence.packages/specis untouched.login/logoutwriters on the auth session hooks, and attribute the unattributedlast_login_atupdate row #8144 casea failed ledger write is reported at ERROR, once, and never breaks the callerdrives the same cause twice, so it stays green under the new key unchanged — and leg C shows it is load-bearing.@objectstack/typesand itspaths/ vitest-alias entries were already added by PR fix(plugin-audit): report a lost audit row once per CAUSE, and name the real cause in the first line #17450, so no manifest or tsconfig change is owed forisMissingTableError.Generated by Claude Code