You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(auth): thread the real better-auth actor into identity writes for attribution (#4586)
better-auth owns every write to the identity tables and its ObjectQL adapter
runs them `isSystem: true` on purpose — the route already authorized the
action under better-auth's own ACL. The human who clicked *make admin* was
known exactly once, in the hook layer, then discarded, so every
`trackHistory` transition on `sys_member` recorded "system" as its actor.
W1 — a general seam, not a `sys_member` special case: a request-scoped
attribution store opened at `AuthManager.handleRequest`, filled lazily from
better-auth's global before-hook, surfaced as
`ExecutionContext.attributedUserId` → `HookContext.provenance.attributedUserId`
and read by the audit writer.
W2 — `auto-org-admin-grant` stamps the attributed human into the `granted_by`
column it always wrote null into, plus a machine-provenance `reason` naming
the writer and the triggering `sys_member` row.
W3 — covered at the real routes (invite-accept, update-member-role, the
reconciler bind, demotion) in a dogfood test over the live HTTP stack.
ATTRIBUTION ONLY: the threaded actor never becomes the authorization subject.
It rides `provenance`, which no security middleware reads; `isSystem` stays
the unconditional authorization half. Re-authorizing as the human would open
the second adjudication track ADR-0095 D3 closed — pinned by tests at the
engine seam, the adapter, and the live route.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
0 commit comments