Skip to content

feat(slack): daily channel-privacy audit backstop (#2849)#2924

Merged
bokelley merged 2 commits into
mainfrom
bokelley/channel-privacy-audit
Apr 23, 2026
Merged

feat(slack): daily channel-privacy audit backstop (#2849)#2924
bokelley merged 2 commits into
mainfrom
bokelley/channel-privacy-audit

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Closes #2849 — backstop for #2735.

#2735 catches channel-privacy drift at send time. A channel that flipped private → public stops receiving sensitive posts on the first send after the drift. What it misses: channels that sit idle between writes. This job runs once a day, checks each of the six configured admin-settings channels (billing / escalation / admin / prospect / error / editorial) against Slack, and emits a structured `channel_privacy_drift_audit` log.

What changed

  • New: `server/src/addie/jobs/channel-privacy-audit.ts`.
  • Registered: 24h interval / 10min initial delay via the existing `jobScheduler` in `job-definitions.ts`.
  • Notification policy:
  • Non-destructive: no auto-null of drifted settings. Enforcement is the send-time gate's job (Notification channels: recheck is_private at send time (TOCTOU) #2735). This audit is pure observability. A dedicated test pins this invariant so a future "helpful" refactor that clears the setting flips red.

Test plan

  • 9 unit-test scenarios cover: all private (no-op), unconfigured channels skipped, drift found → summary posted to admin, admin self-drift → summary suppressed, unknown states recorded separately, `verifyChannelStillPrivate` throwing collapses to 'unknown', no destructive writes.
  • `npm run typecheck` — clean
  • `npm run test:server-unit` — 1923 pass (+ one C2PA flake unrelated to this change; passes in isolation)
  • `npm run test:unit` — 631 pass

🤖 Generated with Claude Code

bokelley and others added 2 commits April 23, 2026 07:11
#2735 added a send-time recheck so a channel that drifted
private → public stops receiving sensitive posts on the first send
after the drift. Channels that sit idle between writes could linger
in a drifted state until someone tries to post, so the fix only
handled the hot path.

Adds a daily audit that proactively checks each of the six
admin-settings channels (billing, escalation, admin, prospect,
error, editorial) against Slack and emits a structured
`channel_privacy_drift_audit` log on drift or unverifiable states.
When drift is found, posts a summary to `admin_slack_channel` —
unless that channel itself drifted, in which case the summary is
suppressed so we don't leak drift details into the now-public
channel.

Non-destructive: the audit does NOT auto-null settings. The
send-time gate (#2735) is the enforcement path; this job is pure
observability. Log aggregation alerting should key on
`event: 'channel_privacy_drift_audit'`.

Registered with the existing jobScheduler at 24h interval / 10min
initial delay. Reuses `verifyChannelStillPrivate` + the
`sendChannelMessage({ requirePrivate: 'strict-public-only' })` mode
from #2861.

9 unit tests cover the orchestration logic: unconfigured channels
skipped, admin-channel self-drift suppresses the summary (the core
#2849 acceptance), throws collapse to 'unknown', non-destructive
behavior pinned against future refactors.

Typecheck clean, 1923 server unit tests pass, 631 root unit tests
pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All Should-Fix items from the code / security / testing reviewers:

- Suppress the admin summary when admin_slack_channel is 'unknown',
  not just 'public'. Security reviewer correctly flagged the
  'strict-public-only' mode lets 'unknown' through — narrow window
  where the admin channel could actually be public while Slack
  info returns unverifiable. The #2849 spec explicitly prioritizes
  "not using the drifted channel as the notification surface" and
  structured-log aggregation is the documented backstop. Now the
  summary is attempted only when admin is confirmed 'private' AND
  sent with the strict `requirePrivate: true` gate (belt-and-braces
  against a drift that raced between the audit loop and the send).
  (security Should-Fix)

- Sanitize the `err` payload in the verify-threw log — use
  `err instanceof Error ? .message : String(err)` so pg / library
  errors don't spill `.query` / `.parameters` through pino's default
  err serializer. Mirrors the pattern from #2830. (code Should-Fix)

- Tighten `shouldLogResult` type in job-definitions.ts by importing
  the exported `ChannelPrivacyAuditResult` instead of the inline
  structural shape. (code Should-Fix)

- Spy on the logger in tests and assert the structured
  `channel_privacy_drift_audit` record fires even when the summary
  is suppressed — it's the only remaining alert signal in those
  cases and wasn't being verified. (testing Should-Fix)

- Pin the admin-'unknown' + billing-drift branch with an explicit
  test: confirms the summary is suppressed, both bucket assignments
  are correct, and the structured log captures the drift.
  (testing Should-Fix)

- New test for multi-drift fan-out (billing + editorial both drifted
  → single summary mentioning both) to guard against a future
  refactor that sends per-channel posts. (testing Nice-to-Have)

- New test for admin + billing both drifted → summary suppressed,
  structured log still carries BOTH settings. Distinct from the
  admin-only case because it proves suppression doesn't accidentally
  drop billing from the drifted array. (testing Nice-to-Have)

- Renamed the non-destructive test for clarity: "does not write to
  the drifted setting — audit is pure observability". (testing
  Nice-to-Have)

12 scenarios total (up from 9), 1932 server + 631 root unit tests
pass, typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley

Copy link
Copy Markdown
Contributor Author

Expert pass complete — no Must-Fix. All Should-Fix addressed:

Security:

  • Admin summary now suppressed on 'unknown' too, not just 'public'. Security reviewer correctly flagged that 'strict-public-only' lets 'unknown' through, which was a narrow leak window if Slack's info endpoint was failing exactly during this run. Spec prioritizes "not using the drifted channel as notification surface"; structured log remains the backstop.

Code:

Testing:

  • Logger spy added; the admin-self-drift case now asserts the structured channel_privacy_drift_audit record still fires (primary signal when summary is suppressed).
  • New test: admin in 'unknown' + billing drifted → summary suppressed (pins the new decision).
  • New test: admin + billing both drifted → summary suppressed, structured log captures both (proves drifted array integrity).
  • New test: multi-drift fan-out (billing + editorial) → single summary mentioning both.
  • Renamed non-destructive test for clarity.

12 scenarios total (up from 9). 1932 server + 631 root unit tests pass, typecheck clean.

@bokelley bokelley merged commit 99690e2 into main Apr 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Periodic audit job for admin-settings channel privacy (#2735 backstop)

1 participant