@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,105 @@
|
|
|
1
|
+
# Consent forms
|
|
2
|
+
|
|
3
|
+
`ConsentApp` gives an app versioned consent forms (terms, privacy, HIPAA, research, and
|
|
4
|
+
custom agreements), a record of every response, and a gate that keeps a user out of the app
|
|
5
|
+
until they have answered every form that applies to them.
|
|
6
|
+
|
|
7
|
+
## Forms and versions
|
|
8
|
+
|
|
9
|
+
A `ConsentForm` has a `slug`, a `version`, localized markdown `content`, and options such as
|
|
10
|
+
required checkboxes, a drawn signature, and whether the user may decline. Publishing
|
|
11
|
+
(`POST /consent-forms/:id/publish`) copies the form to `version + 1`, activates the copy, and
|
|
12
|
+
deactivates every other version of the same slug. Only active forms are shown to users. Publish a new version rather than editing
|
|
13
|
+
a form users have already answered, so each response still points at the text the user saw.
|
|
14
|
+
|
|
15
|
+
An app can narrow which active forms apply to a user with `resolveConsentForms(user, forms)`,
|
|
16
|
+
for example to show a research consent only to enrolled members. Every consent endpoint that
|
|
17
|
+
asks "which forms apply to this user" runs the same resolver.
|
|
18
|
+
|
|
19
|
+
## Responses
|
|
20
|
+
|
|
21
|
+
A `ConsentResponse` records one answer to one form version: `agreed`, `agreedAt`, the locale
|
|
22
|
+
shown, checkbox values, an optional signature, and `formVersionSnapshot`. With
|
|
23
|
+
`auditTrail: true`, it also keeps the user's IP address and user agent and a snapshot of the
|
|
24
|
+
exact content they saw. Responses are append-only. A user who answers again gets a new
|
|
25
|
+
record, so the history of what they agreed to, and when, is never rewritten.
|
|
26
|
+
|
|
27
|
+
## The gate
|
|
28
|
+
|
|
29
|
+
`GET /consents/pending` returns the forms that apply to the user and that the user has not
|
|
30
|
+
answered at the form's current version. The client (`ConsentNavigator` in `@terreno/ui`)
|
|
31
|
+
shows them one at a time until the list is empty. A decline also clears the form from the
|
|
32
|
+
list: the user has answered it.
|
|
33
|
+
|
|
34
|
+
When a form is republished, every user's earlier answer is for an older version, so the new
|
|
35
|
+
version reappears in their gate.
|
|
36
|
+
|
|
37
|
+
## Provisional signatures
|
|
38
|
+
|
|
39
|
+
Some consent is given before the user ever opens the app. A guardian may agree on the phone
|
|
40
|
+
at intake, or staff may record verbal consent during a visit. For compliance the form must
|
|
41
|
+
count as signed from that moment, but the user should still read the form and sign it
|
|
42
|
+
themselves.
|
|
43
|
+
|
|
44
|
+
A provisional signature is a `ConsentResponse` with `provisional: true`. Staff create it with
|
|
45
|
+
`POST /consent-responses/provisional`, naming the target user, the form, and a required note
|
|
46
|
+
that explains how consent was obtained. The response records:
|
|
47
|
+
|
|
48
|
+
- `agreed: true` and `agreedAt` (server time)
|
|
49
|
+
- `provisionalSignedBy`: the staff member who recorded it
|
|
50
|
+
- `provisionalNote`: the note (trimmed, at most 2000 characters)
|
|
51
|
+
- `formVersionSnapshot`, plus `contentSnapshot`, `provisionalSignerIp`, and
|
|
52
|
+
`provisionalSignerUserAgent` when `auditTrail` is on
|
|
53
|
+
|
|
54
|
+
`ipAddress`, `userAgent`, `signature`, and `signedAt` stay empty. Those fields describe the
|
|
55
|
+
user's own device and signature, and the user did not sign.
|
|
56
|
+
|
|
57
|
+
### Why a provisional signature does not clear the gate
|
|
58
|
+
|
|
59
|
+
Only the user's own responses remove a form from `/consents/pending`. A provisional signature
|
|
60
|
+
is a record that someone else vouched for the user's consent. It is not the user's consent in
|
|
61
|
+
their own words. Keeping the form in the gate means the user still meets it, reads it, and
|
|
62
|
+
agrees or declines. The gate tells them who signed on their behalf and when, but not the
|
|
63
|
+
signer's note: that stays in the admin audit history. The user's own answer is stored as a separate response, so both records
|
|
64
|
+
stay for audit.
|
|
65
|
+
|
|
66
|
+
A separate `ProvisionalConsent` model was rejected. A field on `ConsentResponse` keeps one
|
|
67
|
+
audit trail per user and reuses the existing admin screens, populate paths, and PDF code.
|
|
68
|
+
|
|
69
|
+
### Voiding a provisional signature
|
|
70
|
+
|
|
71
|
+
A provisional signature recorded in error (the wrong member, the wrong form) is voided with
|
|
72
|
+
`POST /consent-responses/:id/void` and a required reason. Voiding sets `voidedAt`,
|
|
73
|
+
`voidedBy`, and `voidReason`; it never deletes the record, so the audit history shows both
|
|
74
|
+
the signature and its void. A voided signature no longer counts as consent, and the user's
|
|
75
|
+
gate no longer mentions it. Only provisional signatures can be voided: the user's own
|
|
76
|
+
answers are theirs to change by answering again.
|
|
77
|
+
|
|
78
|
+
### What counts as signed
|
|
79
|
+
|
|
80
|
+
`getConsentStatuses` (and `GET /consent-responses/status`) is the "considered signed"
|
|
81
|
+
contract. For each form that applies to the user, only non-voided responses to the current
|
|
82
|
+
version count. The user's own latest answer always wins: agreeing makes the form `signed`, declining
|
|
83
|
+
makes it `declined`. Without one, a provisional signature makes it `provisional`, which
|
|
84
|
+
counts as consented. Otherwise it is `pending`. App code that needs to know whether a user
|
|
85
|
+
may proceed (for example before scheduling a visit) checks `isConsented` on these rows
|
|
86
|
+
instead of reading responses directly.
|
|
87
|
+
|
|
88
|
+
### Who may sign for another user
|
|
89
|
+
|
|
90
|
+
Pass `accessControl` to `ConsentApp` to use RBAC. Any authenticated user whose roles grant
|
|
91
|
+
`consentResponse:signOnBehalf` may then sign for others. The built-in `admin` role includes
|
|
92
|
+
that action, and existing unsealed admin roles gain it the next time roles are seeded. The
|
|
93
|
+
merge runs on every seed, so removing `signOnBehalf` from the `admin` role does not stick.
|
|
94
|
+
To keep admins from signing, give them a custom role instead of `admin`.
|
|
95
|
+
Without `accessControl`, only users with `admin: true` may sign, and `ConsentApp` logs a
|
|
96
|
+
warning at register.
|
|
97
|
+
|
|
98
|
+
There is no relationship check: a permitted signer may sign for any user. A signer cannot
|
|
99
|
+
sign for themselves, and the form must be active and must apply to the target user under
|
|
100
|
+
`resolveConsentForms`. The same permission covers reading another user's consent statuses.
|
|
101
|
+
When `AuditApp` is registered, every provisional signature, void, and status read writes an
|
|
102
|
+
`AuditEvent` naming the actor and the target user. Audit events gain a `read` operation and
|
|
103
|
+
a `viewed` verb for these reads.
|
|
104
|
+
|
|
105
|
+
Reference: [ConsentApp](../reference/api.md#consent-forms-consentapp).
|
|
@@ -27,6 +27,7 @@ that file (and its imports) to a working example. Gaps stay explicit.
|
|
|
27
27
|
| File upload / GCS | yes — `example-backend/src/api/settings.ts`, AI file routes in `example-backend/src/api/ai.ts`, `file-uploads` flag in `example-backend/src/api/fileUploads.ts` | yes — `example-frontend/app/(tabs)/documents.tsx` (Files section, every signed-in user, 1 upload / IP / min), `example-frontend/app/(tabs)/ai.tsx`, `example-frontend/app/gcs-settings.tsx` | The `file-uploads` feature flag hides upload controls and rejects uploads when off. A missing flag leaves uploads enabled. Deployed bucket is terraform-managed (`google_storage_bucket.example_documents`). |
|
|
28
28
|
| Better Auth | yes — `example-backend/src/utils/betterAuthConfig.ts`, `BetterAuthApp` in `example-backend/src/server.ts` | yes — `example-frontend/app/login.tsx`, `example-frontend/lib/betterAuth.ts` | Default `AUTH_PROVIDER` is `better-auth`. |
|
|
29
29
|
| syncdb local-first | yes — `SyncApp` in `example-backend/src/server.ts` | yes — `example-frontend/store/syncdb.ts`, `example-frontend/app/(tabs)/index.tsx` | PR #869 closed without merge; `@terreno/syncdb` shipped on the launch line and is the example todos path. |
|
|
30
|
+
| List/detail layout | n/a — reuses the synced todos | yes — `example-frontend/app/todo-browser/` (`ListDetailLayout` group, opened from the todos screen's Browse button), list in `example-frontend/components/TodoBrowserList.tsx` | Phones push each todo; at 768px and wider the list stays on the left. e2e: `example-frontend/e2e/todo-browser.spec.ts` (phone, tablet, deep link, resize, keyboard). The demo inbox at `demo/app/demo/list-detail/` shows the same layout with static data. Steps: [build-list-detail-screens.md](../how-to/build-list-detail-screens.md). |
|
|
30
31
|
| SyncDB diagnostics | n/a | yes — `debug` registration in `example-frontend/store/syncdb.ts`, debugger UI at `example-frontend/app/syncdb-debug.tsx` | `terreno-mcp-local` can inspect, snapshot/compare/merge, mutate, flush, reconcile/resync, and exercise offline/conflict paths against the running development client. |
|
|
31
32
|
| RBAC | yes — `example-backend/src/rbacRoles.ts` (including optional `aiObservabilityViewer` / `aiObservabilityOperator` roles), `access.ts`, `accessControl: access` on `ObservabilityApp` / `AdminApp`, `access: {resource: "todo"}` on the todo router | yes — `example-frontend/app/admin/roles.tsx` | Shipped. Plan: [rbac-permissions.md](../implementationPlans/rbac-permissions.md). |
|
|
32
33
|
| Organizations | yes — `createAccess({organizations: true})`, `organizationSettingsSchema`, `orgScopedPlugin` on projects | yes — admin org directory/settings at `example-frontend/app/admin/orgs/` | Settings live on `Organization.settings`; example registers a nested timezone schema. |
|
|
@@ -35,7 +35,7 @@ Terreno 57.
|
|
|
35
35
|
## What you start doing
|
|
36
36
|
|
|
37
37
|
1. **Conflict resolution UI** — When the server rejects a mutation (409 / `sync:nack`), the user must choose a strategy (`useServer` or `keepMine`). Use `ConflictSheet` from `@terreno/ui` or build your own with `useConflicts`.
|
|
38
|
-
2. **Sync-status UX** — Surface offline state, queued mutations, and drain progress with `useSyncStatus` (and optionally `SyncStatusBanner`).
|
|
38
|
+
2. **Sync-status UX** — Surface offline state, queued mutations, and drain progress with `useSyncStatus` (and optionally `SyncStatusBanner`, which waits `offlineGraceMs` — 3s by default — before showing "Offline" so startup and brief socket drops do not flash it).
|
|
39
39
|
3. **Encryption at rest (web)** — Web persistence uses AES-GCM via the default `createServerKeyProvider`; understand key fetch and `onDecryptFailure` for support flows.
|
|
40
40
|
4. **Key lifecycle on logout** — Call `syncDb.stop()` and consider `wipeLocalData` when the authenticated user changes so one account does not read another's local store.
|
|
41
41
|
5. **Backend sync registration** — Every synced collection needs `syncPlugin`, `isDeletedPlugin`, and a `sync` block on `modelRouter`, plus `SyncApp` and `RealtimeApp` on the server. Optional `sync.adminBroadcast: true` also emits `sync:delta` to `{collection}|admin`. Admin clients subscribe with `mode: "window"` (`createSyncDb({windowCollections})`) so they do not snapshot or reconcile that collection; they call `hydrateWindow` (REST membership ids + `GET /sync/entities`). Pass the same `accessControl` into `SyncApp` that `AdminApp` uses so `{collection}|admin` requires `admin:access`. `AdminApp.register` also installs that model's admin list/read/`queryFilter` on the admin window so hydrate and live `|admin` deltas match `/admin` REST. Without RBAC, `user.admin` is enough for the shell gate. Live deltas on that stream apply only to ids already in the window; Refresh is a REST re-query plus `hydrateWindow`.
|
|
@@ -47,6 +47,13 @@ Tenant isolation uses multiple checks rather than one convention:
|
|
|
47
47
|
admin models whose schema contains `organizationId`. Models without that path
|
|
48
48
|
remain platform-scoped.
|
|
49
49
|
|
|
50
|
+
Adding, changing, or removing a membership through `POST` / `PATCH` / `DELETE
|
|
51
|
+
/orgs/:id/members` clears that user's cached permissions, so a new org-admin can open the
|
|
52
|
+
admin page (and a demoted or removed one loses it) on their next request instead of after
|
|
53
|
+
the permission cache's 30-second TTL. Disabling (`PATCH /orgs/:id` with `disabled: true`) or
|
|
54
|
+
deleting (`DELETE /orgs/:id`) an organization suspends all of its memberships and clears the
|
|
55
|
+
cached permissions of every member the same way.
|
|
56
|
+
|
|
50
57
|
## Disabled organizations
|
|
51
58
|
|
|
52
59
|
When `Organization.disabled` is `true`, tenant context and membership mutations
|
|
@@ -64,6 +71,35 @@ stop, but operators can still manage the organization record itself:
|
|
|
64
71
|
Disabling or deleting an organization suspends its memberships. Re-enabling does
|
|
65
72
|
not automatically reactivate them.
|
|
66
73
|
|
|
74
|
+
## Member recovery
|
|
75
|
+
|
|
76
|
+
When the app registers `AccountSecurityApp`, an org-admin can help a locked-out member without
|
|
77
|
+
platform staff. `OrgsApp` mounts the same recovery handlers the platform uses at
|
|
78
|
+
`/orgs/:orgId/members/:userId/security/*`, behind an org authorizer.
|
|
79
|
+
|
|
80
|
+
- **Reach.** An org-admin may act on any active member of their org, even one who also belongs
|
|
81
|
+
to other orgs (D2). A suspended member, or a user outside the org, answers `404`, so the routes
|
|
82
|
+
do not reveal who exists elsewhere.
|
|
83
|
+
- **Guards.** Platform staff (`superadmin`, `operator`) and `admin: true` users are never reachable
|
|
84
|
+
through an org (D3). Neither is anyone who holds a permission the org-admin lacks, nor the
|
|
85
|
+
org-admin themselves (D9). Disabled orgs answer `403`.
|
|
86
|
+
- **Disable means suspend.** One org must not lock a user out of every other org, so the org
|
|
87
|
+
path has no account disable. It offers `suspend-membership` instead, which suspends the
|
|
88
|
+
membership in that org only (Q8). The user still signs in and keeps their other orgs.
|
|
89
|
+
`User.disabled` and `issue-sign-in-link` stay platform-only.
|
|
90
|
+
- **Attach-then-act.** An org-admin can attach any existing user by email
|
|
91
|
+
(`POST /orgs/:id/members`). Every org-admin action is audited with the org's id and emails the
|
|
92
|
+
user, so they learn about it ([notifications](../reference/account-security.md#notifications);
|
|
93
|
+
the org is named only with `notifications.includeOrgName`). Apps that want more can set
|
|
94
|
+
`orgRecovery.minMembershipDays` to refuse members who joined too recently.
|
|
95
|
+
- **Platform staff** holding `user:manageSecurity` pass the org checks. The target must still be
|
|
96
|
+
an active member of the org in the path. Their reach check is the platform one: both sides are
|
|
97
|
+
resolved outside the organization, so an org-only grant does not put the member out of reach.
|
|
98
|
+
|
|
99
|
+
See [Recover a locked-out user](../how-to/recover-locked-out-user.md) for who can do what, and
|
|
100
|
+
the [account security reference](../reference/account-security.md#org-admin-routes) for every
|
|
101
|
+
response.
|
|
102
|
+
|
|
67
103
|
## Why organizations are optional
|
|
68
104
|
|
|
69
105
|
Automatically registering routes, roles, or Mongoose models would change
|
|
@@ -83,6 +119,7 @@ multi-tenant-ready apps.
|
|
|
83
119
|
## Related
|
|
84
120
|
|
|
85
121
|
- [Add organizations](../how-to/add-organizations.md)
|
|
122
|
+
- [Recover a locked-out user](../how-to/recover-locked-out-user.md)
|
|
86
123
|
- [API reference](../reference/api.md#organizations)
|
|
87
124
|
- [Admin backend reference](../reference/admin-backend.md#organization-scoping)
|
|
88
125
|
- [Admin frontend reference](../reference/admin-frontend.md#orgdirectoryscreen)
|
|
@@ -112,3 +112,7 @@ Install both from the marketplaces described in
|
|
|
112
112
|
[install agent skills](../how-to/install-agent-skills.md). Claude Code resolves a plugin
|
|
113
113
|
skill's command from its frontmatter `name`, which is why the two `*-claude/` trees are
|
|
114
114
|
generated with shortened names by `bun run skills:sync`.
|
|
115
|
+
|
|
116
|
+
## Related
|
|
117
|
+
|
|
118
|
+
- [AI development loop](ai-development-loop.md): the tool layer these stages use to see the running app
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Responsive design
|
|
2
|
+
|
|
3
|
+
Terreno apps run on desktop web and native mobile. Responsive behavior follows the workflow, the available viewport, and how important that workflow is on that surface.
|
|
4
|
+
|
|
5
|
+
The goal is not to make every screen work everywhere. The goal is the best experience on the devices and workflows people actually use. Design specs name the supported breakpoints and the support level for each feature. Implement and test only those behaviors.
|
|
6
|
+
|
|
7
|
+
Breakpoints are viewport width in points, not a device model and not a physical screen resolution. Browser chrome, a resized window, split screen, and display scaling all change the available width.
|
|
8
|
+
|
|
9
|
+
## Surfaces
|
|
10
|
+
|
|
11
|
+
Primary devices define the design target. Support for other devices comes from the breakpoint system.
|
|
12
|
+
|
|
13
|
+
| Surface | Who it is for | Where it runs | Design target | Role |
|
|
14
|
+
| --- | --- | --- | --- | --- |
|
|
15
|
+
| Desktop web | People doing data-heavy work | Web, validated in Chrome | 13" laptop at `xl` (≥ 1280pt), such as a MacBook Air 13" | Primary experience. Full functionality, including complex review and multi-panel workflows. |
|
|
16
|
+
| Primary mobile | People whose main experience is the phone or tablet | iOS and Android | Large phone / small tablet at native `lg` (600–1023pt), such as a Samsung A11 | Full supported mobile workflows. |
|
|
17
|
+
| Reduced mobile | People who need emergency or lightweight access | iOS and Android | Common phones | A reduced set of workflows. Not a replacement for desktop. |
|
|
18
|
+
|
|
19
|
+
Other browsers are outside the desktop support contract. Do not create a layout for an individual phone or tablet model.
|
|
20
|
+
|
|
21
|
+
## Support levels
|
|
22
|
+
|
|
23
|
+
Every feature names one of these levels at each breakpoint it claims.
|
|
24
|
+
|
|
25
|
+
| Level | Definition | Example |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Fully supported | Designed and tested at this breakpoint. Intended workflows work. | A filter with search, groups, saved filters, and bulk actions on desktop `lg` and `xl`. |
|
|
28
|
+
| Supported with adaptation | Still fully usable. Layout or interaction is simpler so it fits. | The same filter on a tighter width becomes one category at a time, or moves advanced options behind an "Advanced filters" action. |
|
|
29
|
+
| Functionally available | Essential workflows can be finished. Complexity is intentionally omitted. | On reduced mobile, the filter keeps search and one or two high-value filters. |
|
|
30
|
+
| Not supported | Not available, not optimized, or not guaranteed. | The advanced filter below the minimum supported breakpoint. The app may still open. Layout and functionality are not guaranteed. |
|
|
31
|
+
|
|
32
|
+
### What a feature spec must define
|
|
33
|
+
|
|
34
|
+
| Item | Required? |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| Supported surfaces (desktop web, primary mobile, reduced mobile) | Yes |
|
|
37
|
+
| Supported breakpoints (`sm`, `md`, `lg`, `xl`) | Yes |
|
|
38
|
+
| Support level at each of those breakpoints | Yes |
|
|
39
|
+
| Behavior when a breakpoint is not supported | Yes |
|
|
40
|
+
| Layout changes at supported breakpoints | When the layout changes |
|
|
41
|
+
|
|
42
|
+
## Breakpoints
|
|
43
|
+
|
|
44
|
+
Tokens are platform-specific. `lg` is not the same width on web and native. The tables and the constants `WEB_BREAKPOINT_MIN_WIDTH` and `NATIVE_BREAKPOINT_MIN_WIDTH` are the same contract. API details: [Media query helpers](../reference/ui.md#media-query-helpers).
|
|
45
|
+
|
|
46
|
+
### Desktop web
|
|
47
|
+
|
|
48
|
+
The desktop web app is optimized for desktop. The minimum supported desktop breakpoint is `lg`.
|
|
49
|
+
|
|
50
|
+
| Category | Token | Viewport | Meaning |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| Primary desktop | `xl` | ≥ 1280pt | Full desktop layouts, multi-panel workflows, dense data. |
|
|
53
|
+
| Smaller desktop | `lg` | 1024–1279pt | Minimum supported desktop. Collapse secondary content, adjust panels, or simplify controls. Keep core workflows. |
|
|
54
|
+
| Below supported desktop | below `lg` | < 1024pt | Not a supported desktop experience. The app may stay reachable. Layouts are not guaranteed. |
|
|
55
|
+
|
|
56
|
+
On web, `sm` (320) and `md` (375) still classify widths below 1024 so a layout can stay reachable.
|
|
57
|
+
|
|
58
|
+
Test `xl` fully, including complex layouts and multi-panel interactions. Test `lg` to confirm critical workflows stay usable when space shrinks. Do not do breakpoint-specific testing below `lg`. Degrade gracefully there when it is cheap.
|
|
59
|
+
|
|
60
|
+
### Native mobile
|
|
61
|
+
|
|
62
|
+
| Category | Token | Viewport | Meaning |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| Extra large | `xl` | ≥ 1024pt | Not a supported mobile layout. Use desktop web where it exists. |
|
|
65
|
+
| Large mobile / tablet | `lg` | 600–1023pt | Primary tablet experience. Optimize supported mobile workflows here. |
|
|
66
|
+
| Standard mobile | `md` | 375–599pt | Common phone. Supported workflows stay usable. |
|
|
67
|
+
| Small mobile | `sm` | 320–374pt | Small phone. Critical workflows stay functional. Extra scrolling or a simpler layout is expected. |
|
|
68
|
+
| Below supported mobile | below `sm` | < 320pt | Not supported. The app may stay reachable. Layouts are not guaranteed. |
|
|
69
|
+
|
|
70
|
+
Test native `lg`, `md`, and `sm` unless the spec says otherwise. Do not do breakpoint-specific testing below `sm`.
|
|
71
|
+
|
|
72
|
+
A larger mobile screen does not automatically get the desktop layout. Use a breakpoint only when the layout actually changes.
|
|
73
|
+
|
|
74
|
+
`isSupportedDesktopWidth({width})` is the desktop floor: web `lg` and native `xl`, both 1024pt. Below that, do not ship a desktop multi-panel layout.
|
|
75
|
+
|
|
76
|
+
## Adapting a layout
|
|
77
|
+
|
|
78
|
+
### Desktop
|
|
79
|
+
|
|
80
|
+
Support a primary laptop display, a resized browser window, split screen, and a smaller window that is still inside `lg` or `xl`. Keep the workflow as width drops inside that range.
|
|
81
|
+
|
|
82
|
+
When width drops:
|
|
83
|
+
|
|
84
|
+
- Collapse secondary panels.
|
|
85
|
+
- Allow horizontal scrolling for a dense table.
|
|
86
|
+
- Move secondary actions into an overflow menu.
|
|
87
|
+
- Reduce spacing before removing content.
|
|
88
|
+
|
|
89
|
+
Do not shrink content until it is unreadable, compress a layout into a strip, or hide a critical workflow with no other way to reach it.
|
|
90
|
+
|
|
91
|
+
### Native
|
|
92
|
+
|
|
93
|
+
Mobile layouts favor finishing the task over matching desktop feature parity.
|
|
94
|
+
|
|
95
|
+
Reduced mobile, when a desktop workflow is too complex:
|
|
96
|
+
|
|
97
|
+
- Simplify the workflow.
|
|
98
|
+
- Reduce the available actions.
|
|
99
|
+
- Keep the high-value, time-sensitive tasks.
|
|
100
|
+
- Send people to desktop when the workflow needs it.
|
|
101
|
+
|
|
102
|
+
Do not recreate a desktop layout on mobile, support every desktop workflow, show a dense table or a multi-panel layout, or require a lot of horizontal scrolling.
|
|
103
|
+
|
|
104
|
+
Primary mobile should stay clear, readable, touch-friendly, and focused on the supported workflows.
|
|
105
|
+
|
|
106
|
+
## Checklist
|
|
107
|
+
|
|
108
|
+
Before building a new experience, answer these:
|
|
109
|
+
|
|
110
|
+
1. Is this desktop-only, mobile-only, or both? Is the workflow required on reduced mobile, or only on primary mobile?
|
|
111
|
+
2. What is the minimum supported width? When space decreases, does content collapse, stack, scroll, or hide?
|
|
112
|
+
3. What is the primary goal, which information is essential, and which actions are secondary?
|
|
113
|
+
4. Does this need a breakpoint, or a mobile variant of a component? Does it match an existing responsive pattern?
|
|
114
|
+
|
|
115
|
+
## Framework component audit
|
|
116
|
+
|
|
117
|
+
Audited against this policy. Pixel cutoffs that were not a breakpoint token were moved onto `isSupportedDesktopWidth`.
|
|
118
|
+
|
|
119
|
+
| Component | Result |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `Box` responsive props (`sm` / `md` / `lg` / `xl`) | Compliant. One shared dimension listener. The largest matching token wins. |
|
|
122
|
+
| `DashboardGrid` | Compliant. Column counts follow `sm`, `md`, and `lg` (`xl` uses the `lg` count). |
|
|
123
|
+
| `SplitPage` (web) | Compliant. Side-by-side layout follows `isNarrowViewport()` unless the caller passes `narrowBelowWidth`. |
|
|
124
|
+
| `SplitPage` (native) | Compliant. Stacked list/detail at every native width, including tablet `lg`. Native tablets do not inherit the desktop split. |
|
|
125
|
+
| `Modal` | Compliant. Action sheet on native, centered dialog on web. |
|
|
126
|
+
| `DataTable` | Partial. Web keeps inline filters. Native collapses filters into one sheet. Horizontal scroll stays on for wide tables, including native, which is more scrolling than reduced mobile should require. |
|
|
127
|
+
| `Text` / `Heading` | Compliant. Separate web and native type scales. Type does not shrink again inside `sm`. |
|
|
128
|
+
| `Card` | Fixed. Container and display variants used a 768pt cutoff. They now follow the desktop floor. |
|
|
129
|
+
| `MarkdownEditor` | Fixed. Side-by-side edit/preview used a 768pt cutoff. It now follows the desktop floor. |
|
|
130
|
+
| `AdminShell` | Fixed. The sidebar became a hamburger drawer below 768pt. It now does that below the desktop floor. |
|
|
131
|
+
| `AdminFilterDrawer` | Fixed. The bottom sheet used the same 768pt cutoff. It now follows the desktop floor. `ADMIN_FILTER_MOBILE_BREAKPOINT` is 1024. |
|
|
132
|
+
| `AdminHome` | Fixed. Main and sidebar stay side by side only at or above the desktop floor, and only when the dashboard is not embedded. Below the floor they stack. |
|
|
133
|
+
| Admin SPA shell | Compliant. It wraps `AdminShell`, so it uses the same floor. |
|
|
134
|
+
| Example app screens | Compliant. `maxWidth={800}` is a content measure. Toolbars that sit in a row also wrap. No second pixel cutoff. |
|
|
135
|
+
| Sync debugger | Fixed. The event list and JSON inspector stack below the desktop floor. The screen still uses raw `View` and `Text` on purpose. |
|
|
136
|
+
| Demo headers | Fixed. Back-button visibility and header margin followed `isNarrowViewport()` once at module load. They now follow the desktop floor and update when the window resizes. |
|
|
137
|
+
| Demo stories (Tooltip, Multiselect, Accordion) | Fixed. Width switches followed a one-time `isNarrowViewport()` read. They now follow the desktop floor and update on resize. |
|
|
138
|
+
| Admin action rows | Partial. Some rows (`ConsentHistory`, consent editors) stay in one row without `wrap`. They are not a second breakpoint. |
|
|
139
|
+
| `DateTimeField` | Partial. Inline controls switch to an action sheet when the parent is narrower than 395pt, and also when `isNarrowViewport()` is true. 395pt is the field's own minimum, not a viewport token. |
|
|
140
|
+
| `Image` (`fullWidth`) | Gap. Width is read from `Dimensions` once at module load, so it does not track resize or rotation. |
|
|
141
|
+
| `SideDrawer` | Partial. Width is 40% on web and 95% on native, by platform rather than by breakpoint token. |
|
|
142
|
+
|
|
143
|
+
Do not add a new raw pixel cutoff. Use `Box` breakpoint props, `useResponsiveBreakpoint`, or `isSupportedDesktopWidth`.
|
|
@@ -1342,6 +1342,17 @@ One easy-to-use harness inside `@terreno/ai` that runs long-lived agentic work d
|
|
|
1342
1342
|
|
|
1343
1343
|
---
|
|
1344
1344
|
|
|
1345
|
+
## list-detail-layout
|
|
1346
|
+
|
|
1347
|
+
**Title:** `ListDetailLayout: a route-driven list/detail layout for phone and tablet`
|
|
1348
|
+
|
|
1349
|
+
**Labels:** `area:ui`, `type:feature`
|
|
1350
|
+
**Project fields:** Area=`ui`, Target=`Next`, Impact=`Feature`, IP=`list-detail-layout`, Status=`Planned`
|
|
1351
|
+
|
|
1352
|
+
- **Implementation plan:** [list-detail-layout.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/implementationPlans/list-detail-layout.md)
|
|
1353
|
+
|
|
1354
|
+
---
|
|
1355
|
+
|
|
1345
1356
|
# Shipped, umbrella, and declined IPs
|
|
1346
1357
|
|
|
1347
1358
|
Tracking issues created for IPs that previously lacked a `**Roadmap issue:**` header.
|
|
@@ -43,6 +43,11 @@ const routerDescription = describeModelForRouter(Todo, {
|
|
|
43
43
|
|
|
44
44
|
System paths (`_id`, `__v`, `created`, `updated`, `deleted`) are marked with `system: true`.
|
|
45
45
|
|
|
46
|
+
`required` is true only when the field is required on every document. A conditional
|
|
47
|
+
`required` (a function, such as "required when `provisional` is true") leaves it false, so
|
|
48
|
+
OpenAPI and generated SDKs type the field as optional; Mongoose still enforces the condition
|
|
49
|
+
on save.
|
|
50
|
+
|
|
46
51
|
## Views (not canonical)
|
|
47
52
|
|
|
48
53
|
| Surface | Function | Notes |
|
|
@@ -27,15 +27,18 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
|
|
|
27
27
|
- [Durable background jobs](background-jobs.md) — `@terreno/jobs`: enqueue, workers, schedules, Cloud Tasks / Vercel Queues
|
|
28
28
|
- [Create a Mongoose model](create-a-model.md) — Define models with proper conventions
|
|
29
29
|
- [Enable the framework audit log](audit-log.md) — `AuditApp`, per-router `audit`, append-only events
|
|
30
|
+
- [Sign consent on a user's behalf](sign-consent-on-behalf.md) — Provisional consent signatures: RBAC grant, signer screen, status checks, void, print
|
|
30
31
|
- [Seed a database](seed-a-database.md) — Idempotent sync, dry runs, and guarded reset-and-reseed
|
|
31
32
|
- [Run MongoDB migrations](run-mongodb-migrations.md) — Versioned `up`/`down` files, `terreno-migrate`, production gate
|
|
32
33
|
- [Add GitHub OAuth authentication](add-github-oauth.md) — Enable GitHub login for your API
|
|
33
34
|
- [Configure Better Auth](configure-better-auth.md) — Set up Better Auth with social OAuth (Google, GitHub, Apple)
|
|
35
|
+
- [Set up social login](set-up-social-login.md) — Register Google, GitHub, and Apple once on the stable deploy, proxy previews through it, and move stable onto one domain
|
|
34
36
|
- [Password reset and email verification](password-reset.md) — JWT routes, comms templates, Better Auth hooks
|
|
37
|
+
- [Recover a locked-out user](recover-locked-out-user.md) — Platform and org-admin recovery: 2FA, passkeys, providers, sessions, suspend
|
|
35
38
|
- [Add WebSocket integration](websocket-integration.md) — Set up real-time Socket.io connections
|
|
36
39
|
- [Expose Model Context Protocol tools](expose-mcp-tools.md) — Turn `modelRouter` models into MCP tools an LLM can call
|
|
37
|
-
- [Set up Terreno MCP](set-up-terreno-mcp.md) —
|
|
38
|
-
- [Debug with MCP](debug-with-mcp.md) —
|
|
40
|
+
- [Set up Terreno MCP](set-up-terreno-mcp.md) — Connect Cursor or Claude Code to the hosted docs/codegen server and the local runtime server
|
|
41
|
+
- [Debug with MCP](debug-with-mcp.md) — Follow a failure from symptom to verified fix with the local MCP tools
|
|
39
42
|
- [Connect an MCP client with a service token](connect-mcp-service-token.md) — Mint a personal `mcp_` key for Perplexity and other remote clients
|
|
40
43
|
- [Enable MCP OAuth for a JWT app](enable-mcp-oauth.md) — Let Claude.ai and ChatGPT connect to `/mcp` through OAuth 2.1 with your own consent screen
|
|
41
44
|
- [Install agent skills](install-agent-skills.md) — `npx skills`, the Cursor plugin, the Codex plugin, or the Claude Code plugin
|
|
@@ -48,7 +51,9 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
|
|
|
48
51
|
- [Call external APIs](call-external-apis.md) — Authenticated HTTP client, retries, and error normalization for third-party integrations
|
|
49
52
|
- [Upgrade banner](upgrade-banner.md) — Soft warning and hard-block app update UX
|
|
50
53
|
- [Upgrade Terreno](upgrade-terreno.md) — Lockstep `@terreno/*` bumps, note range, order, rollback
|
|
54
|
+
- [Automate Terreno upgrades](automate-terreno-upgrades.md) — Hourly workflow that launches one upgrade agent when a week-old release is out and no upgrade is open
|
|
51
55
|
- [Track upstream Expo](track-upstream-expo.md) — Morning probe for SDK betas; `release-X.Y.Z` loop log + native fingerprint deps
|
|
56
|
+
- [Build a list/detail screen](build-list-detail-screens.md) — `ListDetailLayout` route group: list on a phone, list + detail panes on a tablet, selection in the URL
|
|
52
57
|
- [Show loading skeletons](show-loading-skeletons.md) — `loading`, `loadingComponent`, and `loadingAnimation` on Box, Card, charts, and Page; `withLoading` for custom components
|
|
53
58
|
- [Compose charts in a dashboard grid](charts-and-dashboards.md) — `LineChart` / `BarChart` / `AreaChart` / `DonutChart` inside `Card`s in `DashboardGrid` (ops-dashboard scorecards and spans: [dashboard-chart-parity](../implementationPlans/dashboard-chart-parity.md))
|
|
54
59
|
- [Compare rendered chart snapshots](compare-chart-rendered-snapshots.md) — PNG goldens for the chart visual gallery, not JSON snapshots
|
|
@@ -38,7 +38,10 @@ modelRouter("/todos", Todo, {
|
|
|
38
38
|
| PATCH `/:id/:field/:itemId` | `updated` | `arrayUpdate` | Changed fields; `recordId` set |
|
|
39
39
|
| DELETE `/:id/:field/:itemId` | `updated` | `arrayRemove` | Changed fields; `recordId` set |
|
|
40
40
|
|
|
41
|
-
`source` is `modelRouter` for these writes.
|
|
41
|
+
`source` is `modelRouter` for these writes. `ConsentApp` also writes `modelRouter` events for
|
|
42
|
+
provisional consent actions: `created`/`create` for a provisional signature, `updated`/`update` for a void, and
|
|
43
|
+
`viewed`/`read` when staff read another user's consent statuses. `read` and `viewed` mark
|
|
44
|
+
audited reads; no other surface writes them. Secrets (`password`, `hash`, `salt`, `token`,
|
|
42
45
|
`secret`, `refreshToken`) are omitted at every object and array depth, including compound
|
|
43
46
|
keys such as `tokenHash`. Extra `redact` names merge with that list as exact field names.
|
|
44
47
|
|
|
@@ -51,6 +54,22 @@ Admin diffs also omit each model's `hiddenFields` and `excludeFields`.
|
|
|
51
54
|
|
|
52
55
|
RBAC mutations fan into the same collection when `createAccess({auditSink: persistRbacAuditToAuditEvent})` is set. `source` is `rbac`.
|
|
53
56
|
|
|
57
|
+
### 4. Account security actions
|
|
58
|
+
|
|
59
|
+
`AccountSecurityApp` writes the same collection for account recovery. It refuses to start
|
|
60
|
+
unless `AuditApp` is registered (or you pass `audit: false`).
|
|
61
|
+
|
|
62
|
+
| `source` | Written by | `modelName` / `recordId` | `after` |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| `admin` | AdminApp POST/PATCH/DELETE | Model / document id | Changed fields |
|
|
65
|
+
| `modelRouter` | `modelRouter` with `audit` | Model / document id | Changed fields |
|
|
66
|
+
| `rbac` | `createAccess({auditSink: persistRbacAuditToAuditEvent})` | RBAC target | Role change |
|
|
67
|
+
| `security` | `AccountSecurityApp` recovery actions (2FA reset and later actions) | `User` / target user id | `{securityAction, organizationId?}` |
|
|
68
|
+
|
|
69
|
+
A recovery call that changes nothing (resetting 2FA that is already off) writes no row. List
|
|
70
|
+
one user's security history with `GET /audit-events?source=security&recordId=<userId>`. Routes
|
|
71
|
+
and action names: [Account security reference](../reference/account-security.md).
|
|
72
|
+
|
|
54
73
|
`organizationId` is copied onto the event when `req.organization` is set (`id` or `_id`), otherwise from the request org AsyncLocalStorage, then `X-Organization-Id`, then the mutated document's `organizationId` string. Socket admin-window mutates attach `organizationId` on the payload so non-tenant models still land in the selected organization's audit log. The field is omitted when none of those exist. Admin list/read for `AuditEvent` is not organization-scoped, so unscoped rows stay visible when no organization is selected.
|
|
55
74
|
|
|
56
75
|
`actorId` comes from `req.user.id` (or `_id`). Only a 24-hex Mongo ObjectId is stored;
|
|
@@ -60,7 +79,7 @@ shape still writes the event with `actorId` omitted and a warning logged.
|
|
|
60
79
|
Update and delete snapshot `before` only when `audit` is on. If that `toJSON` throws, the
|
|
61
80
|
mutation still completes and the event is recorded without a `before` snapshot.
|
|
62
81
|
|
|
63
|
-
###
|
|
82
|
+
### 5. Fire-and-forget persist
|
|
64
83
|
|
|
65
84
|
HTTP handlers do not await Mongo. Diffs still run before the response so `before` / `after` are captured. Failures log; the mutation stays 2xx.
|
|
66
85
|
|
|
@@ -80,6 +99,6 @@ Authenticated malformed queue payloads are acknowledged without persistence. The
|
|
|
80
99
|
become valid on retry, and returning a non-2xx response would make Cloud Tasks retry the
|
|
81
100
|
same poison task indefinitely.
|
|
82
101
|
|
|
83
|
-
###
|
|
102
|
+
### 6. Retention
|
|
84
103
|
|
|
85
104
|
Omit `retentionDays` or set `0` to keep events forever (no TTL index). `new AuditApp({retentionDays: 90})` creates a Mongo TTL index on `created` with `expireAfterSeconds = 90 * 86400`, replacing the default `{created: 1}` field index so Mongo does not reject a duplicate key pattern. Mongo expires documents in the background; lowering or removing TTL later requires dropping that index yourself (`db.auditevents.dropIndex(...)`) — Mongoose will not remove it.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# How to automate Terreno upgrades with a scheduled agent
|
|
2
|
+
|
|
3
|
+
Run a GitHub Actions job every working hour that checks whether your app is behind the latest Terreno release. When it is, and no upgrade is already in progress, the job starts one cloud agent. The agent runs the [`upgrading-terreno`](upgrade-terreno.md) skill and opens a pull request. The job never edits the tree and cannot merge. A person reviews the pull request.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
cron tick ──► in working hours? ──► behind the latest GitHub release?
|
|
7
|
+
│
|
|
8
|
+
▼
|
|
9
|
+
release ≥ 7 days old? ──► no open upgrade PR? ──► no upgrade agent running or in cooldown?
|
|
10
|
+
│
|
|
11
|
+
▼
|
|
12
|
+
npm integrity for every installed package ──► launch one agent ──► agent opens PR
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This page is an example. Copy the workflow and adapt the script to your repository. The Terreno repository does not run it.
|
|
16
|
+
|
|
17
|
+
## Why the job only decides
|
|
18
|
+
|
|
19
|
+
A GitHub runner usually has no MongoDB, no running backend, and no browser. The upgrade skill needs all three: it starts the upgraded backend, regenerates the typed client from `/openapi.json`, and runs the app. So the job decides and enqueues. The agent runs in a cloud environment that already has your app's services, such as a Cursor Cloud Agent environment.
|
|
20
|
+
|
|
21
|
+
## Example workflow
|
|
22
|
+
|
|
23
|
+
`.github/workflows/upgrade-terreno.yml`:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
name: Upgrade Terreno
|
|
27
|
+
|
|
28
|
+
on:
|
|
29
|
+
schedule:
|
|
30
|
+
# 13:00–22:00 UTC covers 9:00am–5:59pm America/New_York in both EST and EDT.
|
|
31
|
+
# The script drops ticks outside the local window.
|
|
32
|
+
- cron: "0 13-22 * * 1-5"
|
|
33
|
+
workflow_dispatch:
|
|
34
|
+
inputs:
|
|
35
|
+
version:
|
|
36
|
+
description: "Target @terreno/* version (empty = latest GitHub release)"
|
|
37
|
+
required: false
|
|
38
|
+
type: string
|
|
39
|
+
dry_run:
|
|
40
|
+
description: "Decide only — do not launch an upgrade agent"
|
|
41
|
+
required: false
|
|
42
|
+
default: false
|
|
43
|
+
type: boolean
|
|
44
|
+
|
|
45
|
+
permissions:
|
|
46
|
+
contents: read
|
|
47
|
+
pull-requests: read
|
|
48
|
+
|
|
49
|
+
# A second tick waits instead of racing the pull-request and active-agent checks.
|
|
50
|
+
concurrency:
|
|
51
|
+
group: upgrade-terreno
|
|
52
|
+
cancel-in-progress: false
|
|
53
|
+
|
|
54
|
+
jobs:
|
|
55
|
+
upgrade-terreno:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
timeout-minutes: 15
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v6
|
|
60
|
+
with:
|
|
61
|
+
persist-credentials: false
|
|
62
|
+
|
|
63
|
+
- uses: oven-sh/setup-bun@v2
|
|
64
|
+
with:
|
|
65
|
+
bun-version: "1.4.4"
|
|
66
|
+
|
|
67
|
+
- run: bun install --frozen-lockfile
|
|
68
|
+
|
|
69
|
+
- name: Launch Terreno upgrade when one is not already open
|
|
70
|
+
env:
|
|
71
|
+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
|
|
72
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
73
|
+
TERRENO_UPGRADE_CLOUD_ENVIRONMENT: ${{ vars.TERRENO_UPGRADE_CLOUD_ENVIRONMENT }}
|
|
74
|
+
TERRENO_UPGRADE_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
|
|
75
|
+
TERRENO_UPGRADE_VERSION: ${{ inputs.version }}
|
|
76
|
+
run: bun .github/scripts/upgrade-terreno.ts
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The job needs read access to contents and pull requests only. The agent pushes with its own credentials.
|
|
80
|
+
|
|
81
|
+
## What the script decides
|
|
82
|
+
|
|
83
|
+
`.github/scripts/upgrade-terreno.ts` checks these in order and stops at the first one that says no. Order matters: the cheap checks run first, and no agent API call happens until every gate passes.
|
|
84
|
+
|
|
85
|
+
| Order | Check | Skip when |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| 1 | Working hours | A scheduled tick lands outside weekdays 9:00am–5:59pm in your team's timezone. Use Luxon with a named zone so daylight saving does not move the window. Manual runs are not gated. |
|
|
88
|
+
| 2 | Installed versions | No `@terreno/*` pin is found. This is an error, not a skip. Read every manifest that pins Terreno. With a Bun catalog, the versions live in the root `package.json` under `workspaces.catalog` (and `workspaces.catalogs.<name>`); workspace packages say `catalog:`. |
|
|
89
|
+
| 3 | Target version | The lowest installed pin already equals the target, or any pin is newer than it. Never downgrade. Resolving the target fails the job when `@terreno/api` at that version is not on npm yet; see below. |
|
|
90
|
+
| 4 | Release age | A scheduled run targets a release under 7 days old. A manual run is the human gate and is not held. |
|
|
91
|
+
| 5 | Open pull request | Any open pull request is an upgrade: its title names a Terreno upgrade, its branch contains `upgrade-terreno`, or its body has the `<!-- terreno-upgrade -->` marker. |
|
|
92
|
+
| 6 | Running agent | An upgrade agent (name starts with `Upgrade Terreno`) is still running. |
|
|
93
|
+
| 7 | Cooldown | An agent for the same target version was created in the last 24 hours, even one that finished or failed. Without this, a broken upgrade is retried every hour. |
|
|
94
|
+
| 8 | npm integrity | Any installed package has no published `dist.integrity` for the target. |
|
|
95
|
+
|
|
96
|
+
Take the target from the latest stable [TerrenoLabs/terreno GitHub release](https://github.com/TerrenoLabs/terreno/releases), not the npm `latest` dist-tag. Reject drafts, prereleases, and tags that are not semver. Validate a manual `version` input as semver before using it.
|
|
97
|
+
|
|
98
|
+
Write the decision to `$GITHUB_STEP_SUMMARY` so each run says why it skipped:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
## Terreno upgrade
|
|
102
|
+
|
|
103
|
+
- Decision: skipped (open-pull-request)
|
|
104
|
+
- Detail: Open Terreno upgrade pull request #261 (Upgrade Terreno to 57.19.2).
|
|
105
|
+
- From: 57.16.2
|
|
106
|
+
- To: 57.19.2
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The job fails, rather than skips, in two cases:
|
|
110
|
+
|
|
111
|
+
- A launch would happen and `CURSOR_API_KEY` is missing. A skip with no key is still green.
|
|
112
|
+
- The latest GitHub release exists but `@terreno/api` at that version is not on npm. This check runs while the target is resolved, before the release-age and open-pull-request gates, so every in-hours run is red until publishing finishes. That points at a stuck release. If you would rather skip quietly, return a skip from that check instead of throwing.
|
|
113
|
+
|
|
114
|
+
## The agent prompt
|
|
115
|
+
|
|
116
|
+
The skill stops for confirmation unless the invoking task names both versions and authorizes the bump. The prompt does that, so the agent prints its plan into the transcript and continues:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
Upgrade Terreno from 57.16.2 to 57.19.2.
|
|
120
|
+
|
|
121
|
+
This unattended task names fromVersion 57.16.2 and toVersion 57.19.2 and authorizes the
|
|
122
|
+
lockstep bump. Follow the upgrading-terreno skill. Print the plan into the transcript, then
|
|
123
|
+
perform the upgrade. Do not wait for confirmation.
|
|
124
|
+
|
|
125
|
+
Check out the latest origin/master and create a feature branch before editing. Do not commit
|
|
126
|
+
to master.
|
|
127
|
+
|
|
128
|
+
Bump every already-installed @terreno/* dependency to 57.19.2. Do not add @terreno packages
|
|
129
|
+
that are not already installed.
|
|
130
|
+
|
|
131
|
+
Installed pins:
|
|
132
|
+
package.json workspaces.catalog @terreno/api@57.16.2
|
|
133
|
+
package.json workspaces.catalog @terreno/ui@57.16.2
|
|
134
|
+
|
|
135
|
+
The target is the TerrenoLabs/terreno GitHub release 57.19.2, not the npm latest dist-tag.
|
|
136
|
+
Install these exact versions. npm dist.integrity:
|
|
137
|
+
@terreno/api@57.19.2 sha512-…
|
|
138
|
+
@terreno/ui@57.19.2 sha512-…
|
|
139
|
+
|
|
140
|
+
Open exactly one pull request against master. Put <!-- terreno-upgrade --> in its body.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Add the repo-specific steps the skill cannot know, such as which apps run `bun run sdk`. Install the skill in the app first ([install agent skills](install-agent-skills.md)); otherwise the agent has nothing to follow.
|
|
144
|
+
|
|
145
|
+
Name the agent `Upgrade Terreno to <version>` so checks 6 and 7 can find it.
|
|
146
|
+
|
|
147
|
+
## Test the script
|
|
148
|
+
|
|
149
|
+
Inject the I/O (manifest reads, GitHub, npm, the agent API, and the clock) as dependencies and test the decision function with fakes. Cover at least:
|
|
150
|
+
|
|
151
|
+
- A scheduled tick at 8:59am and 6:00pm local time in both winter and summer
|
|
152
|
+
- Already current, and a pin newer than the target
|
|
153
|
+
- A 6-day-old release on a schedule, and the same release on a manual run
|
|
154
|
+
- Each open pull request signal on its own
|
|
155
|
+
- A finished agent for the same version inside and outside the cooldown
|
|
156
|
+
- A package with no npm integrity
|
|
157
|
+
- A dry run that reports `launch` without calling the agent API
|
|
158
|
+
|
|
159
|
+
## Try it
|
|
160
|
+
|
|
161
|
+
Run the workflow manually with `dry_run` checked. The summary shows the decision without launching anything. Then run it locally:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
GITHUB_EVENT_NAME=workflow_dispatch \
|
|
165
|
+
GITHUB_REPOSITORY=<owner>/<repo> \
|
|
166
|
+
GITHUB_TOKEN=$(gh auth token) \
|
|
167
|
+
TERRENO_UPGRADE_DRY_RUN=true \
|
|
168
|
+
bun .github/scripts/upgrade-terreno.ts
|
|
169
|
+
```
|