Background
PR #3715 (Phase 2a) introduced id-swap in the auth middleware: when a non-primary identity binding signs in, `req.user.id` is swapped to the canonical workos_user_id and the actual auth credential is preserved on `req.user.authWorkosUserId`.
Audit log inserts that record `req.user!.id` as the actor now record the canonical identity, not the credential that actually performed the action. Forensically, we can't tell which of a person's bound emails was used.
Sites known
From the security review:
- `server/src/routes/admin/accounts-billing.ts:732`
- `server/src/http.ts:7565,7641,7694`
- `server/src/db/user-merge-db.ts:614` (already takes `mergedBy` from caller, not from req — verify)
There may be others; grep for `registry_audit_log` inserts that pull `req.user.id`.
Why deferred from Phase 2a
Today no users have non-singleton identities — Phase 2b's admin "create + bind" tool will create the first ones. Until then, `authWorkosUserId` is always undefined and the audit gap is dormant. We should land this before Phase 2b makes the first non-singleton binding.
Proposed fix
For each audit insert site that uses `req.user!.id` as the actor or in details, also include `req.user!.authWorkosUserId` in the `details` JSON when present. Leave the column `workos_user_id` (= canonical) unchanged so existing queries continue to work.
Acceptance
Background
PR #3715 (Phase 2a) introduced id-swap in the auth middleware: when a non-primary identity binding signs in, `req.user.id` is swapped to the canonical workos_user_id and the actual auth credential is preserved on `req.user.authWorkosUserId`.
Audit log inserts that record `req.user!.id` as the actor now record the canonical identity, not the credential that actually performed the action. Forensically, we can't tell which of a person's bound emails was used.
Sites known
From the security review:
There may be others; grep for `registry_audit_log` inserts that pull `req.user.id`.
Why deferred from Phase 2a
Today no users have non-singleton identities — Phase 2b's admin "create + bind" tool will create the first ones. Until then, `authWorkosUserId` is always undefined and the audit gap is dormant. We should land this before Phase 2b makes the first non-singleton binding.
Proposed fix
For each audit insert site that uses `req.user!.id` as the actor or in details, also include `req.user!.authWorkosUserId` in the `details` JSON when present. Leave the column `workos_user_id` (= canonical) unchanged so existing queries continue to work.
Acceptance