componentsfeat(auth): make the two-factor setup code scannable Enrollment handed out a base32 setup key to type by hand — every authenticator app takes one, but nobody wants to. The otpauth URI was already in client state; this encodes it. Scanning becomes the primary path and the key moves behind a disclosure, since a password manager on the same screen as the QR has no camera. The QR paints its own white ground: Cedar renders dark by default and an inverted code will not scan. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 9:54 AMhooksfix(admin-view): an agent's files open for the user being viewed, not the staff member Under Cedar-staff admin view every tRPC request carries X-Admin-View-User and the server swaps ctx.sessionUser to the impersonated user. The session is the one thing that does not move with it: better-auth talks to /auth/get-session, which the impersonation middleware explicitly skips. So a `user` file scope built from useSession() asked, AS the impersonated user, for the staff member's own workspace — a cross-user request assertAuthorizedScope correctly refuses. The refusal is a tRPC FORBIDDEN that nothing logs, so the symptom was an agent's Files tab that was simply empty against a clean server. Replaces the eight hand-rolled answers to "who am I" with useViewerUserId. It deliberately does not read targetUserId: admin view is a property of the whole client, so reading it ambiently is safe, while the org-admin picker is a property of one surface and an ambient read of it is the documented failure on useScopedInput. Fixes the same class where the viewer id was not merely cosmetic — it also drives isOtherPersonsAgent on the deal Files tab and the task-ownership split, so the viewed user's own agents and follow-ups all read as a teammate's. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 12:20 PMutilsfeat(security): make the security settings page mean what it says Three controls on Settings -> Security were wired to nothing. The two-factor switch ran a setTimeout that console.logged its values; the login-notifications switch had no storage, no trigger and no sender behind its fourteen translations; Delete Account had no onClick at all. A user could turn 2FA on and believe they had it. Two-factor, for an explicit allowlist (currently one user, at his organization's request). better-auth's twoFactor plugin alone would have recorded 2FA without ever enforcing it: emailAndPassword is disabled and every sign-in call site is signIn.social, but the plugin's hook matcher covers only /sign-in/{email,username, phone-number}, so the Google OAuth callback is never challenged. The plugin is registered for its primitives only -- TOTP secrets, encryption at rest, constant-time verify, single-use recovery codes, none of which should be hand-rolled -- and the enforcement is Cedar's own gate in http/app.ts, keyed on a per-session marker. Per-session so that a new sign-in is challenged again and revoking a session revokes its clearance with it. Two traps are documented where they bite: the marker is read from the database because the 30-day session cookie cache would otherwise report a stale value forever, and the first successful verification rotates the session while echoing back the PRE-rotation token. Enrolling also enforces, or the settings toggle would be decoration for anyone off the allowlist. The asymmetry is deliberate -- un-enrolling opts you out only if the allowlist was not what put you there -- and /two-factor/disable is rejected by the gate for required users, not merely hidden in the UI. Login notifications fire on a device not seen before, not on every sign-in. Comparing raw user_agent strings looks correct and is not: one live account shows 46 sessions across 6 user agents but only 3 real devices, because Chrome 148, 149, 150 and 151 each got their own row. That version would have sent three false "new device" warnings for routine auto-updates, and an alert that cries wolf monthly is one nobody reads. It compares a digit-stripped signature, tested against those exact strings. Delivered over email rather than the primary channel, which can be Slack reached with the very Google account the alert is about. Default off: defaulting it true would start mailing every existing user the moment this deploys. Deploy order matters. two_factor_auth.sql is additive and must land BEFORE this code -- registering the plugin makes better-auth select two_factor_enabled on every user query, so a missing column is a full outage, not a degraded feature. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 27, 2026, 11:36 PM