@terreno/mcp 57.19.2 → 57.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +393 -0
- package/README.md +32 -269
- package/dist/bootstrap.d.ts +12 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +77 -122
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
- package/dist/docs/resources/overview.md +4 -2
- package/dist/docs/resources/patterns.md +237 -176
- package/dist/docs/resources/ui.md +48 -17
- package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
- package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
- package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
- package/dist/docs/upgrades/57.20.0.md +238 -0
- package/dist/docs/versioned/next/approval-rubric.md +30 -12
- package/dist/docs/versioned/next/explanation/README.md +5 -0
- package/dist/docs/versioned/next/explanation/account-linking.md +169 -0
- package/dist/docs/versioned/next/explanation/admin-interface.md +1 -1
- package/dist/docs/versioned/next/explanation/agent-guidelines.md +173 -0
- package/dist/docs/versioned/next/explanation/ai-development-loop.md +196 -0
- package/dist/docs/versioned/next/explanation/authentication.md +45 -1
- package/dist/docs/versioned/next/explanation/consent-forms.md +105 -0
- package/dist/docs/versioned/next/explanation/example-coverage.md +1 -0
- package/dist/docs/versioned/next/explanation/local-first-data.md +1 -1
- package/dist/docs/versioned/next/explanation/organizations.md +37 -0
- package/dist/docs/versioned/next/explanation/planning-plugins.md +4 -0
- package/dist/docs/versioned/next/explanation/responsive-design.md +143 -0
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
- package/dist/docs/versioned/next/explanation/schema-metadata.md +5 -0
- package/dist/docs/versioned/next/how-to/README.md +7 -2
- package/dist/docs/versioned/next/how-to/audit-log.md +22 -3
- package/dist/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
- package/dist/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
- package/dist/docs/versioned/next/how-to/circleci.md +15 -5
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +673 -53
- package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
- package/dist/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
- package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
- package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
- package/dist/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +37 -8
- package/dist/docs/versioned/next/how-to/set-up-social-login.md +424 -0
- package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
- package/dist/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
- package/dist/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
- package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
- package/dist/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
- package/dist/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
- package/dist/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
- package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
- package/dist/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
- package/dist/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
- package/dist/docs/versioned/next/reference/README.md +1 -0
- package/dist/docs/versioned/next/reference/account-security.md +700 -0
- package/dist/docs/versioned/next/reference/admin-backend.md +3 -1
- package/dist/docs/versioned/next/reference/admin-frontend.md +225 -1
- package/dist/docs/versioned/next/reference/api.md +461 -35
- package/dist/docs/versioned/next/reference/cli.md +1 -1
- package/dist/docs/versioned/next/reference/comms.md +8 -0
- package/dist/docs/versioned/next/reference/create-terreno-app.md +3 -1
- package/dist/docs/versioned/next/reference/environment-variables.md +9 -3
- package/dist/docs/versioned/next/reference/legacy/rtk.md +19 -3
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
- package/dist/docs/versioned/next/reference/mcp-server.md +1158 -441
- package/dist/docs/versioned/next/reference/syncdb.md +8 -1
- package/dist/docs/versioned/next/reference/ui.md +1271 -27
- package/dist/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
- package/dist/docs/versioned/next/tutorials/README.md +4 -0
- package/dist/httpApp.d.ts +14 -0
- package/dist/httpApp.d.ts.map +1 -0
- package/dist/httpApp.js +99 -0
- package/dist/httpApp.js.map +1 -0
- package/dist/index.js +8 -90
- package/dist/index.js.map +1 -1
- package/dist/local/localTools.d.ts.map +1 -1
- package/dist/local/localTools.js +44 -14
- package/dist/local/localTools.js.map +1 -1
- package/dist/local/metro/metroDevSession.d.ts +18 -0
- package/dist/local/metro/metroDevSession.d.ts.map +1 -1
- package/dist/local/metro/metroDevSession.js +42 -3
- package/dist/local/metro/metroDevSession.js.map +1 -1
- package/dist/local/tools/databaseQuery.d.ts.map +1 -1
- package/dist/local/tools/databaseQuery.js +3 -13
- package/dist/local/tools/databaseQuery.js.map +1 -1
- package/dist/local/tools/databaseSchema.d.ts.map +1 -1
- package/dist/local/tools/databaseSchema.js +3 -13
- package/dist/local/tools/databaseSchema.js.map +1 -1
- package/dist/local/tools/localMongo.d.ts +10 -0
- package/dist/local/tools/localMongo.d.ts.map +1 -0
- package/dist/local/tools/localMongo.js +30 -0
- package/dist/local/tools/localMongo.js.map +1 -0
- package/dist/local/tools/runtime.d.ts +18 -2
- package/dist/local/tools/runtime.d.ts.map +1 -1
- package/dist/local/tools/runtime.js +75 -20
- package/dist/local/tools/runtime.js.map +1 -1
- package/dist/local/tools/syncDb.d.ts +11 -0
- package/dist/local/tools/syncDb.d.ts.map +1 -1
- package/dist/local/tools/syncDb.js +15 -3
- package/dist/local/tools/syncDb.js.map +1 -1
- package/dist/rateLimit.d.ts +35 -0
- package/dist/rateLimit.d.ts.map +1 -0
- package/dist/rateLimit.js +112 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +42 -60
- package/dist/tools.js.map +1 -1
- package/package.json +4 -3
- package/src/docs/guidelines/ui/guidelines/core.md +1 -1
- package/src/docs/resources/overview.md +4 -2
- package/src/docs/resources/patterns.md +237 -176
- package/src/docs/resources/ui.md +48 -17
- package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
- package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
- package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
- package/src/docs/upgrades/57.20.0.md +238 -0
- package/src/docs/versioned/next/approval-rubric.md +30 -12
- package/src/docs/versioned/next/explanation/README.md +5 -0
- package/src/docs/versioned/next/explanation/account-linking.md +169 -0
- package/src/docs/versioned/next/explanation/admin-interface.md +1 -1
- package/src/docs/versioned/next/explanation/agent-guidelines.md +173 -0
- package/src/docs/versioned/next/explanation/ai-development-loop.md +196 -0
- package/src/docs/versioned/next/explanation/authentication.md +45 -1
- package/src/docs/versioned/next/explanation/consent-forms.md +105 -0
- package/src/docs/versioned/next/explanation/example-coverage.md +1 -0
- package/src/docs/versioned/next/explanation/local-first-data.md +1 -1
- package/src/docs/versioned/next/explanation/organizations.md +37 -0
- package/src/docs/versioned/next/explanation/planning-plugins.md +4 -0
- package/src/docs/versioned/next/explanation/responsive-design.md +143 -0
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
- package/src/docs/versioned/next/explanation/schema-metadata.md +5 -0
- package/src/docs/versioned/next/how-to/README.md +7 -2
- package/src/docs/versioned/next/how-to/audit-log.md +22 -3
- package/src/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
- package/src/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
- package/src/docs/versioned/next/how-to/circleci.md +15 -5
- package/src/docs/versioned/next/how-to/configure-better-auth.md +673 -53
- package/src/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
- package/src/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
- package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
- package/src/docs/versioned/next/how-to/password-reset.md +1 -1
- package/src/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +37 -8
- package/src/docs/versioned/next/how-to/set-up-social-login.md +424 -0
- package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
- package/src/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
- package/src/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
- package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
- package/src/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
- package/src/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
- package/src/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
- package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
- package/src/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
- package/src/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
- package/src/docs/versioned/next/reference/README.md +1 -0
- package/src/docs/versioned/next/reference/account-security.md +700 -0
- package/src/docs/versioned/next/reference/admin-backend.md +3 -1
- package/src/docs/versioned/next/reference/admin-frontend.md +225 -1
- package/src/docs/versioned/next/reference/api.md +461 -35
- package/src/docs/versioned/next/reference/cli.md +1 -1
- package/src/docs/versioned/next/reference/comms.md +8 -0
- package/src/docs/versioned/next/reference/create-terreno-app.md +3 -1
- package/src/docs/versioned/next/reference/environment-variables.md +9 -3
- package/src/docs/versioned/next/reference/legacy/rtk.md +19 -3
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
- package/src/docs/versioned/next/reference/mcp-server.md +1158 -441
- package/src/docs/versioned/next/reference/syncdb.md +8 -1
- package/src/docs/versioned/next/reference/ui.md +1271 -27
- package/src/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
- package/src/docs/versioned/next/tutorials/README.md +4 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
# Auth experience: social login, account linking, passkeys, 2FA, and admin recovery
|
|
2
|
+
|
|
3
|
+
Terreno already runs Better Auth with email/password, and `BetterAuthConfig` has slots
|
|
4
|
+
for Google, GitHub, and Apple. On every deployment, though, social login is off, the
|
|
5
|
+
native OAuth return is broken, and the only plugin enabled is `bearer()`. No app has
|
|
6
|
+
passkeys, 2FA, linked-account management, or any way for an admin to help a locked-out
|
|
7
|
+
user. This plan turns auth into a set of features an app switches on in config.
|
|
8
|
+
Consumers get a login screen that knows how you signed in last and who is signed in on
|
|
9
|
+
this device. They also get a security screen for providers, passkeys, 2FA, sessions,
|
|
10
|
+
and password, plus recovery tools for org-admins and platform staff. The example app
|
|
11
|
+
turns on all of it, and it works on per-PR previews and on the stable deployment.
|
|
12
|
+
|
|
13
|
+
## The idea
|
|
14
|
+
|
|
15
|
+
- **One config contract.** `BetterAuthConfig` gains first-class `accountLinking`,
|
|
16
|
+
`twoFactor`, `passkey`, `lastLoginMethod`, `multiSession`, `oauthProxy`, and
|
|
17
|
+
`nativeApps` fields. Terreno maps them to Better Auth plugins, so consumers never
|
|
18
|
+
assemble plugins by hand.
|
|
19
|
+
- **Discovery.** A public endpoint, `GET {basePath}/terreno/methods`, reports what is
|
|
20
|
+
enabled. The login screen renders from it and never shows a provider that isn't
|
|
21
|
+
configured.
|
|
22
|
+
- **Three layers on the frontend:**
|
|
23
|
+
- Presentational screens and cards in `@terreno/ui`.
|
|
24
|
+
- Connected screens and hooks in `@terreno/rtk`, which already depends on ui.
|
|
25
|
+
- Recovery panels in `@terreno/admin-frontend`.
|
|
26
|
+
- **Recovery is Terreno-owned.** An `AccountSecurityApp` plugin adds per-user routes.
|
|
27
|
+
Platform staff reach them at `/users/:id/security/*`. An org-admin reaches the same
|
|
28
|
+
handlers at `/orgs/:orgId/members/:userId/security/*` for any active member of
|
|
29
|
+
their org (D2). Every action writes an `AuditEvent` through the existing `AuditApp`, with
|
|
30
|
+
`source: "security"`, and emails the affected user.
|
|
31
|
+
- **Builds on #1604.** Master already has web bearer mode (`createBetterAuthClient({webAuth:
|
|
32
|
+
"bearer"})`, `bearerStorage`), the OAuth one-time-token handoff (`oauthHandoff`,
|
|
33
|
+
`signInSocialWithHandoff`), the mock OIDC provider (`MOCK_OIDC_ISSUER`), and the
|
|
34
|
+
cross-site bearer Playwright project. This plan reuses them instead of building its own
|
|
35
|
+
(D7).
|
|
36
|
+
- **Previews.** Previews sign in through Better Auth's `oauth-proxy`. The stable
|
|
37
|
+
deployment, called "staging" here (D1), owns the registered callback URLs and
|
|
38
|
+
forwards each OAuth return to the preview that started it.
|
|
39
|
+
- **Rejected alternative:** Better Auth's `admin` and `organization` plugins. Terreno
|
|
40
|
+
already owns orgs, RBAC, and `User.disabled`, and a second authority would drift.
|
|
41
|
+
|
|
42
|
+
## The plan
|
|
43
|
+
|
|
44
|
+
| # | Task | Depends on | Lands in | Proves it |
|
|
45
|
+
| --- | --- | --- | --- | --- |
|
|
46
|
+
| T1 | Methods discovery drives the example login buttons (tracer) | none | api, rtk, example-frontend | api test + Playwright |
|
|
47
|
+
| T2 | Auth feature config contract and plugin wiring | T1 | api | api tests |
|
|
48
|
+
| T3 | Account linking modes and verified-email sync | T2 | api | api tests (stubbed provider) |
|
|
49
|
+
| T4 | Example backend enables the full suite; the mock OIDC provider joins e2e | T2 | example-backend, e2e | api test + Playwright |
|
|
50
|
+
| T5 | Linking a provider works for bearer-mode web clients | T4 | api, rtk | api tests |
|
|
51
|
+
| T27 | The 2FA, trust-device and passkey challenges work without cookies | T5, T18 | api, rtk, example-frontend | api test + Playwright cross-site bearer |
|
|
52
|
+
| T6 | Security summary, 2FA reset, security audit events, platform routes | T2 | api | api tests |
|
|
53
|
+
| T7 | Recovery: remove passkey, unlink provider | T6 | api | api tests |
|
|
54
|
+
| T8 | Recovery: revoke sessions, send reset, disable/enable | T7 | api, example-backend | api tests |
|
|
55
|
+
| T9 | Org-admin scoped recovery routes | T6 | api | api tests (auth matrix) |
|
|
56
|
+
| T10 | Security notification emails | T8, T27 | api, comms | api + comms tests |
|
|
57
|
+
| T11 | Previews sign in through oauth-proxy on the stable deploy | T5 | example-backend, CI, infra, docs | config test, terraform validate |
|
|
58
|
+
| T28 | Live social sign-in on a preview and on the stable deploy (needs H1) | T11 | e2e | live curl + opt-in Playwright |
|
|
59
|
+
| T12 | LoginScreen v2 (presentational) | none | ui, demo | ui tests + snapshots |
|
|
60
|
+
| T13 | OtpField and TwoFactorChallengeScreen | none | ui, demo | ui tests |
|
|
61
|
+
| T14 | LinkedAccountsCard and PasswordCard | none | ui, demo | ui tests |
|
|
62
|
+
| T15 | PasskeysCard and SessionsCard | none | ui, demo | ui tests |
|
|
63
|
+
| T16 | TwoFactorSettingsCard (enroll, backup codes) | T2, T13 | ui, demo | ui tests |
|
|
64
|
+
| T17 | AccountSecurityScreen, exports, demo registration | T12, T14, T15, T16 | ui, demo, docs | ui tests + demo coverage |
|
|
65
|
+
| T18 | rtk client feature plugins | T2 | rtk | rtk tests |
|
|
66
|
+
| T19 | Remembered accounts on device | none | rtk | rtk tests |
|
|
67
|
+
| T20 | Connected TerrenoLoginScreen and login controller | T3, T11, T17, T19, T27 | rtk | rtk tests |
|
|
68
|
+
| T21 | Connected TerrenoAccountSecurityScreen | T20 | rtk | rtk tests |
|
|
69
|
+
| T22 | Example login, 2FA challenge, passkey sign-in | T4, T20 | example-frontend | Playwright |
|
|
70
|
+
| T29 | Android example signs in with the mock provider | T22 | example-frontend (Maestro) | Maestro on emulator |
|
|
71
|
+
| T23 | Example security screen | T21, T22 | example-frontend | Playwright |
|
|
72
|
+
| T24 | Org-admin member security panel | T4, T9 | admin-frontend, example-frontend | admin tests + Playwright |
|
|
73
|
+
| T25 | Platform user security screen | T8, T24 | admin-frontend, example-frontend | admin tests + Playwright |
|
|
74
|
+
| T26 | create-terreno-app generates the auth suite | T4, T21 | create-terreno-app | generator tests |
|
|
75
|
+
|
|
76
|
+
Tracer: config, then `/terreno/methods`, then `useAuthMethods`, then the example login
|
|
77
|
+
buttons.
|
|
78
|
+
|
|
79
|
+
Out of scope: dedicated staging environment, iOS native proof, native Apple button,
|
|
80
|
+
native passkeys, org security policies, magic link / email OTP / One Tap, enterprise
|
|
81
|
+
SSO, and self-service email change or account deletion. The last two are deliberate
|
|
82
|
+
cuts from "all of it" (X6). See Expansions.
|
|
83
|
+
|
|
84
|
+
Open risks:
|
|
85
|
+
- **Apple through oauth-proxy.** Apple returns with `form_post`; whether the proxy
|
|
86
|
+
handles that is unproven.
|
|
87
|
+
- **Safari.** Safari blocks third-party cookies. After H3 the stable sites are
|
|
88
|
+
same-site (D6), but previews stay on `netlify.app` and `run.app`. #1604's bearer mode
|
|
89
|
+
removes the dependence on cookies for sessions; T27 does the same for the 2FA and
|
|
90
|
+
passkey challenges.
|
|
91
|
+
- **Revocation delay.** The 5-minute session cookie cache delays revocation for
|
|
92
|
+
cookie (not bearer) clients.
|
|
93
|
+
- **Credentials.** Real provider credentials need a human (H1).
|
|
94
|
+
|
|
95
|
+
## Human prerequisites
|
|
96
|
+
|
|
97
|
+
- **H1.** Create the OAuth apps and add their secrets with `gcloud secrets versions add`
|
|
98
|
+
to the containers T11 creates:
|
|
99
|
+
- a GitHub OAuth app;
|
|
100
|
+
- a Google OAuth client;
|
|
101
|
+
- an Apple Services ID with a Sign in with Apple key (team ID, key ID, `.p8`).
|
|
102
|
+
|
|
103
|
+
Register exactly one callback per provider, on the stable backend's final host:
|
|
104
|
+
`https://api.<domain>/api/auth/callback/{provider}` once H3 is done (D6). Before H3,
|
|
105
|
+
register `https://terreno-backend-example-7knxlrnpqq-uc.a.run.app/api/auth/callback/{provider}`
|
|
106
|
+
too, and drop it after the move. Apple's Services ID also lists `api.<domain>` and
|
|
107
|
+
`app.<domain>` under Domains.
|
|
108
|
+
Also create a GitHub test account without 2FA for the opt-in live Playwright run.
|
|
109
|
+
Only T28 waits on this; every other task proves itself with the mock
|
|
110
|
+
provider from T4.
|
|
111
|
+
- **H3.** Move the stable sites onto one domain (D6): the Netlify custom domain
|
|
112
|
+
`app.<domain>` for `terreno-frontend`, and a Cloud Run domain mapping `api.<domain>`
|
|
113
|
+
for `terreno-backend-example`. Then set `STABLE_FRONTEND_URL` and `STABLE_BACKEND_URL`
|
|
114
|
+
in CI (T11). Nothing waits on this: until then every URL defaults to today's hosts
|
|
115
|
+
and T27 covers the cross-site gap.
|
|
116
|
+
- **H2.** After T28 passes, sign in once with Google and once with Apple on a preview
|
|
117
|
+
and on the stable deploy. These providers block automated sign-in.
|
|
118
|
+
|
|
119
|
+
## Tasks
|
|
120
|
+
|
|
121
|
+
- [x] **T1** — Methods discovery drives the example login buttons
|
|
122
|
+
- Depends on: none
|
|
123
|
+
- Files: api/src/betterAuthMethods.ts, api/src/betterAuthMethods.test.ts, api/src/betterAuthSetup.ts, rtk/src/useAuthMethods.ts, rtk/src/useAuthMethods.test.ts, rtk/src/index.ts, example-frontend/app/login.tsx, example-frontend/e2e/login.spec.ts, docs/reference/api.md, changelog/unreleased/auth-methods-discovery.md
|
|
124
|
+
- Build:
|
|
125
|
+
- Add a Better Auth plugin that serves a public `GET {basePath}/terreno/methods`
|
|
126
|
+
returning `{emailPassword, signUp, providers: string[], passkey, twoFactor, accountLinking, multiSession}`.
|
|
127
|
+
`providers` lists social providers and `genericOAuth` provider ids (so the
|
|
128
|
+
example's `mock-oidc` appears when `MOCK_OIDC_ISSUER` is set).
|
|
129
|
+
Flags that T2 has not built yet report `false`.
|
|
130
|
+
- Add `useAuthMethods(authClient)` in rtk.
|
|
131
|
+
- The example login replaces its hard-coded `["google","github","apple"]`
|
|
132
|
+
(`example-frontend/app/login.tsx:62`) with the result.
|
|
133
|
+
- Proof:
|
|
134
|
+
- `api/src/betterAuthMethods.test.ts` "lists only configured providers": with only
|
|
135
|
+
`githubOAuth` set, `providers` equals `["github"]` and the endpoint answers
|
|
136
|
+
without a session.
|
|
137
|
+
- `e2e/login.spec.ts` "user sees only configured providers": with no provider env,
|
|
138
|
+
`login-screen-oauth` has no buttons.
|
|
139
|
+
- Docs: `docs/reference/api.md` Better Auth section (endpoint + response).
|
|
140
|
+
- Skills: terreno-backend-api, terreno-data-fetching, verify-ui-changes
|
|
141
|
+
|
|
142
|
+
- [x] **T2** — Auth feature config contract and plugin wiring
|
|
143
|
+
- Depends on: T1
|
|
144
|
+
- Files: api/src/betterAuth.ts, api/src/betterAuthSetup.ts, api/src/betterAuthFeatures.ts, api/src/betterAuthFeatures.test.ts, api/src/betterAuthMethods.ts, api/src/rateLimit/policies.ts, api/src/rateLimit/policies.test.ts, api/package.json, rtk/package.json, ui/package.json, example-backend/package.json, example-frontend/package.json, package.json, bun.lock, docs/how-to/configure-better-auth.md, changelog/unreleased/auth-feature-config.md
|
|
145
|
+
- Build:
|
|
146
|
+
- Add these optional fields to `BetterAuthConfig`:
|
|
147
|
+
- `accountLinking: {mode: "verified-email" | "explicit" | "never", trustedProviders?}`
|
|
148
|
+
- `verified-email`: a sign-in whose verified email matches joins that account.
|
|
149
|
+
- `explicit`: the user links a method from the Security screen.
|
|
150
|
+
- `never`: no linking. Each sign-in method stays its own account, and a same-email
|
|
151
|
+
sign-in with a new method is refused.
|
|
152
|
+
- Better Auth's `requireLocalEmailVerified` stays `true` in every mode.
|
|
153
|
+
- `twoFactor: {issuer, backupCodes?, trustDeviceDays?}`
|
|
154
|
+
- `passkey: {rpID, rpName, origin: string[]}`
|
|
155
|
+
- `signUp: {mode: "open" | "closed"}` (default `open`). `closed` maps to Better
|
|
156
|
+
Auth `emailAndPassword.disableSignUp` plus `disableImplicitSignUp` on every
|
|
157
|
+
social provider, so only existing users can sign in (D4).
|
|
158
|
+
- `lastLoginMethod: boolean` (`storeInDatabase: true`)
|
|
159
|
+
- `multiSession: {maximumSessions}`
|
|
160
|
+
- `oauthProxy: {productionURL}`
|
|
161
|
+
- `nativeApps: boolean` (registers the `@better-auth/expo` server plugin)
|
|
162
|
+
- `appleOAuth` additionally accepts `{teamId, keyId, privateKey, appBundleIdentifier}`
|
|
163
|
+
and signs Apple's client-secret JWT at startup.
|
|
164
|
+
- `buildFeaturePlugins(config)` in `betterAuthFeatures.ts` maps each field to its
|
|
165
|
+
plugin:
|
|
166
|
+
- `twoFactor`, `passkey` (add `@better-auth/passkey` to the catalog)
|
|
167
|
+
- `lastLoginMethod`, `multiSession`, `oAuthProxy`, `expo`
|
|
168
|
+
- `account.accountLinking`
|
|
169
|
+
- The plugins are appended before `config.plugins`, which keeps the escape hatch
|
|
170
|
+
(#1592).
|
|
171
|
+
- The methods endpoint reports the real flags.
|
|
172
|
+
- Add every new dependency for the plan here, so one task owns the lockfile:
|
|
173
|
+
- `@better-auth/passkey` (catalog) in api and rtk;
|
|
174
|
+
- `react-native-qrcode-svg` in ui;
|
|
175
|
+
- `otpauth` in example-frontend dev dependencies (the mock OIDC provider from
|
|
176
|
+
#1604 replaces `oauth2-mock-server`).
|
|
177
|
+
- Add `/two-factor`, `/passkey`, `/link-social`, `/unlink-account`, `/multi-session`
|
|
178
|
+
to the `auth` rate-limit bucket (`/one-time-token` and `/oauth-handoff` are
|
|
179
|
+
already there from #1604; keep them).
|
|
180
|
+
- Proof:
|
|
181
|
+
- `betterAuthFeatures.test.ts` "each feature mounts only when configured": for each
|
|
182
|
+
feature, its endpoint is non-404 when enabled and 404 when absent. The endpoints
|
|
183
|
+
are:
|
|
184
|
+
- `/two-factor/enable`
|
|
185
|
+
- `/passkey/generate-register-options`
|
|
186
|
+
- `/multi-session/list-device-sessions`
|
|
187
|
+
- `/expo-authorization-proxy`
|
|
188
|
+
- `policies.test.ts`: `/api/auth/two-factor/verify-totp` resolves to the `auth`
|
|
189
|
+
bucket.
|
|
190
|
+
- `signUp: closed`:
|
|
191
|
+
- email sign-up is refused;
|
|
192
|
+
- a social sign-in for an unknown email returns `signup_disabled` and creates no
|
|
193
|
+
user;
|
|
194
|
+
- an existing user still signs in;
|
|
195
|
+
- `/terreno/methods` reports `signUp: false`.
|
|
196
|
+
- Existing `betterAuthSetup.test.ts` passes unchanged, which shows the change is
|
|
197
|
+
backward compatible.
|
|
198
|
+
- Docs: `configure-better-auth.md` gets a new "Auth features" table, with each field,
|
|
199
|
+
the plugin it maps to, and a default.
|
|
200
|
+
- Skills: terreno-backend-api, mongoose-schema-safety (Better Auth adds the
|
|
201
|
+
`twoFactor`/`passkey` collections and user fields), backend-test-env, security-audit
|
|
202
|
+
|
|
203
|
+
- [x] **T3** — Account linking modes and verified-email sync
|
|
204
|
+
- Depends on: T2
|
|
205
|
+
- Files: api/src/betterAuthSetup.ts, api/src/betterAuthLinking.test.ts, docs/explanation/account-linking.md, docs/how-to/configure-better-auth.md, changelog/unreleased/account-linking.md
|
|
206
|
+
- Changelog: marked breaking. Unverified sign-ups no longer claim an existing app
|
|
207
|
+
User by email. The fragment includes the migration note.
|
|
208
|
+
- Build:
|
|
209
|
+
- `syncBetterAuthUser` (`api/src/betterAuthSetup.ts:488-498`) stops linking to an
|
|
210
|
+
existing app `User` by email unless the Better Auth user's `emailVerified` is
|
|
211
|
+
true.
|
|
212
|
+
- The provider id is passed through so `oauthProvider` gets filled in.
|
|
213
|
+
- Build (legacy migration): an unverified Better Auth user links to an existing
|
|
214
|
+
legacy app `User` with the same email only when the Better Auth password verifies
|
|
215
|
+
against that User's passport hash. Otherwise the sync returns 409 "Verify your
|
|
216
|
+
email to use this account". It never inserts a duplicate `User` (no 11000 error).
|
|
217
|
+
This closes today's takeover path, where anyone signing up unverified with a
|
|
218
|
+
victim's email is linked to the victim's User.
|
|
219
|
+
- Proof (`betterAuthLinking.test.ts`, Google stubbed through
|
|
220
|
+
`verifyIdToken`/`getUserInfo` and an idToken sign-in):
|
|
221
|
+
- `verified-email`: an email/password user whose email is verified signs in with a
|
|
222
|
+
verified Google identity on the same email. The result is one Better Auth user,
|
|
223
|
+
two `account` rows and one app `User`.
|
|
224
|
+
- `verified-email`, with the local email unverified: the sign-in returns
|
|
225
|
+
`account_not_linked`, and nothing is created.
|
|
226
|
+
- `never`: `linkSocial` returns 403, and a same-email social sign-in is refused.
|
|
227
|
+
- Legacy migration:
|
|
228
|
+
- a legacy User plus a Better Auth sign-up with the same password gives one
|
|
229
|
+
User;
|
|
230
|
+
- a different password gives 409 and still one User;
|
|
231
|
+
- a legacy User with no hash (a GitHub-JWT user) and no `sendMail` gives 409.
|
|
232
|
+
Nothing is left behind (no Better Auth user, account row, or verification
|
|
233
|
+
mail). The T8 `issue-sign-in-link` admin action then lets them in (D8).
|
|
234
|
+
- `explicit`: the same sign-in returns error code `account_not_linked` and creates
|
|
235
|
+
no user. `linkSocial` from the signed-in session then links it.
|
|
236
|
+
- An unverified provider email never links in either mode.
|
|
237
|
+
- Unlinking the only remaining sign-in method is refused.
|
|
238
|
+
- Docs: new `docs/explanation/account-linking.md`. Covers the three modes, why both
|
|
239
|
+
emails must be verified, Apple private-relay emails, `multiSession`, and a
|
|
240
|
+
migration note for JWT apps. `configure-better-auth.md`'s "Migration from JWT
|
|
241
|
+
Auth" section links to it.
|
|
242
|
+
- Skills: terreno-backend-api, security-audit
|
|
243
|
+
|
|
244
|
+
- [x] **T4** — Example backend enables the full suite; the mock OIDC provider joins e2e
|
|
245
|
+
- Depends on: T2
|
|
246
|
+
- Files: example-backend/src/utils/betterAuthConfig.ts, example-backend/src/utils/betterAuthConfig.test.ts, example-backend/.env.example, example-frontend/playwright.config.ts, example-frontend/e2e/helpers/mockOidcProvider.ts, example-frontend/e2e/authMethods.spec.ts, changelog/unreleased/example-auth-suite.md
|
|
247
|
+
- Build:
|
|
248
|
+
- The example turns on every T2 feature:
|
|
249
|
+
- `accountLinking.mode` comes from `AUTH_ACCOUNT_LINKING` (default
|
|
250
|
+
`verified-email`).
|
|
251
|
+
- `signUp.mode` comes from `AUTH_SIGN_UP` (default `open`).
|
|
252
|
+
- Email verification stays optional at sign-up, as today. Linking needs a
|
|
253
|
+
verified email, and the T20 notice covers that.
|
|
254
|
+
- The seeded e2e users are created verified.
|
|
255
|
+
- `twoFactor.issuer` is "Terreno Example".
|
|
256
|
+
- `passkey.origin` comes from `FRONTEND_URL`. `passkey.rpID` comes from
|
|
257
|
+
`PASSKEY_RP_ID` when set, otherwise the `FRONTEND_URL` host. On stable after H3
|
|
258
|
+
it is `<domain>`, so a passkey works on every subdomain (D6).
|
|
259
|
+
- `lastLoginMethod`, `multiSession` and `nativeApps` are on.
|
|
260
|
+
- The `mock-oidc` provider from #1604 (`MOCK_OIDC_ISSUER`) stays the e2e provider.
|
|
261
|
+
The auth-experience specs (T22, T23) join the cross-site bearer project's spec
|
|
262
|
+
list, which already starts the mock provider and runs the backend with the
|
|
263
|
+
handoff on (D7).
|
|
264
|
+
- The mock provider can name any email and whether it is verified per sign-in
|
|
265
|
+
(a login-hint or query option on its authorize page), so T3 and T22 can drive the
|
|
266
|
+
verified and unverified linking cases.
|
|
267
|
+
- Proof:
|
|
268
|
+
- `betterAuthConfig.test.ts`: the default env enables all features,
|
|
269
|
+
`AUTH_ACCOUNT_LINKING=explicit` switches the mode, and `PASSKEY_RP_ID` overrides
|
|
270
|
+
the rpID.
|
|
271
|
+
- `e2e/authMethods.spec.ts` (cross-site bearer project): `GET
|
|
272
|
+
/api/auth/terreno/methods` lists `mock-oidc` and reports every feature on.
|
|
273
|
+
- Skills: terreno-backend-api, backend-test-env
|
|
274
|
+
|
|
275
|
+
- [x] **T5** — Linking a provider works for bearer-mode web clients
|
|
276
|
+
- Depends on: T4
|
|
277
|
+
- Files: api/src/betterAuthOAuthHandoff.ts, api/src/betterAuthOAuthHandoff.test.ts, api/src/betterAuth.ts, rtk/src/betterAuthClient.ts, rtk/src/betterAuthClient.test.ts, rtk/src/index.ts, docs/reference/api.md, docs/how-to/configure-better-auth.md, changelog/unreleased/oauth-handoff-link.md
|
|
278
|
+
- Why: #1604's handoff covers sign-in. Linking a provider from the Security screen
|
|
279
|
+
needs the OAuth state cookie on the API's origin too, and a bearer client's
|
|
280
|
+
`POST /link-social` fetch can't set it cross-site.
|
|
281
|
+
- Build:
|
|
282
|
+
- `POST {basePath}/oauth-handoff/link-intent` (bearer-authenticated,
|
|
283
|
+
`{provider, callbackURL}`) mints a single-use, three-minute link token for the
|
|
284
|
+
caller's session. `callbackURL` follows the handoff's `errorCallbackURL` rule
|
|
285
|
+
(any page on a listed web origin), since no token travels on a link return.
|
|
286
|
+
- `POST {basePath}/oauth-handoff/link-start` takes the token from a form body in a
|
|
287
|
+
top-level navigation (so it never lands in a URL or access log), consumes it,
|
|
288
|
+
runs `linkSocial` for that session, forwards the state cookie, and redirects to
|
|
289
|
+
the provider.
|
|
290
|
+
- The provider callback links the account and redirects to `callbackURL` with no
|
|
291
|
+
one-time token (no session is created).
|
|
292
|
+
- rtk exports `linkSocialWithHandoff({client, provider, callbackURL})`, which on web
|
|
293
|
+
bearer mode fetches the intent and submits the form. Cookie mode and native call
|
|
294
|
+
`linkSocial` as before.
|
|
295
|
+
- Proof (`betterAuthOAuthHandoff.test.ts`, mock genericOAuth provider):
|
|
296
|
+
- a signed-in bearer user links the provider and ends with two `account` rows and no
|
|
297
|
+
new session;
|
|
298
|
+
- a reused or expired token answers 400 with no redirect;
|
|
299
|
+
- a `callbackURL` outside the web origins answers 400;
|
|
300
|
+
- with `accountLinking.mode: "never"` the intent answers 403.
|
|
301
|
+
- rtk test: bearer mode posts the intent and submits the form; cookie mode calls
|
|
302
|
+
`linkSocial`.
|
|
303
|
+
- Docs: `api.md` Better Auth section and the bearer-mode section of
|
|
304
|
+
`configure-better-auth.md`.
|
|
305
|
+
- Skills: terreno-backend-api, terreno-data-fetching, security-audit
|
|
306
|
+
|
|
307
|
+
- [x] **T27** — The 2FA, trust-device and passkey challenges work without cookies
|
|
308
|
+
- Depends on: T5, T18
|
|
309
|
+
- Files: api/src/betterAuthChallengeHeaders.ts, api/src/betterAuthChallengeHeaders.test.ts, api/src/betterAuthFeatures.ts, rtk/src/betterAuthClient.ts, rtk/src/betterAuthClient.test.ts, example-frontend/playwright.config.ts, example-frontend/e2e/crossSiteChallenges.spec.ts, docs/explanation/authentication.md, changelog/unreleased/cookieless-challenges.md
|
|
310
|
+
- Why: bearer mode (#1604) sends no cookies (`credentials: "omit"`). Better Auth keeps
|
|
311
|
+
the 2FA challenge, the trust-device token, and the passkey challenge in cookies, so
|
|
312
|
+
a bearer client on previews (and on stable before H3) can't finish 2FA or passkeys.
|
|
313
|
+
- Build:
|
|
314
|
+
- A Terreno Better Auth plugin copies each challenge cookie the server sets
|
|
315
|
+
(`two_factor`, `trust_device`, the passkey challenge) into a response header,
|
|
316
|
+
`X-Terreno-Auth-Challenge`, carrying the signed cookie values.
|
|
317
|
+
- On requests it accepts that header and converts it back to the cookies in a
|
|
318
|
+
`before` hook, the same way `bearer()` converts the Authorization header. Values
|
|
319
|
+
stay signed, so a tampered header fails the signature check.
|
|
320
|
+
- rtk bearer mode stores the header value next to the bearer (same
|
|
321
|
+
`bearerStorage`) and sends it on `/two-factor/*` and `/passkey/*` calls. It clears
|
|
322
|
+
it on sign-out and after a successful verify (except trust-device, which lives
|
|
323
|
+
for `trustDeviceDays`).
|
|
324
|
+
- Proof:
|
|
325
|
+
- `betterAuthChallengeHeaders.test.ts`: verify-totp with only the header (no cookie)
|
|
326
|
+
succeeds; a tampered header gives 401; passkey register and sign-in finish with
|
|
327
|
+
only the header; a trusted device skips the challenge on the next sign-in.
|
|
328
|
+
- Playwright, cross-site bearer project (cookies are never sent, so the browser
|
|
329
|
+
engine doesn't matter): `e2e/crossSiteChallenges.spec.ts` covers "user with 2FA
|
|
330
|
+
completes the challenge" and "user signs in with a passkey" (CDP virtual
|
|
331
|
+
authenticator).
|
|
332
|
+
- Docs: `authentication.md` gets "Challenges without cookies" next to #1604's bearer
|
|
333
|
+
section, including XSS exposure of `bearerStorage` and the HIPAA note to use
|
|
334
|
+
`sessionStorage`.
|
|
335
|
+
- Skills: terreno-backend-api, terreno-data-fetching, security-audit
|
|
336
|
+
|
|
337
|
+
- [x] **T6** — Security summary, 2FA reset, security audit events, platform routes
|
|
338
|
+
- Depends on: T2
|
|
339
|
+
- Files: api/src/accountSecurity/accountSecurityService.ts, api/src/accountSecurity/accountSecurityApp.ts, api/src/accountSecurity/securityEvents.ts, api/src/accountSecurity/accountSecurity.test.ts, api/src/audit/record.ts, api/src/audit/auditEventModel.ts, api/src/rbac/statements.ts, api/src/rbac/roleModel.ts, api/src/index.ts, example-backend/src/server.ts, docs/reference/account-security.md, docs/how-to/audit-log.md, changelog/unreleased/account-security-app.md
|
|
340
|
+
- Build:
|
|
341
|
+
- The `AccountSecurityApp` TerrenoPlugin exports
|
|
342
|
+
`mountSecurityRoutes(router, authorize)`.
|
|
343
|
+
- `GET /users/:id/security` returns:
|
|
344
|
+
- linked providers;
|
|
345
|
+
- passkey count and names;
|
|
346
|
+
- 2FA on or off and backup codes remaining;
|
|
347
|
+
- active session count;
|
|
348
|
+
- last sign-in method and time;
|
|
349
|
+
- `disabled`;
|
|
350
|
+
- `actions: [{id, label, destructive}]`.
|
|
351
|
+
- `POST /users/:id/security/reset-two-factor` removes the user's TOTP secret and
|
|
352
|
+
backup codes and turns 2FA off.
|
|
353
|
+
- Access is granted to holders of the new RBAC statement `user:manageSecurity`.
|
|
354
|
+
`superadmin` and `operator` hold it; the operator grant goes in
|
|
355
|
+
`api/src/rbac/roleModel.ts:131-140`.
|
|
356
|
+
- The example backend registers `AccountSecurityApp`
|
|
357
|
+
(`example-backend/src/server.ts`). Only the user's own session credential is
|
|
358
|
+
accepted; delegated credentials are refused.
|
|
359
|
+
- `recordSecurityAction({actor, target, action, org})` in `securityEvents.ts`
|
|
360
|
+
writes an `AuditEvent` with `source: "security"`, `modelName: "User"`,
|
|
361
|
+
`recordId` set to the target, `verb: "updated"`, `operation: "update"`, and
|
|
362
|
+
`after: {securityAction, organizationId?}`. That source is added to
|
|
363
|
+
`AuditEventWrite` (`api/src/audit/record.ts:22-33`) and to the model enum.
|
|
364
|
+
`recordSecurityAction` also emits to in-process listeners, which T10 uses.
|
|
365
|
+
- `securityEvents.ts` defines every action name used in T7–T10.
|
|
366
|
+
- `AccountSecurityApp` throws at startup when `AuditApp` is not registered, unless
|
|
367
|
+
the consumer passes `audit: false` (Q6). The example already registers `AuditApp`
|
|
368
|
+
(`example-backend/src/server.ts:428`).
|
|
369
|
+
- Proof (`accountSecurity.test.ts`):
|
|
370
|
+
- An operator gets the summary and can reset 2FA. Afterwards the target signs in
|
|
371
|
+
with a password and gets no 2FA challenge.
|
|
372
|
+
- A member is refused with 403.
|
|
373
|
+
- An MCP service-token request is refused with 403.
|
|
374
|
+
- Each reset writes one `AuditEvent` with `source: "security"`.
|
|
375
|
+
- Startup without `AuditApp` throws; `audit: false` boots.
|
|
376
|
+
- Docs: new `docs/reference/account-security.md` (routes, actions, permissions).
|
|
377
|
+
`audit-log.md` gains a `security` source row.
|
|
378
|
+
- Skills: terreno-backend-api, mongoose-schema-safety, security-audit
|
|
379
|
+
|
|
380
|
+
- [x] **T7** — Recovery: remove passkey, unlink provider
|
|
381
|
+
- Depends on: T6
|
|
382
|
+
- Files: api/src/accountSecurity/credentialActions.ts, api/src/accountSecurity/accountSecurityApp.ts, api/src/accountSecurity/credentialActions.test.ts, docs/reference/account-security.md
|
|
383
|
+
- Build:
|
|
384
|
+
- `DELETE /users/:id/security/passkeys/:passkeyId`
|
|
385
|
+
- `DELETE /users/:id/security/accounts/:providerId`
|
|
386
|
+
- An unlink that would leave zero sign-in methods returns 409, unless the user has
|
|
387
|
+
a verified email, in which case it also sends a password reset.
|
|
388
|
+
- Proof:
|
|
389
|
+
- After removal, the passkey is no longer listed, and the provider row is gone.
|
|
390
|
+
- The 409 guard holds.
|
|
391
|
+
- Each action writes a security `AuditEvent`.
|
|
392
|
+
- The summary `actions` include both.
|
|
393
|
+
- Skills: terreno-backend-api, security-audit
|
|
394
|
+
|
|
395
|
+
- [x] **T8** — Recovery: revoke sessions, send reset, disable/enable
|
|
396
|
+
- Depends on: T7
|
|
397
|
+
- Files: api/src/accountSecurity/accessActions.ts, api/src/accountSecurity/accountSecurityApp.ts, api/src/accountSecurity/accessActions.test.ts, example-backend/src/models/user.ts, example-backend/src/types/models/userTypes.ts, docs/reference/account-security.md
|
|
398
|
+
- Build:
|
|
399
|
+
- Fix the existing Better Auth reset-password JWT sync: over HTTP,
|
|
400
|
+
`applyBetterAuthPasswordResetToJwt` re-reads a request body Better Auth already
|
|
401
|
+
consumed, so `POST {basePath}/reset-password` answers 500 whenever a `userModel`
|
|
402
|
+
is set (found in T7). Capture `newPassword` from the parsed endpoint body instead
|
|
403
|
+
(a `hooks.before` matcher on `/reset-password`). Proof: an HTTP reset with a
|
|
404
|
+
`userModel` succeeds, signs in with the new password, and bumps `tokenEpoch`.
|
|
405
|
+
- `POST .../revoke-sessions`
|
|
406
|
+
- `POST .../send-password-reset`, which uses `sendMail` and returns 501 when
|
|
407
|
+
`publicAppUrl` is unset, matching the existing hooks.
|
|
408
|
+
- `POST .../disable` and `.../enable`, which set `User.disabled`.
|
|
409
|
+
- `POST .../issue-sign-in-link` (platform-only, for legacy users with no password
|
|
410
|
+
hash and no mail path, T3) creates the Better Auth user for that app User,
|
|
411
|
+
linked by `betterAuthId`, and returns a single-use set-password link the admin
|
|
412
|
+
hands over. It never marks an attacker-chosen credential verified: T3 refuses
|
|
413
|
+
unverified same-email sign-ups before any Better Auth user exists (D8).
|
|
414
|
+
- The example User gains `isDisabledPlugin`, applied in `user.ts` only.
|
|
415
|
+
- Proof:
|
|
416
|
+
- After revoke, the next REST request with the old bearer token gets 401.
|
|
417
|
+
- After disable, REST gets 401 and the socket handshake is refused. After enable,
|
|
418
|
+
sign-in works again.
|
|
419
|
+
- The reset email is captured by the test mailer.
|
|
420
|
+
- After `issue-sign-in-link`, the link sets a password and the user signs in to
|
|
421
|
+
the same app User; the link is refused on reuse.
|
|
422
|
+
- Skills: terreno-backend-api, mongoose-schema-safety, security-audit
|
|
423
|
+
|
|
424
|
+
- [x] **T9** — Org-admin scoped recovery routes
|
|
425
|
+
- Depends on: T6
|
|
426
|
+
- Files: api/src/orgs/orgsApp.ts, api/src/terrenoApp.ts, api/src/accountSecurity/orgSecurityRoutes.ts, api/src/accountSecurity/orgSecurityRoutes.test.ts, api/src/rbac/membershipOrgAdminSource.ts, docs/explanation/organizations.md, docs/how-to/recover-locked-out-user.md
|
|
427
|
+
- Build:
|
|
428
|
+
- `TerrenoApp` passes the registered `AccountSecurityApp`, if any, to the
|
|
429
|
+
`OrgsApp` it builds (`api/src/terrenoApp.ts:599`). `OrgsApp` mounts
|
|
430
|
+
`mountSecurityRoutes` at `/orgs/:orgId/members/:userId/security` only when one is
|
|
431
|
+
present.
|
|
432
|
+
- The authorizer allows an org-admin of `:orgId` when the target has an active
|
|
433
|
+
membership there, even if the target also belongs to other orgs (D2). It always
|
|
434
|
+
refuses targets with a platform role (`superadmin`, `operator`) or `admin: true`
|
|
435
|
+
(Q1).
|
|
436
|
+
- `orgRecovery.minMembershipDays` is opt-in hardening and defaults to 0. When set,
|
|
437
|
+
it refuses members who joined more recently than that. This defends against an
|
|
438
|
+
org-admin attaching an existing user by email
|
|
439
|
+
(`api/src/orgs/orgsApp.ts:406-450`) and acting on them right away. T10's email
|
|
440
|
+
tells the user whenever an org-admin acts.
|
|
441
|
+
- Under the org prefix, a separate `suspend-membership` action
|
|
442
|
+
(`POST /orgs/:orgId/members/:userId/security/suspend-membership`) suspends that
|
|
443
|
+
membership; `disable`, `enable` and `issue-sign-in-link` are not mounted there.
|
|
444
|
+
Global `User.disabled` stays platform-only (Q8). Restoring a member uses the
|
|
445
|
+
existing `PATCH /orgs/:id/members/:memberId {status: "active"}`.
|
|
446
|
+
- Platform actors always pass.
|
|
447
|
+
- Events record `org`.
|
|
448
|
+
- Proof (`orgSecurityRoutes.test.ts`, authorization matrix on summary and
|
|
449
|
+
reset-two-factor):
|
|
450
|
+
- The org-admin of A acting on an active member of A is allowed. This includes a
|
|
451
|
+
member who also belongs to B.
|
|
452
|
+
- The org-admin of A acting on a member of B only gets 404.
|
|
453
|
+
- The org-admin of A acting on a suspended member gets 404.
|
|
454
|
+
- The org-admin of A acting on a member who holds `superadmin` gets 403.
|
|
455
|
+
- The org-admin of A acting on another org-admin of A is allowed.
|
|
456
|
+
- With `minMembershipDays: 7`, attaching an existing user by email and resetting
|
|
457
|
+
their 2FA the same day gets 403. With the default 0, it is allowed.
|
|
458
|
+
- Org-prefix `suspend-membership` suspends only the A membership, and the user can
|
|
459
|
+
still sign in.
|
|
460
|
+
- An app without `AccountSecurityApp` exposes no `/orgs/*/security` routes.
|
|
461
|
+
- A plain member gets 403.
|
|
462
|
+
- A disabled org gets 403.
|
|
463
|
+
- A generic check confirms that every route `mountSecurityRoutes` registers is
|
|
464
|
+
reachable under the org prefix.
|
|
465
|
+
- Docs: `organizations.md` gets a "Member recovery" section. New
|
|
466
|
+
`recover-locked-out-user.md` covers platform and org paths.
|
|
467
|
+
- Skills: terreno-backend-api, security-audit, building-admin-interfaces
|
|
468
|
+
|
|
469
|
+
- [x] **T10** — Security notification emails
|
|
470
|
+
- Depends on: T8, T27
|
|
471
|
+
- Files: api/src/accountSecurity/securityNotifications.ts, api/src/accountSecurity/securityEventHooks.ts, api/src/accountSecurity/securityEvents.ts, api/src/betterAuthFeatures.ts, api/src/accountSecurity/securityNotifications.test.ts, comms/src/templates.ts, comms/src/templates.test.ts, comms/src/types.ts, docs/reference/account-security.md
|
|
472
|
+
- Build:
|
|
473
|
+
- A listener on `recordSecurityAction` emails the affected user. This covers both admin
|
|
474
|
+
actions and self-service ones (provider linked or unlinked, passkey added or
|
|
475
|
+
removed, 2FA enabled or disabled, password changed).
|
|
476
|
+
- Self-service events are recorded by a Better Auth `hooks.after` plugin.
|
|
477
|
+
- Each email names who acted: "you", or "an administrator". The org name appears
|
|
478
|
+
only when `notifications.includeOrgName` is true (default false; Q9).
|
|
479
|
+
- Proof:
|
|
480
|
+
- Each event type sends exactly one email to the target, through the test mailer.
|
|
481
|
+
- `renderAuthMail` renders each new template id.
|
|
482
|
+
- No email is sent when `sendMail` is unset.
|
|
483
|
+
- Skills: terreno-backend-api, security-audit
|
|
484
|
+
|
|
485
|
+
- [x] **T11** — Previews sign in through oauth-proxy on the stable deploy
|
|
486
|
+
- Depends on: T5
|
|
487
|
+
- Files: example-backend/src/utils/betterAuthConfig.ts, example-backend/src/utils/betterAuthConfig.test.ts, api/src/betterAuthOAuthHandoff.ts, api/src/betterAuthOAuthProxy.test.ts, scripts/ci/gcp-deploy.sh, infra/flourish/main.tf, docs/how-to/set-up-social-login.md, changelog/unreleased/preview-oauth.md
|
|
488
|
+
- Build:
|
|
489
|
+
- When `OAUTH_PROXY_PRODUCTION_URL` is set, the example backend enables
|
|
490
|
+
`oauthProxy`.
|
|
491
|
+
- `gcp-deploy.sh` reads `STABLE_BACKEND_URL` and `STABLE_FRONTEND_URL`, defaulting
|
|
492
|
+
to today's `run.app` and `netlify.app` hosts, so the H3 move is a two-variable
|
|
493
|
+
change (D6). It sets `OAUTH_PROXY_PRODUCTION_URL` to `STABLE_BACKEND_URL` for
|
|
494
|
+
`pr-N` revisions, sets `BETTER_AUTH_URL` to it on stable, and sets `FRONTEND_URL`
|
|
495
|
+
for both. `PASSKEY_RP_ID` is set on stable only when `STABLE_FRONTEND_URL` is a
|
|
496
|
+
subdomain (its parent domain); previews keep their own host.
|
|
497
|
+
- The stable `trustedOrigins` include both the old and new frontend hosts during the
|
|
498
|
+
move.
|
|
499
|
+
- `gcp-deploy.sh` mounts the `GOOGLE_*`, `GITHUB_*` and `APPLE_*` secrets.
|
|
500
|
+
- Terraform adds those secret containers.
|
|
501
|
+
- Previews already share the stable `BETTER_AUTH_SECRET`
|
|
502
|
+
(`scripts/ci/gcp-deploy.sh:129-131`), which oauth-proxy needs.
|
|
503
|
+
- The #1604 handoff hook also runs on oauth-proxy's `/oauth-proxy-callback`, so a
|
|
504
|
+
bearer-mode preview gets its one-time token after the proxied return. This file is
|
|
505
|
+
`api/src/betterAuthOAuthHandoff.ts` (ordered after T5).
|
|
506
|
+
- Proof:
|
|
507
|
+
- Config test: the preview env includes the oauthProxy plugin, with `productionURL`
|
|
508
|
+
set to stable. Stable runs it too (Better Auth 1.7.4 forwards a preview's return
|
|
509
|
+
from stable's callback hook), naming itself, and its own sign-ins skip the proxy
|
|
510
|
+
so they keep the state-cookie check.
|
|
511
|
+
- api test (`betterAuthOAuthProxy.test.ts`): two Better Auth instances, "stable" and
|
|
512
|
+
"preview", with separate databases and one secret. A mock-provider sign-in started
|
|
513
|
+
on the preview returns through stable's callback to the preview, and the preview's
|
|
514
|
+
redirect carries the one-time token. This proves the state survives the hop.
|
|
515
|
+
- Config test: with `STABLE_*` on subdomains of one domain, stable gets
|
|
516
|
+
`passkey.rpID` = the parent domain and no `crossSubDomainCookies`; with the
|
|
517
|
+
defaults it gets the `netlify.app` host.
|
|
518
|
+
- `terraform validate` passes.
|
|
519
|
+
- `bash -n scripts/ci/gcp-deploy.sh` passes.
|
|
520
|
+
- Docs: new `set-up-social-login.md` (linked from `configure-better-auth.md` by T20),
|
|
521
|
+
covering:
|
|
522
|
+
- registering each provider;
|
|
523
|
+
- Apple key and JWT rotation;
|
|
524
|
+
- one callback per provider;
|
|
525
|
+
- how previews proxy;
|
|
526
|
+
- passkey `rpID` per preview host, given that `netlify.app` is a public suffix,
|
|
527
|
+
and the parent domain on stable once it shares a domain;
|
|
528
|
+
- moving stable onto one domain (H3): the custom domain, the domain mapping, the
|
|
529
|
+
callback re-registration, and why passkeys made on the old host stop working.
|
|
530
|
+
- Skills: deploy-gcp, terreno-backend-api, verify-ui-changes
|
|
531
|
+
|
|
532
|
+
- [ ] **T28** — Live social sign-in on a preview and on the stable deploy
|
|
533
|
+
- Depends on: T11
|
|
534
|
+
- Blocked by: H1
|
|
535
|
+
- Status: deferred past PR #1613 and tracked as X10. It needs the H1 OAuth apps; every other
|
|
536
|
+
proof uses the mock provider.
|
|
537
|
+
- Files: example-frontend/e2e/live-social-login.spec.ts
|
|
538
|
+
- Proof:
|
|
539
|
+
- On this PR's preview and on the stable deploy, after H1:
|
|
540
|
+
- On both, `GET /api/auth/terreno/methods` lists github, google and apple.
|
|
541
|
+
- For each provider on both, `POST /api/auth/sign-in/social` returns an
|
|
542
|
+
authorization URL whose `redirect_uri` host is the stable backend.
|
|
543
|
+
- Apple `form_post` gate: a `POST` to the stable
|
|
544
|
+
`/api/auth/callback/apple`, carrying a preview-originated state, answers 302 to
|
|
545
|
+
the preview host (not 404/405). If it fails, Apple on previews moves to Open
|
|
546
|
+
risks with the cause, and stable Apple still ships.
|
|
547
|
+
- `e2e/live-social-login.spec.ts` (opt-in, `LIVE_OAUTH=1`) signs in with the H1
|
|
548
|
+
GitHub test account on both URLs.
|
|
549
|
+
- Google and Apple are checked by reaching the provider consent page with the
|
|
550
|
+
stable `redirect_uri`.
|
|
551
|
+
|
|
552
|
+
Screenshots go to `/opt/cursor/artifacts/`. A full Google/Apple sign-in is H2.
|
|
553
|
+
- Skills: verify-ui-changes, deploy-gcp
|
|
554
|
+
|
|
555
|
+
- [x] **T12** — LoginScreen v2 (presentational)
|
|
556
|
+
- Depends on: none
|
|
557
|
+
- Files: ui/src/login/LoginScreen.tsx, ui/src/login/loginTypes.ts, ui/src/login/LoginScreen.test.tsx, ui/src/login/__snapshots__/LoginScreen.test.tsx.snap, ui/src/signUp/OAuthButtons.tsx, ui/src/signUp/signUpTypes.ts, ui/src/SocialLoginButton.tsx, ui/src/Common.ts, demo/stories/LoginScreen.stories.tsx, demo/story-config/LoginScreen.config.tsx, docs/reference/ui.md
|
|
558
|
+
- Build: new optional props.
|
|
559
|
+
- `lastUsedMethod`: shows a "Last used" badge on that method and moves it to the
|
|
560
|
+
top.
|
|
561
|
+
- `currentSessions` / `rememberedAccounts`: shows "Continue as" cards with avatar,
|
|
562
|
+
name, provider, `onContinue` and `onForget`.
|
|
563
|
+
- `onPasskeyPress` and `passkeyAutofill`: the email field gets
|
|
564
|
+
`autoComplete="username webauthn"`.
|
|
565
|
+
- `notice`: for `account_not_linked` guidance.
|
|
566
|
+
- Provider buttons move above the form when `lastUsedMethod` is a provider.
|
|
567
|
+
- Fix `SocialLoginButton` ignoring later `loading` prop changes
|
|
568
|
+
(`ui/src/SocialLoginButton.tsx:76`).
|
|
569
|
+
- Every new element gets a testID, named `login-screen-continue-<i>` and so on.
|
|
570
|
+
- Proof: `LoginScreen.test.tsx` covers:
|
|
571
|
+
- badge and ordering per `lastUsedMethod`;
|
|
572
|
+
- continue and forget callbacks;
|
|
573
|
+
- the passkey button only when `onPasskeyPress` is set;
|
|
574
|
+
- the loading prop update.
|
|
575
|
+
|
|
576
|
+
Existing snapshots stay unchanged with no new props.
|
|
577
|
+
- Docs: `ui.md` LoginScreen section.
|
|
578
|
+
- Skills: terreno-ui, accessibility-testing, verify-ui-changes
|
|
579
|
+
|
|
580
|
+
- [x] **T13** — OtpField and TwoFactorChallengeScreen
|
|
581
|
+
- Depends on: none
|
|
582
|
+
- Files: ui/src/OtpField.tsx, ui/src/OtpField.test.tsx, ui/src/login/TwoFactorChallengeScreen.tsx, ui/src/login/TwoFactorChallengeScreen.test.tsx, demo/stories/OtpField.stories.tsx, demo/story-config/OtpField.config.tsx, demo/stories/TwoFactorChallengeScreen.stories.tsx, demo/story-config/TwoFactorChallengeScreen.config.tsx
|
|
583
|
+
- Build:
|
|
584
|
+
- `OtpField` takes N digits, supports paste, and uses `autoComplete="one-time-code"`.
|
|
585
|
+
- The challenge screen has TOTP mode, a backup-code toggle, a "Trust this device"
|
|
586
|
+
checkbox, an error state, and `onSubmit({code, kind, trustDevice})`.
|
|
587
|
+
- Proof:
|
|
588
|
+
- Pasting "123456" fills every cell and fires `onComplete`.
|
|
589
|
+
- The backup toggle switches `kind`.
|
|
590
|
+
- Errors render under `two-factor-challenge-error`.
|
|
591
|
+
- Skills: terreno-ui, accessibility-testing
|
|
592
|
+
|
|
593
|
+
- [x] **T14** — LinkedAccountsCard and PasswordCard
|
|
594
|
+
- Depends on: none
|
|
595
|
+
- Files: ui/src/security/LinkedAccountsCard.tsx, ui/src/security/LinkedAccountsCard.test.tsx, ui/src/security/PasswordCard.tsx, ui/src/security/PasswordCard.test.tsx, ui/src/security/securityTypes.ts, demo/stories/LinkedAccountsCard.stories.tsx, demo/story-config/LinkedAccountsCard.config.tsx, demo/stories/PasswordCard.stories.tsx, demo/story-config/PasswordCard.config.tsx
|
|
596
|
+
- Build:
|
|
597
|
+
- `LinkedAccountsCard` lists each provider as linked (with email and linked date) or
|
|
598
|
+
not linked. It offers Link and Unlink, with a confirm. Unlink is disabled with an
|
|
599
|
+
explanation when it is the last method.
|
|
600
|
+
- `PasswordCard` shows "Change password" when a credential exists, and "Set a
|
|
601
|
+
password" for OAuth-only users.
|
|
602
|
+
- Proof: tests for the last-method disabled state, the confirm modal, and the
|
|
603
|
+
set/change variants.
|
|
604
|
+
- Skills: terreno-ui, accessibility-testing
|
|
605
|
+
|
|
606
|
+
- [x] **T15** — PasskeysCard and SessionsCard
|
|
607
|
+
- Depends on: none
|
|
608
|
+
- Files: ui/src/security/PasskeysCard.tsx, ui/src/security/PasskeysCard.test.tsx, ui/src/security/SessionsCard.tsx, ui/src/security/SessionsCard.test.tsx, demo/stories/PasskeysCard.stories.tsx, demo/story-config/PasskeysCard.config.tsx, demo/stories/SessionsCard.stories.tsx, demo/story-config/SessionsCard.config.tsx
|
|
609
|
+
- Build:
|
|
610
|
+
- `PasskeysCard`: add, rename and remove passkeys, each showing its created and
|
|
611
|
+
last-used dates.
|
|
612
|
+
- `SessionsCard`: device and browser, location if given, last active (Luxon
|
|
613
|
+
relative time), "This device" marker, revoke one, and "Sign out everywhere else".
|
|
614
|
+
- Proof: tests for remove confirm, the current session not being revocable
|
|
615
|
+
individually, and relative time rendering.
|
|
616
|
+
- Skills: terreno-ui, accessibility-testing
|
|
617
|
+
|
|
618
|
+
- [x] **T16** — TwoFactorSettingsCard (enroll, backup codes)
|
|
619
|
+
- Depends on: T2, T13
|
|
620
|
+
- Files: ui/src/security/TwoFactorSettingsCard.tsx, ui/src/security/TwoFactorSettingsCard.test.tsx, demo/stories/TwoFactorSettingsCard.stories.tsx, demo/story-config/TwoFactorSettingsCard.config.tsx
|
|
621
|
+
- Build: the card walks through these steps:
|
|
622
|
+
1. Password confirm.
|
|
623
|
+
2. QR code from `totpURI` (`react-native-qrcode-svg` over the existing
|
|
624
|
+
`react-native-svg`), plus a copyable secret.
|
|
625
|
+
3. Verify the code with `OtpField`.
|
|
626
|
+
4. Show backup codes with copy and download. The user must check "I saved these"
|
|
627
|
+
before finishing.
|
|
628
|
+
|
|
629
|
+
Once enrolled, the card offers regenerate backup codes and disable.
|
|
630
|
+
- Proof: tests for step progression, the finish button gated on "I saved these", and
|
|
631
|
+
regenerate showing the new codes.
|
|
632
|
+
- Skills: terreno-ui, accessibility-testing
|
|
633
|
+
|
|
634
|
+
- [x] **T17** — AccountSecurityScreen, exports, demo registration
|
|
635
|
+
- Depends on: T12, T14, T15, T16
|
|
636
|
+
- Files: ui/src/security/AccountSecurityScreen.tsx, ui/src/security/AccountSecurityScreen.test.tsx, ui/src/index.tsx, demo/demoConfig.tsx, demo/stories/AccountSecurityScreen.stories.tsx, demo/story-config/AccountSecurityScreen.config.tsx, docs/reference/ui.md, changelog/unreleased/ui-auth-components.md
|
|
637
|
+
- Build:
|
|
638
|
+
- The composite screen renders the cards that the props enable, so a disabled
|
|
639
|
+
feature hides its card.
|
|
640
|
+
- Export everything from T13–T17 at the ui package root.
|
|
641
|
+
- Register every new story config.
|
|
642
|
+
- Proof:
|
|
643
|
+
- `bun run check:demo-coverage` passes.
|
|
644
|
+
- The screen test hides the passkey card when `passkeys` is undefined.
|
|
645
|
+
- `bun run ui:test` passes.
|
|
646
|
+
- Docs: `ui.md` gets an "Account security" section.
|
|
647
|
+
- Skills: terreno-ui, update-docs, verify-ui-changes
|
|
648
|
+
|
|
649
|
+
- [x] **T18** — rtk client feature plugins
|
|
650
|
+
- Depends on: T2
|
|
651
|
+
- Files: rtk/src/betterAuthClient.ts, rtk/src/betterAuthTypes.ts, rtk/src/betterAuthClient.test.ts
|
|
652
|
+
- Build:
|
|
653
|
+
- `createBetterAuthClient({features})` adds `twoFactorClient` (with
|
|
654
|
+
`onTwoFactorRedirect`), `passkeyClient`, `multiSessionClient`,
|
|
655
|
+
`lastLoginMethodClient` and `oneTimeTokenClient`.
|
|
656
|
+
- Provider types widen to `string` so generic providers work.
|
|
657
|
+
- Proof:
|
|
658
|
+
- A test checks that each feature flag exposes its namespace on the client
|
|
659
|
+
(`client.twoFactor.enable`, and so on).
|
|
660
|
+
- Calls without `features` still produce today's client shape, so existing apps are
|
|
661
|
+
unaffected.
|
|
662
|
+
- Skills: terreno-data-fetching
|
|
663
|
+
|
|
664
|
+
- [x] **T19** — Remembered accounts on device
|
|
665
|
+
- Depends on: none
|
|
666
|
+
- Files: rtk/src/auth/rememberedAccounts.ts, rtk/src/auth/rememberedAccounts.test.ts
|
|
667
|
+
- Build:
|
|
668
|
+
- `createRememberedAccountsStore({rememberIdentity})` stores data in SecureStore
|
|
669
|
+
on native and localStorage on web.
|
|
670
|
+
- It records `{userId, method, lastUsedAt}`. Name, email (masked) and avatar are
|
|
671
|
+
stored only when `rememberIdentity` is true.
|
|
672
|
+
- It keeps the newest 5, supports `forget(userId)`, and exposes
|
|
673
|
+
`useRememberedAccounts()`.
|
|
674
|
+
- Proof:
|
|
675
|
+
- With `rememberIdentity: false`, the persisted record has no name, email or
|
|
676
|
+
image.
|
|
677
|
+
- `forget` removes the record.
|
|
678
|
+
- Ordering is by `lastUsedAt`.
|
|
679
|
+
- Skills: terreno-data-fetching, security-audit
|
|
680
|
+
|
|
681
|
+
- [x] **T20** — Connected TerrenoLoginScreen and login controller
|
|
682
|
+
- Depends on: T3, T11, T17, T19, T27
|
|
683
|
+
- Files: rtk/src/auth/useLoginController.ts, rtk/src/auth/useLoginController.test.ts, rtk/src/auth/TerrenoLoginScreen.tsx, rtk/src/index.ts, docs/how-to/configure-better-auth.md, changelog/unreleased/terreno-login-screen.md
|
|
684
|
+
- Build: the controller combines:
|
|
685
|
+
- `useAuthMethods`;
|
|
686
|
+
- a live session (the #1604 bearer from `getBetterAuthBearerToken`, or the cookie
|
|
687
|
+
session): a valid one skips the login screen entirely;
|
|
688
|
+
- device sessions from `multiSession`, which become one-tap "Continue as" cards
|
|
689
|
+
where the backend cookie is first-party (native cookie jar, same-site web);
|
|
690
|
+
- remembered accounts from T19, which become "Continue as" cards that
|
|
691
|
+
re-authenticate with the remembered method. On cross-site web this is the only
|
|
692
|
+
source;
|
|
693
|
+
- the last used method, read from T19's on-device store. Better Auth's
|
|
694
|
+
`lastLoginMethodClient` reads the backend's cookie, which a cross-site page
|
|
695
|
+
cannot see;
|
|
696
|
+
- passkey conditional UI on web;
|
|
697
|
+
- social sign-in through #1604's `signInSocialWithHandoff` in bearer mode;
|
|
698
|
+
- the 2FA redirect;
|
|
699
|
+
- mapping `account_not_linked` to guidance for the linking mode: "verify your
|
|
700
|
+
email to link" when the local email is unverified, "sign in with <method> and
|
|
701
|
+
link from Security" in `explicit` mode, or "this email already has an account"
|
|
702
|
+
in `never` mode;
|
|
703
|
+
- hiding sign-up links and mapping `signup_disabled` to "ask your administrator
|
|
704
|
+
for access" when `signUp` is closed.
|
|
705
|
+
|
|
706
|
+
`TerrenoLoginScreen` renders ui's `LoginScreen` from it.
|
|
707
|
+
- Proof: controller tests with a mocked client cover:
|
|
708
|
+
- device sessions taking precedence over remembered accounts;
|
|
709
|
+
- the 2FA redirect firing `onTwoFactorRequired`;
|
|
710
|
+
- `account_not_linked` producing the notice;
|
|
711
|
+
- a successful sign-in recording a remembered account.
|
|
712
|
+
- Docs: `configure-better-auth.md` gets a "Drop-in login screen" section and a link
|
|
713
|
+
to `set-up-social-login.md`.
|
|
714
|
+
- Skills: terreno-data-fetching, terreno-ui
|
|
715
|
+
|
|
716
|
+
- [x] **T21** — Connected TerrenoAccountSecurityScreen
|
|
717
|
+
- Depends on: T20
|
|
718
|
+
- Files: rtk/src/auth/useAccountSecurity.ts, rtk/src/auth/useAccountSecurity.test.ts, rtk/src/auth/TerrenoAccountSecurityScreen.tsx, rtk/src/index.ts, docs/how-to/configure-better-auth.md, api/src/betterAuthSetPassword.ts, api/src/betterAuthSetPassword.test.ts, api/src/rateLimit/policies.ts
|
|
719
|
+
- Build: the hook wraps:
|
|
720
|
+
- `listAccounts`, `linkSocialWithHandoff` (T5), `unlinkAccount`;
|
|
721
|
+
- the passkey list, add and delete;
|
|
722
|
+
- the `twoFactor` enable, verify, disable and backup-code calls;
|
|
723
|
+
- `listSessions`, `revokeSession`, `revokeOtherSessions`;
|
|
724
|
+
- `changePassword`, and "set a password" for OAuth-only users. Better Auth's
|
|
725
|
+
`setPassword` is server-only, so T21 adds a Terreno endpoint
|
|
726
|
+
`POST {basePath}/terreno/set-password` (session required; refused when a
|
|
727
|
+
credential account already exists; rate-limited in the `auth` bucket) in
|
|
728
|
+
`api/src/betterAuthSetPassword.ts` with its own api test.
|
|
729
|
+
|
|
730
|
+
After each mutation it refetches the affected section. The connected screen renders
|
|
731
|
+
ui's `AccountSecurityScreen`.
|
|
732
|
+
- Proof: hook tests with a mocked client cover:
|
|
733
|
+
- unlinking the last method never calls the client;
|
|
734
|
+
- enabling 2FA returns backup codes to the card;
|
|
735
|
+
- "sign out everywhere else" calls `revokeOtherSessions`.
|
|
736
|
+
- Skills: terreno-data-fetching, terreno-ui
|
|
737
|
+
|
|
738
|
+
- [x] **T22** — Example login, 2FA challenge, passkey sign-in
|
|
739
|
+
- Depends on: T4, T20
|
|
740
|
+
- Files: example-frontend/app/login.tsx, example-frontend/app/_layout.tsx, example-frontend/lib/betterAuth.ts, example-frontend/e2e/login.spec.ts, example-frontend/e2e/two-factor.spec.ts, example-frontend/e2e/account-linking.spec.ts, example-frontend/e2e/helpers/login.ts
|
|
741
|
+
- Build:
|
|
742
|
+
- The example login uses `TerrenoLoginScreen` with `rememberIdentity: true`.
|
|
743
|
+
- The two-factor challenge runs inline on the login route (TerrenoLoginScreen's default), so there is no separate `app/twoFactor.tsx`.
|
|
744
|
+
- `loginAs` handles a 2FA challenge when a fixture user has a TOTP secret.
|
|
745
|
+
- Proof (Playwright; social, 2FA and passkey cases run in the cross-site bearer
|
|
746
|
+
project, which has the mock OIDC provider; the rest in the default project):
|
|
747
|
+
- "user sees last used method after signing out": sign in with mock OIDC, sign
|
|
748
|
+
out, and `login-screen-oauth-mock-oidc` shows the badge.
|
|
749
|
+
- "user continues as a remembered account".
|
|
750
|
+
- "user with 2FA completes the TOTP challenge": the code is computed in the test
|
|
751
|
+
from the fixture secret.
|
|
752
|
+
- "user signs in with a passkey": uses a CDP `WebAuthn.addVirtualAuthenticator`
|
|
753
|
+
authenticator with a passkey registered through the API.
|
|
754
|
+
- "explicit linking mode shows the link guidance": the backend runs with
|
|
755
|
+
`AUTH_ACCOUNT_LINKING=explicit`.
|
|
756
|
+
- "user's email and mock-OAuth sign-ins land in one account" (the requested
|
|
757
|
+
josh@ example):
|
|
758
|
+
1. Sign up by email and verify it.
|
|
759
|
+
2. Sign out, then sign in with mock OAuth on the same email.
|
|
760
|
+
3. The same todos show.
|
|
761
|
+
The unverified variant shows "verify your email to link", and after verifying,
|
|
762
|
+
the same sign-in joins the account.
|
|
763
|
+
- Remove `test.fixme` from `e2e/crossSiteChallenges.spec.ts` (added by T27) once the
|
|
764
|
+
example login wires 2FA and passkey, and make it pass.
|
|
765
|
+
- In the cross-site bearer project, "last used" and "Continue as" both render after
|
|
766
|
+
sign-out (they come from T19's on-device store, not a backend cookie).
|
|
767
|
+
|
|
768
|
+
Screenshots go to `/opt/cursor/artifacts/`.
|
|
769
|
+
- Skills: verify-ui-changes, accessibility-testing, building-terreno-apps
|
|
770
|
+
|
|
771
|
+
- [ ] **T29** — Android example signs in with the mock provider (device-proven native)
|
|
772
|
+
- Depends on: T22
|
|
773
|
+
- Status: deferred past PR #1613 and tracked as X11. The emulator companion never stayed up.
|
|
774
|
+
The flow and the native proxy fix shipped, so only the device run is left.
|
|
775
|
+
- Blocked by: an Android emulator companion (`hive-companion start`). If none is
|
|
776
|
+
available, return BLOCKED, environment.
|
|
777
|
+
- Files: example-frontend/maestro/native-social-login.yaml, docs/how-to/set-up-social-login.md
|
|
778
|
+
- Build: no app code is expected. It is a native proof of the T2 `nativeApps`
|
|
779
|
+
wiring and the T20 controller.
|
|
780
|
+
- `adb reverse` exposes the backend and the mock OAuth server to the emulator.
|
|
781
|
+
- The Maestro flow taps `login-screen-oauth-mock-oidc`, completes the browser sign-in,
|
|
782
|
+
and lands on `todos-screen`.
|
|
783
|
+
- Proof: `maestro --device 127.0.0.1:5555 test example-frontend/maestro/native-social-login.yaml`
|
|
784
|
+
passes. A screenshot from `adb ... screencap` is saved to `/opt/cursor/artifacts/`.
|
|
785
|
+
- Docs: `set-up-social-login.md` gets a "Native" section (scheme, `trustedOrigins`).
|
|
786
|
+
- Skills: verify-ui-changes, building-terreno-apps
|
|
787
|
+
|
|
788
|
+
- [x] **T23** — Example security screen
|
|
789
|
+
- Depends on: T21, T22
|
|
790
|
+
- Files: example-frontend/app/settings/security.tsx, example-frontend/app/settings/_layout.tsx, example-frontend/app/(tabs)/profile.tsx, example-frontend/e2e/account-security.spec.ts
|
|
791
|
+
- Build:
|
|
792
|
+
- Add `settings/security`, using `TerrenoAccountSecurityScreen`.
|
|
793
|
+
- Profile links to it (`profile-security-link`).
|
|
794
|
+
- Password change on profile moves to Better Auth `changePassword`, replacing the
|
|
795
|
+
users PATCH at `app/(tabs)/profile.tsx:151`.
|
|
796
|
+
- Proof (Playwright): the user can:
|
|
797
|
+
- link the mock provider and see it listed;
|
|
798
|
+
- unlink it;
|
|
799
|
+
- enroll 2FA, saving backup codes, then sign in with a backup code;
|
|
800
|
+
- add a passkey with the virtual authenticator and then remove it;
|
|
801
|
+
- revoke another session (a second browser context gets 401 afterwards);
|
|
802
|
+
- change the password and sign in with the new one.
|
|
803
|
+
|
|
804
|
+
Screenshots go to artifacts.
|
|
805
|
+
- Skills: verify-ui-changes, accessibility-testing, building-terreno-apps
|
|
806
|
+
|
|
807
|
+
- [x] **T24** — Org-admin member security panel
|
|
808
|
+
- Depends on: T4, T9
|
|
809
|
+
- Files: admin-frontend/src/orgs/MemberSecurityPanel.tsx, admin-frontend/src/orgs/MemberSecurityPanel.test.tsx, admin-frontend/src/orgs/useMemberSecurityApi.ts, admin-frontend/src/orgs/OrgMembersScreen.tsx, admin-frontend/src/index.tsx, example-frontend/e2e/org-admin-security.spec.ts, docs/reference/admin-frontend.md, changelog/unreleased/admin-member-security.md
|
|
810
|
+
- Build:
|
|
811
|
+
- Each member row on `OrgMembersScreen` gets a "Security" action, which opens a
|
|
812
|
+
panel with the T6 summary.
|
|
813
|
+
- The panel renders the summary's `actions` generically, with a confirm modal on
|
|
814
|
+
destructive ones, so T7 and T8 actions appear without frontend changes.
|
|
815
|
+
- It calls the routes through the host-bound `adminRequest`
|
|
816
|
+
(`admin-frontend/src/adminRequest.ts`). The package rules forbid new admin
|
|
817
|
+
`injectEndpoints`.
|
|
818
|
+
- Proof:
|
|
819
|
+
- Panel test: actions render from the summary, and confirm calls the action route.
|
|
820
|
+
- Playwright "org-admin resets a member's 2FA": the member then signs in without a
|
|
821
|
+
challenge.
|
|
822
|
+
- Docs: `admin-frontend.md` MemberSecurityPanel section.
|
|
823
|
+
- Skills: building-admin-interfaces, verify-ui-changes, accessibility-testing
|
|
824
|
+
|
|
825
|
+
- [x] **T25** — Platform user security screen
|
|
826
|
+
- Depends on: T8, T24
|
|
827
|
+
- Files: admin-frontend/src/UserSecurityScreen.tsx, admin-frontend/src/UserSecurityScreen.test.tsx, admin-frontend/src/index.tsx, example-frontend/app/admin/users/[id]/security.tsx, example-frontend/e2e/admin-user-security.spec.ts, docs/reference/admin-frontend.md
|
|
828
|
+
- Build: operators get a user security screen that reuses `MemberSecurityPanel`
|
|
829
|
+
against `/users/:id/security`, linked from the admin user record.
|
|
830
|
+
- Proof: Playwright "operator disables and re-enables a user". The user's session gets
|
|
831
|
+
401 while disabled, and sign-in works after enable.
|
|
832
|
+
- Skills: building-admin-interfaces, verify-ui-changes
|
|
833
|
+
|
|
834
|
+
- [x] **T26** — create-terreno-app generates the auth suite
|
|
835
|
+
- Depends on: T4, T21
|
|
836
|
+
- Files: create-terreno-app/src/generate.ts, create-terreno-app/src/__tests__/generate.test.ts, docs/how-to/configure-better-auth.md, changelog/unreleased/cta-auth-suite.md
|
|
837
|
+
- Build: the generated backend config enables:
|
|
838
|
+
- `accountLinking: verified-email`
|
|
839
|
+
- `twoFactor`
|
|
840
|
+
- `passkey` (`rpID` from `FRONTEND_URL`)
|
|
841
|
+
- `lastLoginMethod`
|
|
842
|
+
- `multiSession`
|
|
843
|
+
- `nativeApps`
|
|
844
|
+
|
|
845
|
+
The generated frontend uses `TerrenoLoginScreen` and `settings/security`. The
|
|
846
|
+
login route is the OAuth return page (#1604's handoff and T20 make a separate
|
|
847
|
+
`auth/callback` route unnecessary).
|
|
848
|
+
- Proof: generator test asserting the generated `server.ts` and `app/login.tsx`
|
|
849
|
+
contain the features and the connected screens. The existing generator tests pass.
|
|
850
|
+
- Skills: build-terreno-app, update-docs
|
|
851
|
+
|
|
852
|
+
## Assumptions
|
|
853
|
+
|
|
854
|
+
- **Better Auth only.** Legacy JWT/Passport auth gets no new features.
|
|
855
|
+
- **Plugins come from the catalog.** Better Auth plugins are pinned through the
|
|
856
|
+
`better-auth` catalog version (lock 1.7.4). `@better-auth/passkey` is added to the
|
|
857
|
+
catalog at the matching version (1.7.x on npm).
|
|
858
|
+
- **"Multiple accounts".** Better Auth enforces a unique email per user, so "multiple
|
|
859
|
+
accounts" is the three-way `accountLinking.mode` plus `multiSession` (Q2). Same-email
|
|
860
|
+
duplicate users are not offered.
|
|
861
|
+
- **Recovery routes use Express routers inside plugins.** They follow the `OrgsApp`
|
|
862
|
+
precedent (a framework plugin, an allowed exception to modelRouter actions). The
|
|
863
|
+
example SDK is not regenerated because admin-frontend calls the routes through
|
|
864
|
+
`adminRequest`.
|
|
865
|
+
- **Security actions reuse the existing audit log.** They are recorded as `AuditEvent`
|
|
866
|
+
rows with `source: "security"` through `AuditApp`, not in a new collection.
|
|
867
|
+
- **Grouped recovery tasks.** T7 and T8 each group recovery actions that share one route
|
|
868
|
+
table, so T8 is ordered after T7 for that file.
|
|
869
|
+
- **Dependency chains.**
|
|
870
|
+
- T1→T2→T6→T9→T24→T25 is ordered because each link reads what the previous one
|
|
871
|
+
creates:
|
|
872
|
+
- methods file
|
|
873
|
+
- feature plugin tables
|
|
874
|
+
- mount function
|
|
875
|
+
- endpoints
|
|
876
|
+
- panel
|
|
877
|
+
- T2→T4→T5→T27→T20→T21→T23 is ordered for the same reason:
|
|
878
|
+
- feature wiring
|
|
879
|
+
- mock provider and e2e config
|
|
880
|
+
- link handoff in `betterAuthOAuthHandoff.ts` and `betterAuthClient.ts`
|
|
881
|
+
- challenge headers in `betterAuthClient.ts`
|
|
882
|
+
- controller and `rtk/src/index.ts`
|
|
883
|
+
- security hook
|
|
884
|
+
- screen
|
|
885
|
+
- Docs pages shared across tasks are ordered too:
|
|
886
|
+
- `configure-better-auth.md` goes T2→T3→T20→T21→T26;
|
|
887
|
+
- `account-security.md` goes T6→T7→T8→T10;
|
|
888
|
+
- `set-up-social-login.md` goes T11→T29.
|
|
889
|
+
- All new dependencies land in T2, so one task owns `bun.lock`.
|
|
890
|
+
- **Passkey `rpID` on previews.** It is the full preview host (`netlify.app` is a public
|
|
891
|
+
suffix), so a passkey made on a preview works only on that preview. On stable after
|
|
892
|
+
H3 it is the shared parent domain.
|
|
893
|
+
- **Host-only session cookies.** Same-site subdomains (D6) don't need Better Auth's
|
|
894
|
+
`crossSubDomainCookies`: a host-only cookie on `api.<domain>` is sent on same-site
|
|
895
|
+
fetches. Leaving it off keeps the session cookie away from every other subdomain.
|
|
896
|
+
- **Passkeys made before H3 are lost.** A passkey registered on the `netlify.app` host
|
|
897
|
+
can't be used under the new `rpID`. Users sign in another way and add a new one;
|
|
898
|
+
the example has no real users, and `set-up-social-login.md` says so for consumers.
|
|
899
|
+
- **This repo has no PHI.** The example app holds no PHI. Privacy-sensitive defaults are
|
|
900
|
+
still chosen for consumers like Flourish (Q3).
|
|
901
|
+
- **Native: Android-proven, iOS parked.** Native social sign-in uses the browser
|
|
902
|
+
through the `@better-auth/expo` server plugin (T2, `nativeApps`). T29 proves it on
|
|
903
|
+
the Android emulator. iOS, the native Apple button and native passkeys are X2.
|
|
904
|
+
|
|
905
|
+
## Open questions (recommendation assumed)
|
|
906
|
+
|
|
907
|
+
| ID | Question | Recommendation | Why | If answered differently | Tasks affected |
|
|
908
|
+
| --- | --- | --- | --- | --- | --- |
|
|
909
|
+
| Q10 | Where should the reusable auth screens live? | ui for presentation, rtk for connected screens and hooks, admin-frontend for recovery panels | ui cannot import rtk (rtk depends on ui); admin-frontend already takes an `api` prop | A new `@terreno/auth` package: same tasks, new package scaffolding, files move | T17–T21, T26 |
|
|
910
|
+
| Q4 | Should orgs get security policies (require 2FA, restrict sign-in methods)? | Not in this plan (X3) | The request asks for org-admin recovery; policies are new enforcement surface | +3 tasks: org settings, sign-in enforcement, member UI | T9, T24 |
|
|
911
|
+
| Q5 | Should an admin unlink be allowed to remove a user's last sign-in method? | Only when the user has a verified email, and then a reset email goes out automatically | Otherwise recovery creates new lockouts | Always allowed: drop the 409 guard; never allowed: always 409 | T7 |
|
|
912
|
+
| Q6 | Should recovery routes refuse to start without `AuditApp`? | Yes: throw at startup unless `audit: false` is passed explicitly | HIPAA reviews expect who-did-what for access changes; D2 makes cross-org reach auditable; `AuditApp` is opt-in today | Warn and run unaudited: drop the startup check in T6 | T6 |
|
|
913
|
+
| Q8 | Under the org prefix, should "disable" suspend the membership rather than the global account? | Yes; global `User.disabled` stays platform-only | One org must not lock a user out of every other org | Org-admins disable globally: T9 drops the remap | T9, T24 |
|
|
914
|
+
| Q9 | Should security emails name the org whose admin acted? | No by default (`notifications.includeOrgName`) | On a shared inbox, the org name can reveal care (same concern as Q3) | Name it by default: flip one default | T10 |
|
|
915
|
+
| Q7 | Accept a 5-minute revocation delay for cookie (web) sessions? | Yes, documented; bearer sessions revoke immediately | Better Auth `cookieCache` (`api/src/betterAuthSetup.ts:229-232`); disabling it costs a DB read per request | Disable the cache when recovery is enabled: one config line in T2, tested in T8 | T2, T8 |
|
|
916
|
+
|
|
917
|
+
## Expansions (follow up later)
|
|
918
|
+
|
|
919
|
+
| ID | Idea | Why it came up | Rough size | Depends on |
|
|
920
|
+
| --- | --- | --- | --- | --- |
|
|
921
|
+
| X1 | A dedicated staging environment (Cloud Run service, Netlify site, terraform, CI) | No staging exists today; D1 uses the stable deploy | L | T11 |
|
|
922
|
+
| X2 | iOS proof, the native Sign in with Apple button (`expo-apple-authentication`), and native passkeys | The example is universal; iOS proof needs an EAS build | M | T18, T29 |
|
|
923
|
+
| X3 | Org security policies: require 2FA, allowed methods, session length | "Works for organization management" | M | T9, Q4 |
|
|
924
|
+
| X4 | Magic link, email OTP, Google One Tap | Other sign-in methods for the login screen | M | T2, T20 |
|
|
925
|
+
| X5 | Enterprise SSO (SAML/OIDC per org) | Org-managed identity | L | X3 |
|
|
926
|
+
| X6 | Self-service email change and account deletion | Profile completeness | M | T21 |
|
|
927
|
+
| X7 | Account lockout after repeated failed attempts | "Locked out" recovery | S | T8 |
|
|
928
|
+
| X9 | Previews under the shared domain (Netlify deploy subdomains plus a load balancer for Cloud Run tag URLs), making previews same-site too | D6 makes stable same-site; previews stay cross-site | M | T11, H3 |
|
|
929
|
+
| X8 | Implement the empty `PasswordField` stub; fix the stale rtk rule doc (`betterAuthReducer`) | Found during research | S | none |
|
|
930
|
+
| X10 | Finish T28: the live GitHub sign-in on a preview and on stable, plus the Google and Apple consent-page checks, then H2 | T28 waits on H1 (OAuth apps and secrets); deferred past PR #1613 | S | H1, T11 |
|
|
931
|
+
| X11 | Finish T29: run `example-frontend/maestro/native-social-login.yaml` on an Android emulator and save a screenshot | The emulator companion never stayed up; the flow and the native proxy fix shipped in PR #1613 | S | T22 |
|
|
932
|
+
|
|
933
|
+
## Decisions
|
|
934
|
+
|
|
935
|
+
| ID | Question asked | Answer | What it changes |
|
|
936
|
+
| --- | --- | --- | --- |
|
|
937
|
+
| D1 | There is no staging environment. What is "the final deployed staging one"? | Use the existing stable example deploy | It hosts the registered callbacks and proxies to previews (T11); a real staging env is X1 |
|
|
938
|
+
| D2 (durable) | Who may an org-admin run recovery actions on? | Any active member of their org, even one in other orgs | T9 authorizer; every action is audited (T6) and emails the user (T10) |
|
|
939
|
+
| D3 | An org-admin can attach any existing user by email and then act on them. Which guards? | Light guards: always refuse platform staff and `admin` targets, email the user on every org-admin action, and make a minimum membership age opt-in (default 0) | T9 as written; `orgRecovery.minMembershipDays` defaults to 0 |
|
|
940
|
+
| D4 (durable) | "Leave it to the consumer whether people can make multiple accounts": which knobs? | All three: linking policy (`verified-email` / `explicit` / `never`), account switching (`multiSession`), and open vs closed sign-up | T2 adds `signUp.mode`; T4 reads `AUTH_SIGN_UP`; T20 hides sign-up and maps `signup_disabled` |
|
|
941
|
+
| D6 | Stable frontend and backend will move to subdomains of one domain. Does the plan still work? | Yes. Stable becomes same-site, so cookies work in Safari there; previews stay cross-site, so T27 stays | H1 callbacks go on `api.<domain>`; new H3; T4 adds `PASSKEY_RP_ID`; T11 reads `STABLE_*` URLs; X9 parks same-site previews |
|
|
942
|
+
| D7 | Master merged #1604 after approval: web bearer mode, the OAuth one-time-token handoff, the mock OIDC provider, and a cross-site bearer Playwright project | Reuse them (engineering reconciliation, no product change) | T4 reuses `mock-oidc`; T5 becomes the link handoff; T27 narrows to the 2FA/passkey challenges; T11 extends the handoff to oauth-proxy returns; cross-site proofs use the bearer project, where cookies are never sent, instead of WebKit |
|
|
943
|
+
| D8 | Review found that a refused unverified sign-up left a Better Auth user behind, which a later email verification could turn into a takeover of the legacy User | Refuse before the Better Auth user is created; T8's admin action issues a set-password link instead of marking an email verified | T3 adds a `user.create.before` guard; T8 `verify-email` becomes `issue-sign-in-link` |
|
|
944
|
+
| D9 | Review: should recovery routes let an operator act on a superadmin, or on themselves? | No: refuse targets holding permissions the actor lacks (a `*` target needs a superadmin actor) and refuse self-targeting; self-service lives on the user's own Security screen | T6 adds the guard to the shared route pipeline, so T9 org routes inherit it (alongside D3's platform-staff refusal) |
|
|
945
|
+
| D10 | Review of T7: removing a user's last passkey can lock them out just like unlinking their last provider | Apply Q5's rule to passkey removal too: 409 unless the email is verified, then send a reset first | T7 guards both routes; unlink also points operators to revoke-sessions, since sessions don't record their provider |
|
|
946
|
+
| D11 | Review of T8: a sign-in link sets the password of a user with no inbox, which is a full takeover by whoever issues it | It needs its own RBAC statement, `user:issueSignInLink`, granted to superadmin only by default; expired links can be reissued | T8 |
|
|
947
|
+
| D5 (durable) | Remember name, email and avatar on the login screen by default? | No: method only by default; identity is opt-in (`rememberIdentity`); the example opts in | T19, T20 and T26 as written |
|
|
948
|
+
|
|
949
|
+
## Sign-off
|
|
950
|
+
|
|
951
|
+
Status: approved 2026-10-22
|
|
952
|
+
Cut: 2 rounds, 24 findings (8 blocking): 22 fixed in the plan, 2 left for the human as fundamental questions (Q1, Q2), 0 rebutted. Round 1: 12 fixed; Q1, Q2, Q8 and Q9 added to Open questions. Round 2: 10 fixed; C2 and C3 carried into Q1 and Q2, which go first at sign-off.
|