Skip to content

feat(audit): attribute MP writes to acting user via session context#64

Merged
chriskehayias merged 1 commit into
mainfrom
feat/mp-write-user-attribution
May 21, 2026
Merged

feat(audit): attribute MP writes to acting user via session context#64
chriskehayias merged 1 commit into
mainfrom
feat/mp-write-user-attribution

Conversation

@chriskehayias

Copy link
Copy Markdown
Contributor

Summary

  • MP write APIs (createTableRecords / updateTableRecords / deleteTableRecords) were not receiving $userId, so every Contact_Log and Contacts write was attributed to the OAuth integration account in MP's audit log rather than the user who performed the action. The MPHelper layer supported $userId; the gap was at the call sites.
  • This PR closes that gap with a small, loosely-coupled service at the boundary, plus a graceful + observable contract for anonymous writes.

Changes

Session enrichment (src/lib/auth.ts)

  • customSession now resolves the acting user's MP User_ID from session.user.userGuid via a process-wide userGuid → User_ID cache and attaches it as session.user.userId. Lookup failures are logged but never block session creation — the JWT cookie cache means the resolution happens at most once per (user × container).

New SessionContextService (src/services/sessionContextService.ts)

  • getCurrentUserId() — pure read.
  • getActingUserIdForWrite({ table, operation }) — returns the resolved User_ID or null, and when null emits a structured mp.write.non_user warn to logs so anonymous / system writes are surfaced in production observability (Vercel, log aggregators) without hard-failing the request.

Write call sites wired up

  • ContactLogService.createContactLog / updateContactLog / deleteContactLog
  • ContactService.updateContact

Each resolves $userId via SessionContextService.getActingUserIdForWrite(...) and forwards it to MPHelper only when non-null.

Design note: graceful + log, not hard fail

Anonymous writes are treated as legitimate (the app may serve unauthenticated users in the future) but are always observable. The structured log key event: "mp.write.non_user" is stable on purpose so anyone can grep / alert on unattributed writes.

Test plan

  • npm run lint — clean
  • npm run test:run — 272 / 272 passing (43 in the touched / new files)
  • New sessionContextService.test.ts covers: resolved user → returns id and no warn; no session → null + structured warn with correct table/operation; getSession throws → null + warn; pure-read path never warns.
  • Existing write tests in contactLogService.test.ts and contactService.test.ts updated to assert { $userId } is forwarded on the authenticated path and omitted on the anonymous path.
  • Manually verify in MP dp_Audit_Log that a Contact_Log create from a signed-in session shows the user's User_ID rather than the integration account (post-deploy).

🤖 Generated with Claude Code

MP write APIs were not receiving $userId, so every Contact_Log and Contacts
write was recorded in MP's audit log under the OAuth integration account
rather than the user who performed the action. The MPHelper layer supported
$userId on create/update/delete; the gap was at the call sites.

- Resolve MP User_ID in customSession from session.user.userGuid via a
  process-wide cache, attach to session.user.userId. Failures are logged
  but never block session creation.
- New SessionContextService.getActingUserIdForWrite({ table, operation })
  returns the userId or null. When null, emits a structured
  mp.write.non_user warn so anonymous/system writes are visible in
  production logs without hard-failing the request (the app may serve
  unauthenticated users in the future).
- ContactLogService.createContactLog / updateContactLog / deleteContactLog
  and ContactService.updateContact now resolve and forward $userId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chriskehayias chriskehayias merged commit 9fc6427 into main May 21, 2026
1 check passed
@chriskehayias chriskehayias deleted the feat/mp-write-user-attribution branch May 21, 2026 11:15
@codecov

codecov Bot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

1 participant