@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.
Files changed (197) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/README.md +32 -269
  3. package/dist/bootstrap.d.ts +12 -0
  4. package/dist/bootstrap.d.ts.map +1 -1
  5. package/dist/bootstrap.js +77 -122
  6. package/dist/bootstrap.js.map +1 -1
  7. package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
  8. package/dist/docs/resources/overview.md +4 -2
  9. package/dist/docs/resources/patterns.md +237 -176
  10. package/dist/docs/resources/ui.md +48 -17
  11. package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  12. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  13. package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  14. package/dist/docs/upgrades/57.20.0.md +238 -0
  15. package/dist/docs/versioned/next/approval-rubric.md +30 -12
  16. package/dist/docs/versioned/next/explanation/README.md +5 -0
  17. package/dist/docs/versioned/next/explanation/account-linking.md +169 -0
  18. package/dist/docs/versioned/next/explanation/admin-interface.md +1 -1
  19. package/dist/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  20. package/dist/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  21. package/dist/docs/versioned/next/explanation/authentication.md +45 -1
  22. package/dist/docs/versioned/next/explanation/consent-forms.md +105 -0
  23. package/dist/docs/versioned/next/explanation/example-coverage.md +1 -0
  24. package/dist/docs/versioned/next/explanation/local-first-data.md +1 -1
  25. package/dist/docs/versioned/next/explanation/organizations.md +37 -0
  26. package/dist/docs/versioned/next/explanation/planning-plugins.md +4 -0
  27. package/dist/docs/versioned/next/explanation/responsive-design.md +143 -0
  28. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  29. package/dist/docs/versioned/next/explanation/schema-metadata.md +5 -0
  30. package/dist/docs/versioned/next/how-to/README.md +7 -2
  31. package/dist/docs/versioned/next/how-to/audit-log.md +22 -3
  32. package/dist/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  33. package/dist/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  34. package/dist/docs/versioned/next/how-to/circleci.md +15 -5
  35. package/dist/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  36. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  37. package/dist/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  38. package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  39. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  40. package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
  41. package/dist/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  42. package/dist/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  43. package/dist/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  44. package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  45. package/dist/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  46. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  47. package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  48. package/dist/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  49. package/dist/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  50. package/dist/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  51. package/dist/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  52. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  53. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  54. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  55. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  56. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  57. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  58. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  59. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  60. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  61. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  62. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  63. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  64. package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  65. package/dist/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  66. package/dist/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  67. package/dist/docs/versioned/next/reference/README.md +1 -0
  68. package/dist/docs/versioned/next/reference/account-security.md +700 -0
  69. package/dist/docs/versioned/next/reference/admin-backend.md +3 -1
  70. package/dist/docs/versioned/next/reference/admin-frontend.md +225 -1
  71. package/dist/docs/versioned/next/reference/api.md +461 -35
  72. package/dist/docs/versioned/next/reference/cli.md +1 -1
  73. package/dist/docs/versioned/next/reference/comms.md +8 -0
  74. package/dist/docs/versioned/next/reference/create-terreno-app.md +3 -1
  75. package/dist/docs/versioned/next/reference/environment-variables.md +9 -3
  76. package/dist/docs/versioned/next/reference/legacy/rtk.md +19 -3
  77. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  78. package/dist/docs/versioned/next/reference/mcp-server.md +1158 -441
  79. package/dist/docs/versioned/next/reference/syncdb.md +8 -1
  80. package/dist/docs/versioned/next/reference/ui.md +1271 -27
  81. package/dist/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  82. package/dist/docs/versioned/next/tutorials/README.md +4 -0
  83. package/dist/httpApp.d.ts +14 -0
  84. package/dist/httpApp.d.ts.map +1 -0
  85. package/dist/httpApp.js +99 -0
  86. package/dist/httpApp.js.map +1 -0
  87. package/dist/index.js +8 -90
  88. package/dist/index.js.map +1 -1
  89. package/dist/local/localTools.d.ts.map +1 -1
  90. package/dist/local/localTools.js +44 -14
  91. package/dist/local/localTools.js.map +1 -1
  92. package/dist/local/metro/metroDevSession.d.ts +18 -0
  93. package/dist/local/metro/metroDevSession.d.ts.map +1 -1
  94. package/dist/local/metro/metroDevSession.js +42 -3
  95. package/dist/local/metro/metroDevSession.js.map +1 -1
  96. package/dist/local/tools/databaseQuery.d.ts.map +1 -1
  97. package/dist/local/tools/databaseQuery.js +3 -13
  98. package/dist/local/tools/databaseQuery.js.map +1 -1
  99. package/dist/local/tools/databaseSchema.d.ts.map +1 -1
  100. package/dist/local/tools/databaseSchema.js +3 -13
  101. package/dist/local/tools/databaseSchema.js.map +1 -1
  102. package/dist/local/tools/localMongo.d.ts +10 -0
  103. package/dist/local/tools/localMongo.d.ts.map +1 -0
  104. package/dist/local/tools/localMongo.js +30 -0
  105. package/dist/local/tools/localMongo.js.map +1 -0
  106. package/dist/local/tools/runtime.d.ts +18 -2
  107. package/dist/local/tools/runtime.d.ts.map +1 -1
  108. package/dist/local/tools/runtime.js +75 -20
  109. package/dist/local/tools/runtime.js.map +1 -1
  110. package/dist/local/tools/syncDb.d.ts +11 -0
  111. package/dist/local/tools/syncDb.d.ts.map +1 -1
  112. package/dist/local/tools/syncDb.js +15 -3
  113. package/dist/local/tools/syncDb.js.map +1 -1
  114. package/dist/rateLimit.d.ts +35 -0
  115. package/dist/rateLimit.d.ts.map +1 -0
  116. package/dist/rateLimit.js +112 -0
  117. package/dist/rateLimit.js.map +1 -0
  118. package/dist/tools.d.ts.map +1 -1
  119. package/dist/tools.js +42 -60
  120. package/dist/tools.js.map +1 -1
  121. package/package.json +4 -3
  122. package/src/docs/guidelines/ui/guidelines/core.md +1 -1
  123. package/src/docs/resources/overview.md +4 -2
  124. package/src/docs/resources/patterns.md +237 -176
  125. package/src/docs/resources/ui.md +48 -17
  126. package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  127. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  128. package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  129. package/src/docs/upgrades/57.20.0.md +238 -0
  130. package/src/docs/versioned/next/approval-rubric.md +30 -12
  131. package/src/docs/versioned/next/explanation/README.md +5 -0
  132. package/src/docs/versioned/next/explanation/account-linking.md +169 -0
  133. package/src/docs/versioned/next/explanation/admin-interface.md +1 -1
  134. package/src/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  135. package/src/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  136. package/src/docs/versioned/next/explanation/authentication.md +45 -1
  137. package/src/docs/versioned/next/explanation/consent-forms.md +105 -0
  138. package/src/docs/versioned/next/explanation/example-coverage.md +1 -0
  139. package/src/docs/versioned/next/explanation/local-first-data.md +1 -1
  140. package/src/docs/versioned/next/explanation/organizations.md +37 -0
  141. package/src/docs/versioned/next/explanation/planning-plugins.md +4 -0
  142. package/src/docs/versioned/next/explanation/responsive-design.md +143 -0
  143. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  144. package/src/docs/versioned/next/explanation/schema-metadata.md +5 -0
  145. package/src/docs/versioned/next/how-to/README.md +7 -2
  146. package/src/docs/versioned/next/how-to/audit-log.md +22 -3
  147. package/src/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  148. package/src/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  149. package/src/docs/versioned/next/how-to/circleci.md +15 -5
  150. package/src/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  151. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  152. package/src/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  153. package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  154. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  155. package/src/docs/versioned/next/how-to/password-reset.md +1 -1
  156. package/src/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  157. package/src/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  158. package/src/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  159. package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  160. package/src/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  161. package/src/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  162. package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  163. package/src/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  164. package/src/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  165. package/src/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  166. package/src/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  167. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  168. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  169. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  170. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  171. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  172. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  173. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  174. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  175. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  176. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  177. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  178. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  179. package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  180. package/src/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  181. package/src/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  182. package/src/docs/versioned/next/reference/README.md +1 -0
  183. package/src/docs/versioned/next/reference/account-security.md +700 -0
  184. package/src/docs/versioned/next/reference/admin-backend.md +3 -1
  185. package/src/docs/versioned/next/reference/admin-frontend.md +225 -1
  186. package/src/docs/versioned/next/reference/api.md +461 -35
  187. package/src/docs/versioned/next/reference/cli.md +1 -1
  188. package/src/docs/versioned/next/reference/comms.md +8 -0
  189. package/src/docs/versioned/next/reference/create-terreno-app.md +3 -1
  190. package/src/docs/versioned/next/reference/environment-variables.md +9 -3
  191. package/src/docs/versioned/next/reference/legacy/rtk.md +19 -3
  192. package/src/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  193. package/src/docs/versioned/next/reference/mcp-server.md +1158 -441
  194. package/src/docs/versioned/next/reference/syncdb.md +8 -1
  195. package/src/docs/versioned/next/reference/ui.md +1271 -27
  196. package/src/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  197. package/src/docs/versioned/next/tutorials/README.md +4 -0
@@ -293,7 +293,9 @@ assigned after the write through `access.roles.assign`, which requires `rbac:ass
293
293
  Admin update, bulk-patch, delete, and admin-window sync writes to another user also require the
294
294
  actor to hold every permission the target user holds (the RoleManager target-privilege rule).
295
295
  A delegate with `user:update` gets 403 `Cannot modify a user with permissions you do not hold` for
296
- a superadmin target; bulk-patch reports that target under `failures` and patches the rest. Legacy
296
+ a superadmin target; bulk-patch reports that target under `failures` and patches the rest. When a
297
+ permission source is down and the target's grants from it are unknown, the check answers 503
298
+ `Permission source unavailable` instead of comparing against a partial set. Legacy
297
299
  `admin: true` actors and writes to the actor's own record are not checked.
298
300
 
299
301
  Changing another user's `email`, `disabled`, `betterAuthId`, `githubId`, or `emailVerified`
@@ -82,7 +82,10 @@ Wrap organization-aware admin routes with `OrgContextProvider` and render
82
82
  organization navigates to `{routeBase}/orgs/:orgId` and adds
83
83
  `X-Organization-Id` to subsequent `useAdminApi` requests. Query cache keys also
84
84
  include the organization id, preventing rows cached for one org from appearing
85
- in another. Derive `initialOrganization` from the current pathname so context
85
+ in another. `useAdminApi`'s `useListQuery` and `useReadQuery` carry the organization
86
+ in the query argument, so a switch after a request started (such as the switcher
87
+ settling on a default organization) fetches the new organization's rows instead of
88
+ leaving the screen loading. Derive `initialOrganization` from the current pathname so context
86
89
  follows URL changes after mount without overriding a switcher selection while
87
90
  navigation is in progress.
88
91
 
@@ -136,6 +139,182 @@ email, changes member roles, and removes members. Backend errors such as
136
139
  disabled because invitation tokens and email belong to the later invitations
137
140
  feature. The members page uses the full available admin content width.
138
141
 
142
+ Each active member row has a **Security** button (`org-members-security-{userId}`)
143
+ that opens a [`MemberSecurityPanel`](#membersecuritypanel) for that member below
144
+ the table. Suspended rows show **Reactivate** instead
145
+ (`org-members-reactivate-{userId}`), which sends
146
+ `PATCH /orgs/:orgId/members/:memberId` with `{status: "active"}`. After any
147
+ security action the members list reloads, so a suspended membership shows up at
148
+ once.
149
+
150
+ ### MemberSecurityPanel
151
+
152
+ Shows one user's sign-in security and runs the recovery actions the server
153
+ offers for them. It reads the summary from the
154
+ [account security routes](./account-security.md) through the host-bound
155
+ `adminRequest` (`AdminProvider` `credentials` or `getAuthHeaders`, plus
156
+ `apiOrigin`), never through RTK Query. Without a fetch-capable `AdminProvider`
157
+ it shows `Admin RPC client is unavailable`.
158
+
159
+ ```tsx
160
+ import {MemberSecurityPanel} from "@terreno/admin-frontend";
161
+
162
+ // Org-admin routes: /orgs/:orgId/members/:userId/security
163
+ <MemberSecurityPanel memberLabel="Ada" orgId={orgId} userId={userId} />
164
+
165
+ // Platform routes: /users/:id/security
166
+ <MemberSecurityPanel userId={userId} />
167
+ ```
168
+
169
+ | Prop | Type | Description |
170
+ | --- | --- | --- |
171
+ | `userId` | `string` | App `User` id of the member |
172
+ | `orgId` | `string` | Organization id. Set it for the org-admin routes; omit it for the platform routes |
173
+ | `baseURL` | `string` | Route prefix. Defaults to `/orgs` with `orgId` and `/users` without |
174
+ | `memberLabel` | `string` | Name or email shown in the heading (`Security: {memberLabel}`) |
175
+ | `onActionComplete` | `(result) => void` | Called with the action's `{action, changed, ...}` after each success |
176
+ | `onClose` | `() => void` | Shows a **Close** button when set |
177
+ | `focusOnMount` | `boolean` | Move focus to the panel heading (`{testID}-heading`) when it mounts; on web it also scrolls it into view. Default `true` |
178
+ | `testID` | `string` | Root test id. Default `member-security-panel` |
179
+
180
+ What it shows:
181
+
182
+ - Email verified and disabled badges, and the last sign-in method and time.
183
+ - Sign-in methods: whether a password is set, and each linked provider with an
184
+ **Unlink provider** button.
185
+ - Passkeys, each with a **Remove passkey** button.
186
+ - Two-factor status and backup codes left, with **Reset two-factor**.
187
+ - Active sessions, with **Sign out everywhere**.
188
+ - An **Actions** row with every other action in the summary's `actions`
189
+ (`send-password-reset`, `suspend-membership`, `disable`, `enable`,
190
+ `issue-sign-in-link`, and any action added on the server later).
191
+
192
+ Buttons render only for actions the summary lists, so the server decides what
193
+ each caller may do. The panel does not hard-code the list: an action it has
194
+ never seen still gets a button with the server's `label`, and it runs as
195
+ `{method} {securityPath}{path}` from the summary entry. For servers older than
196
+ the summary's `method` and `path`, it falls back to the built-in routes, then
197
+ `POST {securityPath}/{id}`.
198
+
199
+ Where a button goes depends on the action's path params:
200
+
201
+ - `:passkeyId` only: one button per passkey, filled from `passkeys.items[].id`.
202
+ - `:providerId` only: one button per linked provider, filled from
203
+ `providers[].providerId`.
204
+ - No params: `reset-two-factor` and `revoke-sessions` sit in their sections;
205
+ every other action goes in the **Actions** row.
206
+ - Any other param (such as `/devices/:deviceId`): no button, because the
207
+ summary has nothing to fill it with.
208
+
209
+ Per-item buttons have accessible names that include the item, such as
210
+ `Remove passkey Work laptop` and `Unlink GitHub`; an unknown per-item action is
211
+ named `{label}: {item}`. The members screen's row buttons are named
212
+ `Security for {member}` and `Reactivate {member}`.
213
+
214
+ Destructive actions confirm in a `Modal` first (`{testID}-confirm`, buttons
215
+ `{testID}-confirm.primary` and `{testID}-confirm.secondary`). Known actions have
216
+ their own copy; for example, **Unlink provider** warns that sessions started with
217
+ that provider are not ended and points to **Sign out everywhere**. Unknown
218
+ actions get a generic confirmation.
219
+
220
+ After an action the panel shows the result in a `role="status"` region
221
+ (`{testID}-result`), such as
222
+ `Password reset email sent` or `Signed out of 2 sessions`, and refetches the
223
+ summary. `suspend-membership` puts the member out of the org routes' reach, so
224
+ the panel shows the result without refetching. When the response carries a
225
+ `url` (`issue-sign-in-link`), the panel shows it once with a **Copy link**
226
+ button (`{testID}-link-copy`) and a warning that it works once. After copying,
227
+ the button reads **Copied** and a status region (`{testID}-link-copied`) says
228
+ `Link copied to the clipboard`. Server error
229
+ titles, such as `Cannot remove the last sign-in method` or
230
+ `publicAppUrl is required to send recovery mail`, appear inline in a
231
+ `role="alert"` element (`{testID}-action-error`). A failed first load replaces the summary with
232
+ `{testID}-error`; a failed refetch keeps the summary, the result, and any
233
+ one-time link on screen and adds an inline alert (`{testID}-refresh-error`).
234
+
235
+ `useMemberSecurityApi({baseURL, orgId, userId})` is the hook behind the panel.
236
+ It returns `{summary, error, isLoading, isFetching, isRunning, refetch,
237
+ runAction, securityPath}`; `runAction({action, params})` fills path params such
238
+ as `{passkeyId}` and throws before sending when one is missing.
239
+ `memberSecurityPath` and `resolveSecurityActionRequest` are exported for custom
240
+ screens.
241
+
242
+ ### UserSecurityScreen
243
+
244
+ The platform staff page for one user's sign-in security. It renders a header
245
+ naming the user, then a [`MemberSecurityPanel`](#membersecuritypanel) against the
246
+ platform routes (`/users/:id/security`). Only holders of `user:manageSecurity`
247
+ reach those routes: `superadmin`, and the locked `operator` role, which is seeded
248
+ only when the backend calls `createAccess({organizations: true})`. Without
249
+ organizations, grant `user:manageSecurity` on a role of your own. The server
250
+ also refuses acting on yourself and on a user holding any permission you lack,
251
+ such as a superadmin. The panel then shows the refusal title inline
252
+ (`{testID}-panel-error`) and the header still names the user.
253
+
254
+ ```tsx
255
+ // app/admin/users/[id]/security.tsx
256
+ import {UserSecurityScreen} from "@terreno/admin-frontend";
257
+ import {useLocalSearchParams} from "expo-router";
258
+
259
+ const AdminUserSecurityRoute: React.FC = () => {
260
+ const {id} = useLocalSearchParams<{id: string}>();
261
+ return <UserSecurityScreen userId={id} />;
262
+ };
263
+
264
+ export default AdminUserSecurityRoute;
265
+ ```
266
+
267
+ | Prop | Type | Description |
268
+ | --- | --- | --- |
269
+ | `userId` | `string` | App `User` id |
270
+ | `userModelName` | `string` | Admin model name of the user record. Default `User` |
271
+ | `apiBase` | `string` | Admin JSON base for `{apiBase}/config`. Defaults to the `AdminProvider` `apiBase` |
272
+ | `routeBase` | `string` | Admin route base. Defaults to the `AdminProvider` `routeBase` |
273
+ | `securityBaseURL` | `string` | Prefix of the security routes. Default `/users` |
274
+ | `backHref` | `string` | Where the back arrow goes. Default `{routeBase}/{userModelName}/{userId}`, the admin user record |
275
+ | `testID` | `string` | Root test id. Default `user-security-screen`; the panel is `{testID}-panel` |
276
+
277
+ The header reads `GET {apiBase}/config` to find `userModelName`'s `routePath`,
278
+ then `GET {routePath}/{userId}`, both through the host-bound `adminRequest`. It
279
+ shows the user's `name` (`{testID}-user-name`) and `email`
280
+ (`{testID}-user-email`). Without a name it shows the email, and without a
281
+ record it shows the id. While the record loads it shows a spinner
282
+ (`{testID}-user-loading`). When the record fails to load, an inline alert
283
+ (`{testID}-user-error`) says `Could not load the user record.`. The panel loads
284
+ on its own either way, because the security routes do not depend on the admin
285
+ record. If the user model is not registered in admin, the header shows the id
286
+ and no record request is sent.
287
+
288
+ #### Linking from the user record
289
+
290
+ `UserSecurityLink` is a **Security** button (`admin-user-security-link`) that
291
+ opens `userSecurityRoute({routeBase, userId})`, which is
292
+ `{routeBase}/users/{userId}/security`. Mount `UserSecurityScreen` at that path,
293
+ or pass `href` to the link. admin-frontend does not know which admin model is
294
+ your user model, so the host adds the link to its user record. The usual place
295
+ is `AdminModelForm`'s `footerContent`:
296
+
297
+ ```tsx
298
+ // app/admin/[model]/[id].tsx
299
+ import {AdminModelForm, UserSecurityLink} from "@terreno/admin-frontend";
300
+
301
+ const footerContent =
302
+ model === "User" && canManageUserSecurity ? <UserSecurityLink userId={id} /> : undefined;
303
+
304
+ <AdminModelForm api={api} baseUrl="/admin" footerContent={footerContent} itemId={id} mode="edit" modelName={model} />
305
+ ```
306
+
307
+ Show the link and the route only to staff holding `user:manageSecurity` (or
308
+ `admin: true` without RBAC), and hide the link on the viewer's own record, since
309
+ the routes refuse self-targeting; staff manage their own account from their own
310
+ security screen. The example reads `permissions` from `/auth/me`
311
+ (`example-frontend/utils/userSecurityAccess.ts`), shows the link on other users'
312
+ `User` records, and shows everyone else an `AdminScreenPage`
313
+ (`admin-user-security-forbidden`) with a forbidden message and a back arrow to
314
+ the record. Add the
315
+ route to the admin `Stack` (`<Stack.Screen name="users/[id]/security" />`) next
316
+ to `[model]`.
317
+
139
318
  ### AdminModelList
140
319
 
141
320
  Entry screen showing all available models as cards.
@@ -203,6 +382,19 @@ Auto-generates fields from model schema:
203
382
  - `date` → DateTimeField
204
383
  - `objectid` (ref) → SelectField
205
384
  - `enum` → SelectField with options
385
+ - `object` with `items` (a nested schema path such as `tasks: {enabled: Boolean, intakePollMs: Number}`)
386
+ → a titled group with one control per subfield, recursing to any depth
387
+ - `object` without `items` (`Mixed`, `Map`) and `mixed` → JSON editor
388
+
389
+ A nested object group uses the field's humanized key as its heading and its `description` as
390
+ helper text. Each subfield shows its own description and its schema default when the stored
391
+ object has no value for it. Subfield test IDs use the dotted path, for example
392
+ `admin-field-tasks.enabled`; the group itself is `admin-field-group-tasks`. Editing a subfield
393
+ merges it into the parent object, and Save sends the whole object for that field. In create mode
394
+ the object starts from each subfield's default; enums, refs, dates, and Mixed subfields without a
395
+ default are left out. Nested objects inside an array of subdocuments render the same way; their
396
+ test IDs add the array key and item index (`admin-field-servers.0.media.port`), and new array
397
+ items follow the same default rules.
206
398
 
207
399
  System fields (`_id`, `__v`, `created`, `updated`, `deleted`) are automatically skipped.
208
400
 
@@ -296,6 +488,34 @@ Renders reference fields as clickable links.
296
488
  />
297
489
  ``````
298
490
 
491
+ ### Consent records and PDFs
492
+
493
+ `ConsentResponseViewer` (admin, one `ConsentResponse`) and `ConsentHistory` (the signed-in
494
+ user's own `GET /consents/my` entries) download a consent record as a PDF:
495
+ `generateConsentPdf(response)` and `generateConsentHistoryPdf(entry)`. Web builds the PDF with
496
+ jsPDF; native renders `buildConsentPdfHtml` from `@terreno/ui` and shares it with
497
+ `sharePdfFromHtml`.
498
+
499
+ When the response is provisional (staff signed on the user's behalf), both PDFs add a
500
+ "Provisional signature" section: the signer and date, and "Voided <date>" when it was voided.
501
+ The admin PDF also includes the signer's note and IP address. The user's own PDF never
502
+ includes the note.
503
+
504
+ Both views can also print a record: `ConsentResponseViewer`'s "Print" button
505
+ (`consent-response-print-button`) and each `ConsentHistory` entry's print icon
506
+ (`consent-history-print-{entryId}`, in the expanded details) call `printConsentHtml` from
507
+ `@terreno/ui` with `buildConsentPdfHtml` HTML, including the provisional block. The
508
+ "Download PDF" buttons are unchanged.
509
+
510
+ Provisional display:
511
+
512
+ - `ConsentResponseViewer` adds a "Provisional Signature" card (`consent-response-provisional`)
513
+ with a "Provisional" badge, the signer, signing date, note, signer IP, and, once voided, a
514
+ "Voided" badge (`consent-response-provisional-voided`), void date, and reason.
515
+ - `ConsentHistory` marks provisional entries with a "Provisional" badge and "Signed on your
516
+ behalf by <name>" (`consent-history-provisional-{entryId}`), plus "Voided" when voided. It
517
+ never shows the note.
518
+
299
519
  ## Hooks
300
520
 
301
521
  ### useAdminConfig
@@ -418,6 +638,10 @@ When RBAC is enabled, `/admin/config` is filtered for the current user. `AdminSh
418
638
  renders model or custom-screen links returned by the server. The shell lifts Audit Log, Feature
419
639
  Flags, and Jobs into the Platform section (Jobs still comes from `customScreens`).
420
640
 
641
+ A user record's `roles` field loads the role catalog (`GET /rbac/roles`, `rbac:read`) only when
642
+ it is editable. A read-only record, such as an operator's view with `adminUser:read`, lists the
643
+ assigned role names without requesting the catalog.
644
+
421
645
  ### Custom screen page chrome
422
646
 
423
647
  Wrap custom admin screen content in `AdminScreenPage`. It renders the standard `Page` header with a back arrow by default; the arrow navigates to `/admin` via `router.push`, not `router.back()`, because sidebar navigation does not always leave a reliable history entry on web. Pass the host's `routeBase` as `backHref` when admin uses a different prefix; an empty standalone-admin base resolves to `/`. Detail screens can target their parent route (for example `/admin/comms`). Pass `backButton={false}` only when the host supplies equivalent navigation.