Skip to content

fix(plugin-audit): key the lost auth-event row report per CAUSE, and name the real cause in its first line - #18246

Merged
claude[bot] merged 3 commits into
mainfrom
claude/issue-17452-auth-event-audit-second-copy
Sep 15, 2026
Merged

claude[bot] merged 3 commits into
mainfrom
claude/issue-17452-auth-event-audit-second-copy

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #17452

Clause-②: no

What this is

packages/plugins/plugin-audit/src/auth-event-audit.ts — the writer behind the login / logout rows of the compliance ledger — carried a second, independent copy of both defects that #15166 removed from audit-writers.ts:

  1. 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;
  2. its own fixed message literal, printing the ADR-0057 §3.6 / OS_TELEMETRY_DB datasource guidance unconditionally, regardless of what actually failed.

persistAuthEventAuditRow is registered in DURABILITY_CRITICAL_CALLEES (scripts/check-durability-degradation-log-level.mjs:361), whose entire purpose is that durability loss is reported at error. 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/main at b3b43b6ea (this branch's base), they had not:

audit-writers.ts auth-event-audit.ts (before)
reporter reportAuditWriteFailure reportAuthEventWriteFailure
dedupe state reportedAuditFailureCauses (a Set) failureReported (a boolean)
message cause-led, conditional remedy one fixed literal
logger (engine as any).logger injected AuthEventAuditLogger, error OPTIONAL
tables written sys_audit_log + sys_activity sys_audit_log only

So 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.tsauditFailureCauseKey and auditFailureCauseSummary become module exports so the auth-event sink can use them. ⛔ Not added to src/index.ts: the sharing is internal to the package. This mirrors the existing createFieldPresenceProbe import that already crosses these two files.
  • auth-event-audit.ts — the boolean becomes reportedAuthEventFailureCauses, a Set keyed by auditFailureCauseKey(SESSION_OBJECT, err). The first error line leads with auditFailureCauseSummary(err, detail) — the code and message that were already computed one line above the branch and passed only into the debug payload. The datasource guidance is kept and made conditional on isMissingTableError(err, 'sys_audit_log'), asked for the one table this writer writes.
  • auth-event-audit.test.ts — 7 new cases (see below).
  • A patch changeset.

⛔ 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 3985dd2bb is the pin block alone, on top of unmodified source. pnpm --filter @objectstack/plugin-audit exec vitest run src/auth-event-audit.test.ts:

Tests  4 failed | 10 passed (14)

FAIL  reports a SECOND, DIFFERENT cause at error — a new cause is a new degradation
  AssertionError: expected [ { level: 'error', …(2) } ] to have a length of 2 but got 1

FAIL  prints the datasource remedy for the cause it is the remedy FOR, and not for others
  AssertionError: expected 'Auth-event audit write FAILED — the c…' not to match /OS_TELEMETRY_DB/
  Received: "… Fix: confirm `sys_audit_log` is reachable … Set `OS_TELEMETRY_DB=0` …"
  (the cause driven through the reporter was ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED)

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 typecheck exit 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 -c on the anchor text and on the injected text, plus the on-disk blob hash moving), runs the suite, then restores with git checkout HEAD -- path and proves git hash-object equals the HEAD blob and git diff HEAD is empty. HEAD blob ba07853195ca57faf56734d32e005a836cd0aae2 before and after all three. No build/dist leg is owed: the suite imports ./auth-event-audit.js relatively and vitest.config.ts aliases @objectstack/types to source, so nothing on the tested path resolves through dist.

leg mutation anchor→inject result
A cause key collapsed to one constant bucket (= the old boolean) 1→0 / 0→1, hash 31b8e4c… 2 failedreports a SECOND, DIFFERENT cause at error, [#9657] the warn fallback is per-cause too
B missingTable forced to true (= the old unconditional hint) 1→0 / 0→1, hash 13edd5b… 1 failedprints the datasource remedy for the cause it is the remedy FOR, and not for others
C dedupe removed entirely (the named falsifier) 1→0 / 0→1, hash 7d998a1… 4 failed — the three anti-noise controls, plus the pre-existing #8144 reported at ERROR, once case

The 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 ⇒ 1 error + 4 debug, and the debug lines carry the same cause key.
  • 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 ⇒ 1 error.
  • folds a fault carrying NO code into ONE bucket rather than growing one — 200 sign-ins, no code at all ⇒ 1 error.

⇒ 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.ts is untouched (git diff origin/main --stat on it is empty), and tsup builds the single entry src/index.ts.

  • .d.ts export list of the published entry (dist/index.d.ts): 27 names, neither auditFailureCauseKey nor auditFailureCauseSummary among them.
  • runtime probe, name in await import('dist/index.mjs') (the path exports["."] names):
auditFailureCauseKey          not reachable
auditFailureCauseSummary      not reachable
createAuthEventAuditSink      REACHABLE       [POSITIVE CONTROL]
installAuditWriters           REACHABLE       [POSITIVE CONTROL]
thisSymbolDoesNotExist        not reachable   [NEGATIVE CONTROL]
total runtime exports: 11

Clause-②: no, derived by reachability rather than by the word export. The changeset is graded patch.

Gate denominator

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 families at HEAD d20bd8765. All 65 were run with exit codes recorded before any pipe, and reconciled:

Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN.
  EXIT CODES — all 65 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them.
✓ dispatch-gates --ran: 65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED
  (a DERIVED zero — all 65 recorded an exit code and none of them is 3).

Three first answered exit 3, PREREQUISITE NOT METcheck:i18n, check:dual-build-cjs-loads, check:type-check-debt. ⛔ Not read as green: their prerequisite closures were built (turbo run build over 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…) and check-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-level exit 0 — ✓ durability-degradation log levels: 36 durability-critical catch seam(s), all loud….

eslint — a declared narrowing, with its three readings

Repo-wide pnpm lint is CI's run. Narrowed here to the three source files this diff touches, at HEAD d20bd8765:

① Population read from eslint's own eslint.config.mjs, not guessed: the base block is files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] with further packages/**/*.{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.mjs states at its own line 328 that this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) 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:

Notes for review


Generated by Claude Code

…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-audit, touching 4 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/production-readiness.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/kernel/runtime-services/audit-service.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/permissions/record-view-auditing.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/plugins/packages.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/ui/setup-app.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))

4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/index.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/releases/v14.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/releases/v17/17-0.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))
  • content/docs/releases/v17/17-1.mdx (via sys_audit_log (literal, a string literal in createAuthEventAuditSink))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 9 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 b3b43b6ea0c30f35d861c9cc0fedc9c085027b7bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 0c7232a313ed46bc8a53ceb304d4eb29bd966f74 — the merge of head d20bd87650185735e45342e52a63686bf7f94118 into base b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant