@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
@@ -0,0 +1,297 @@
1
+ # Provisional consent signing
2
+
3
+ Terreno's consent system today lets a user sign only for themselves: `ConsentNavigator` blocks
4
+ the app until the user agrees to every pending form, and admins can only read responses. Apps
5
+ such as Flourish need staff to record consent ahead of the user, for example after verbal
6
+ consent from a guardian at intake, so the form counts as signed for compliance while the user
7
+ still sees it and signs it themselves. This plan adds that provisional signature to the
8
+ consent API, ships an exported UI component apps can drop into any screen, and makes consent
9
+ printouts show the provisional signature.
10
+
11
+ ## The idea
12
+
13
+ A `ConsentResponse` can now be **provisional**: `agreed: true`, `provisional: true`, signed by
14
+ another user (`provisionalSignedBy`), at a time (`agreedAt`), with a required free-text note
15
+ (`provisionalNote`). A new RBAC action, `consentResponse:signOnBehalf`, controls who may
16
+ create one. A provisional response counts as signed in `getConsentStatuses`, a new server helper
17
+ and endpoint. It does **not** clear the form from `/consents/pending`, so the user still
18
+ meets the form in the gate, now with a "Signed on your behalf by <name> on <date>" notice.
19
+ The user's own answer always wins: agreeing makes the status `signed`, and declining makes it
20
+ `declined`. Both records stay for audit. A provisional signature made in error can be voided,
21
+ with the void kept on record.
22
+
23
+ `@terreno/ui` exports `ProvisionalConsentSigner`: given a target `userId`, it lists that user's
24
+ forms with status and lets the signer read a form, fill in the note box, and sign it
25
+ provisionally. Consent printouts (a new print action, plus the existing PDF downloads)
26
+ include a "Provisional signature" block.
27
+
28
+ Rejected: a separate `ProvisionalConsent` model. A field on `ConsentResponse` keeps one audit
29
+ trail per user and reuses the existing admin screens, populate paths, and PDF code.
30
+
31
+ ## The plan
32
+
33
+ | # | Task | Depends on | Lands in | Proves it |
34
+ | --- | --- | --- | --- | --- |
35
+ | T1 | Provisional sign endpoint + RBAC (tracer) | none | api | `bun test api/src/consentApp.test.ts api/src/rbac` |
36
+ | T2 | Provisional notice data on user-facing payloads | T1 | api | `bun test api/src/consentApp.test.ts` |
37
+ | T3 | `getConsentStatuses` helper + status endpoint + admin columns + SDK | T2 | api, example-frontend SDK | `bun test api/src/consentStatus.test.ts api/src/consentApp.test.ts` |
38
+ | T4 | Void a provisional signature | T3 | api | `bun test api/src/consentApp.test.ts` |
39
+ | T5 | Provisional notice in the consent gate | none | ui | `bun test ui/src/ConsentFormScreen.test.tsx` |
40
+ | T6 | `ProvisionalConsentSigner` status list + hook | none | ui | `bun test ui/src/ProvisionalConsentSigner.test.tsx` |
41
+ | T7 | Sign flow with note box in `ProvisionalConsentSigner` | T6 | ui | same |
42
+ | T8 | Print helper + provisional block in PDF template | none | ui | `bun test ui/src/pdfHtmlTemplate.test.ts` |
43
+ | T9 | Provisional block in admin-frontend PDF generators | T8 | admin-frontend | `bun test admin-frontend/src` (PDF + isolated) |
44
+ | T10 | Print buttons + provisional display in admin and user consent views | T9 | admin-frontend | `bun test admin-frontend/src/Consent*.test.tsx` |
45
+ | T11 | Print and void buttons in `ProvisionalConsentSigner` | T7, T8 | ui | `bun test ui/src/ProvisionalConsentSigner.test.tsx` |
46
+ | T12 | Example app routes + backend wiring + E2E (admin and RBAC staff) | T4, T5, T11 | example-backend, example-frontend | `bun run frontend:e2e -- provisionalConsent.spec.ts` + screenshots |
47
+ | T13 | UI, admin, how-to docs + changelog | T10, T12 | docs, changelog | `bun run check:changelog`, `bun run check:knip`, docs-audit skill |
48
+
49
+ Tracer: `POST /consent-responses/provisional` → `ConsentResponse{provisional: true}` → form still in `/consents/pending`.
50
+ Out of scope: printing blank forms, bulk provisional signing, org or relationship scoping of who can sign for whom (see Expansions).
51
+ Open risks: staff with `signOnBehalf` can list any user's consent statuses. You chose RBAC with no relationship check, so these reads and writes are audited (T1, T3) when `AuditApp` is registered.
52
+
53
+ ## Contracts
54
+
55
+ ### Model: `ConsentResponse` (additive, no migration)
56
+
57
+ | Field | Type | Notes |
58
+ | --- | --- | --- |
59
+ | `provisional` | `Boolean`, default `false` | `true` only on responses created by `/consent-responses/provisional` |
60
+ | `provisionalSignedBy` | `ObjectId` ref `User` | The signer. Required when `provisional` |
61
+ | `provisionalNote` | `String` | Required, trimmed, non-empty, max 2000 chars when `provisional` |
62
+ | `provisionalSignerIp` / `provisionalSignerUserAgent` | `String` | Signer's request metadata, set when `auditTrail` is on |
63
+ | `voidedAt` / `voidedBy` / `voidReason` | `Date` / `ObjectId` ref `User` / `String` | Set only by the void action (T4) |
64
+
65
+ For provisional responses, `agreed` is always `true` and `agreedAt` is the server time.
66
+ `signature`/`signedAt` stay unset. `ipAddress`/`userAgent` stay unset, because those fields mean
67
+ the **user's** device. `formVersionSnapshot` is always set. `contentSnapshot` is set when
68
+ `auditTrail` is on. Existing documents read as `provisional: false` through the schema default.
69
+
70
+ ### RBAC
71
+
72
+ `terrenoStatements.consentResponse` becomes `["list", "read", "signOnBehalf"]`, with a
73
+ description in `terrenoStatementDescriptions`. The built-in `admin` role definition gets
74
+ `consentResponse: ["signOnBehalf"]`. The `admin` role upsert in `roleModel.ts` adds
75
+ `consentResponse` to **existing** unsealed admin roles through `mergeMissingResourcePermissions`,
76
+ as it already does for observability resources. Without that, existing databases such as
77
+ Flourish would not get the permission. `superadmin` already has `*`.
78
+
79
+ `ConsentAppOptions` gains `accessControl?: AnyTerrenoAccess`. The same rule covers sign, status,
80
+ and void:
81
+
82
+ | `accessControl` passed | Who may act |
83
+ | --- | --- |
84
+ | yes | Any authenticated user whose roles grant `consentResponse:signOnBehalf` |
85
+ | no | `user.admin` only (legacy apps without RBAC); one `logger.warn` at register |
86
+
87
+ ### Status rules (`getConsentStatuses`, T3)
88
+
89
+ For each active form after `resolveConsentForms(targetUser, forms)`, look only at non-voided
90
+ responses for the form's **current version**:
91
+
92
+ 1. If the user has their own (non-provisional) response, the latest one decides: `agreed` →
93
+ `signed`, declined → `declined`.
94
+ 2. Otherwise, if a provisional response exists → `provisional`. It counts as signed.
95
+ 3. Otherwise → `pending`.
96
+
97
+ `isConsented` is `true` for `signed` and `provisional`. Exported from `@terreno/api`:
98
+ `getConsentStatuses({userId, resolveConsentForms?}) → Promise<ConsentStatusRow[]>`. App code
99
+ uses this as the "considered signed" contract.
100
+
101
+ ### API (modelRouter `collectionActions` / `instanceActions` on `/consent-responses`)
102
+
103
+ | Method + path | Body / query | Response | Errors |
104
+ | --- | --- | --- | --- |
105
+ | `POST /consent-responses/provisional` | `{userId, consentFormId, note, locale?}` | `{data: ConsentResponse}` (populated `provisionalSignedBy` name/email) | 400: missing ids, blank note, inactive form, form not in target user's resolved set, `userId` = signer. 404: unknown user or form. 403/405: no permission |
106
+ | `GET /consent-responses/status?userId=` | — | `{data: ConsentStatusRow[]}` | 400 missing `userId`; 404 unknown user; 403/405 |
107
+ | `POST /consent-responses/:id/void` | `{reason}` | `{data: ConsentResponse}` | 400: blank reason, not provisional, already voided. 403/405 |
108
+
109
+ `ConsentStatusRow = {form: ConsentFormPublic, status: "pending" | "provisional" | "signed" | "declined", isConsented, respondedAt?, provisional?: {responseId, signedAt, signedBy: {id, name, email}, note}}`.
110
+
111
+ User-facing changes (T2). The user never sees the note or the signer's request metadata:
112
+
113
+ - `GET /consents/pending`: only non-provisional responses remove a form, as today, including
114
+ declines. Each form carries `provisionalSignature?: {signedAt, signedByName}` from the latest
115
+ non-voided provisional response for its current version.
116
+ - `GET /consents/my`: entries add `provisional`, `provisionalSignedBy: {name} | null`, `voidedAt`.
117
+ No note and no signer IP/UA.
118
+ - `GET /consents/audit/:userId` (admin): adds every provisional and void field.
119
+
120
+ Audit: when `AuditApp` is registered, sign, void, and status reads each call `recordAuditEvent`
121
+ (`api/src/audit/record.ts`) with actor, target user, form id, and action. Writes are best-effort,
122
+ as everywhere else.
123
+
124
+ ### UI exports (`@terreno/ui`)
125
+
126
+ ```tsx
127
+ <ProvisionalConsentSigner
128
+ api={terrenoApi} // consumer RTK api, same pattern as ConsentNavigator
129
+ baseUrl?: string
130
+ userId="..." // the user being signed for
131
+ onSigned?={(response) => void}
132
+ onError?={(error) => void}
133
+ variables?: Record<string, string>
134
+ />
135
+ ```
136
+
137
+ - `useProvisionalConsent(api, baseUrl)` → `{statuses, isLoading, error, refetch, sign, isSigning, voidSignature}`. It injects `getConsentStatus`, `signConsentProvisionally`, and `voidProvisionalConsent`. Mutations invalidate `ConsentStatus` and `PendingConsents`.
138
+ - `printConsentHtml({html})`: native → `expo-print` `printAsync`; web → hidden iframe + `contentWindow.print()`.
139
+ - `PdfTemplateData.provisionalSignature?: {signedAt, signedByName, note?, signerIp?, voidedAt?}` rendered by `buildConsentPdfHtml` as a "Provisional signature" section.
140
+
141
+ testIDs: `provisional-consent-signer`, `provisional-consent-row-{formId}`, `provisional-consent-status-{formId}`, `provisional-consent-open-{formId}`, `provisional-consent-note-input`, `provisional-consent-submit-button`, `provisional-consent-success`, `provisional-consent-print-{formId}`, `provisional-consent-void-{formId}`, `consent-form-provisional-notice`, `consent-history-print-{entryId}`, `consent-response-print-button`.
142
+
143
+ ## Tasks
144
+
145
+ - [x] **T1** — Add provisional consent signing endpoint
146
+ - Depends on: none
147
+ - Files: api/src/types/consentResponse.ts, api/src/models/consentResponse.ts, api/src/rbac/statements.ts, api/src/rbac/roleModel.ts, api/src/rbac/roleModel.test.ts, api/src/rbac/statements.test.ts, api/src/consentApp.ts, api/src/consentApp.test.ts, docs/explanation/consent-forms.md (new), docs/reference/api.md
148
+ - Criteria (each a test):
149
+ - Admin, no `accessControl`: POST returns 200. The stored response has `provisional: true`, `agreed: true`, `provisionalSignedBy` = signer id, `provisionalNote` = trimmed note, `agreedAt` in the test window, `formVersionSnapshot` = form version, and no `ipAddress`/`userAgent`. With `auditTrail`, `provisionalSignerIp` is set.
150
+ - With `createAccess` and a role granting `consentResponse:signOnBehalf`, a non-admin gets 200. Without it, 403 or 405. A non-admin without `accessControl` gets 403 or 405.
151
+ - Blank note → 400; `userId` = signer → 400; inactive form → 400; form filtered out by `resolveConsentForms` for the target → 400; unknown user → 404.
152
+ - After a provisional sign, the target's `/consents/pending` still lists the form. After the target's own `/consents/respond`, it does not.
153
+ - Role upsert: a fresh database's Admin role includes `consentResponse:signOnBehalf`. An **existing** unsealed admin role without `consentResponse` gains it on the next upsert. A sealed role is untouched.
154
+ - With `AuditApp` registered, a sign writes one `AuditEvent` naming actor, target user, and form.
155
+ - Verification: `bun test --only-failures api/src/consentApp.test.ts api/src/rbac`
156
+ - Docs: create `docs/explanation/consent-forms.md` (forms, responses, versions, the gate, provisional signing, and why it doesn't clear the gate). Add a `ConsentApp` section to `docs/reference/api.md`: the options table with `accessControl`, plus the provisional endpoint.
157
+ - Skills: terreno:terreno-backend-api, terreno:mongoose-schema-safety, model-router-actions, terreno:backend-test-env
158
+
159
+ - [x] **T2** — Expose provisional signature notice data on user-facing consent payloads
160
+ - Depends on: T1
161
+ - Files: api/src/consentApp.ts, api/src/consentApp.test.ts, docs/reference/api.md
162
+ - Criteria: after a provisional sign, the `/consents/pending` form has `provisionalSignature.{signedAt, signedByName}` and no `note`. A form without one has no `provisionalSignature`. `/consents/my` entries include `provisional` and `provisionalSignedBy.name`, but no note and no signer IP/UA (asserted absent). `/consents/audit/:userId` includes all provisional fields. Existing pending/my/audit tests still pass.
163
+ - Verification: `bun test --only-failures api/src/consentApp.test.ts`
164
+ - Docs: response shapes in `docs/reference/api.md`.
165
+ - Skills: terreno:terreno-backend-api
166
+
167
+ - [x] **T3** — Add getConsentStatuses helper, status endpoint, and provisional admin columns
168
+ - Depends on: T2
169
+ - Files: api/src/consentStatus.ts (new), api/src/consentStatus.test.ts (new), api/src/index.ts, api/src/consentApp.ts, api/src/consentApp.test.ts, docs/reference/api.md, docs/explanation/consent-forms.md, example-frontend/store/openApiSdk.ts
170
+ - Criteria: `getConsentStatuses` implements the status rules, with one test each for `pending`, `provisional`, `signed`, `declined`, "user agrees after provisional → `signed`", "user declines after provisional → `declined`", "old-version responses ignored", and "resolver filters forms for the target user". It is exported from `@terreno/api`. `GET /consent-responses/status?userId=` returns its rows, with the same permission matrix as T1, and writes an `AuditEvent` per read when `AuditApp` is registered. The "Consent Responses" admin contribution adds `provisional` to `listFields` plus a boolean filter, and `provisionalSignedBy` to populate paths.
171
+ - Verification: `bun test --only-failures api/src/consentStatus.test.ts api/src/consentApp.test.ts`; `cd example-frontend && bun run sdk` diff contains the new operations
172
+ - Docs: helper and endpoint in `docs/reference/api.md`; "considered signed" rules in the explanation page.
173
+ - Skills: terreno:terreno-backend-api, terreno:generate-sdk
174
+
175
+ - [x] **T4** — Void a provisional consent signature
176
+ - Depends on: T3
177
+ - Files: api/src/types/consentResponse.ts, api/src/models/consentResponse.ts, api/src/consentApp.ts, api/src/consentStatus.ts, api/src/consentApp.test.ts, api/src/consentStatus.test.ts, docs/reference/api.md, example-frontend/store/openApiSdk.ts
178
+ - Criteria: `POST /consent-responses/:id/void` with a reason sets `voidedAt`/`voidedBy`/`voidReason` and never deletes the record. Voiding a non-provisional response, voiding twice, or a blank reason → 400. Same permission matrix as T1. After a void, `getConsentStatuses` returns `pending`, and `/consents/pending` drops `provisionalSignature`. With `AuditApp`, a void writes an `AuditEvent`.
179
+ - Verification: `bun test --only-failures api/src/consentApp.test.ts api/src/consentStatus.test.ts`; SDK regenerated
180
+ - Skills: terreno:terreno-backend-api, terreno:mongoose-schema-safety
181
+
182
+ - [x] **T5** — Show provisional notice in the consent gate
183
+ - Depends on: none (contract above)
184
+ - Files: ui/src/useConsentForms.ts, ui/src/ConsentFormScreen.tsx, ui/src/ConsentFormScreen.test.tsx
185
+ - Criteria: adds `ConsentFormPublic.provisionalSignature?: {signedAt, signedByName}`. When it is set, `consent-form-provisional-notice` shows "Signed on your behalf by <name> on <date>" (Luxon `DATETIME_MED`) above the content. Agree, decline, the scroll gate, checkboxes, and the signature requirement are unchanged, and existing tests pass. When it is unset, no notice shows.
186
+ - Verification: `bun test --only-failures ui/src/ConsentFormScreen.test.tsx`
187
+ - Skills: terreno:terreno-ui, accessibility-testing
188
+
189
+ - [x] **T6** — Add ProvisionalConsentSigner status list and useProvisionalConsent hook
190
+ - Depends on: none (contract above)
191
+ - Files: ui/src/useProvisionalConsent.ts, ui/src/useProvisionalConsent.test.ts, ui/src/ProvisionalConsentSigner.tsx, ui/src/ProvisionalConsentSigner.test.tsx, ui/src/lazyBoundaries/heavyOptionalExports.tsx, ui/src/index.tsx
192
+ - Criteria: the hook injects `getConsentStatus` (`GET {base}/consent-responses/status?userId=`) once per (api, baseUrl), following the `useConsentForms` cache pattern. The component shows a spinner while loading, an error with Retry on failure, and one row per form with title and status badge: `Pending`, `Signed provisionally by X on date`, `Signed by user on date`, or `Declined by user on date`. It is exported lazily from `@terreno/ui`, like `ConsentNavigator`. `RootImportRegression` passes.
193
+ - Verification: `bun test --only-failures ui/src/useProvisionalConsent.test.ts ui/src/ProvisionalConsentSigner.test.tsx ui/src/RootImportRegression.test.tsx`
194
+ - Skills: terreno:terreno-ui, terreno:terreno-data-fetching
195
+
196
+ - [x] **T7** — Sign on behalf with a note box in ProvisionalConsentSigner
197
+ - Depends on: T6
198
+ - Files: ui/src/useProvisionalConsent.ts, ui/src/useProvisionalConsent.test.ts, ui/src/ProvisionalConsentSigner.tsx, ui/src/ProvisionalConsentSigner.test.tsx
199
+ - Criteria: `pending` and `declined` rows have `provisional-consent-open-{formId}`. Opening one shows the form content read-only (`MarkdownView`, `variables` applied) and a required multiline `provisional-consent-note-input`. Submit stays disabled until the note is non-blank. Submit POSTs `{userId, consentFormId, note, locale}`, then shows `provisional-consent-success` with the signer name and time from the response, and calls `onSigned`. Server errors show inline and call `onError`.
200
+ - Verification: `bun test --only-failures ui/src/ProvisionalConsentSigner.test.tsx ui/src/useProvisionalConsent.test.ts`
201
+ - Skills: terreno:terreno-ui, terreno:terreno-data-fetching, accessibility-testing
202
+
203
+ - [x] **T8** — Add print helper and provisional block to the consent PDF template
204
+ - Depends on: none
205
+ - Files: ui/src/pdfHtmlTemplate.ts, ui/src/pdfHtmlTemplate.test.ts
206
+ - Criteria: `buildConsentPdfHtml` renders a "Provisional signature" section (signer, date, escaped note when given, signer IP when given, "Voided <date>" when voided) when `provisionalSignature` is set, and nothing extra otherwise. `printConsentHtml({html})` calls `expo-print` `printAsync({html})` on native. On web it appends a hidden iframe with the HTML, calls `contentWindow.print()`, and removes the iframe. Unit tests cover both paths with mocks.
207
+ - Verification: `bun test --only-failures ui/src/pdfHtmlTemplate.test.ts`
208
+ - Skills: terreno:terreno-ui
209
+
210
+ - [x] **T9** — Include provisional signatures in admin-frontend consent PDFs
211
+ - Depends on: T8
212
+ - Files: admin-frontend/src/generateConsentPdf.ts, admin-frontend/src/generateConsentHistoryPdf.ts, admin-frontend/src/useConsentHistory.ts, admin-frontend/src/isolated/generateConsentPdf.isolated.ts, admin-frontend/src/isolated/generateConsentHistoryPdf.isolated.ts, admin-frontend/src/isolated/generateConsentPdfMobile.isolated.ts, admin-frontend/src/isolated/generateConsentPdfsMobile.isolated.ts, plus their test files
213
+ - Criteria: response and history types carry the provisional fields. Web jsPDF output for a provisional response contains "Provisional signature", the signer name, and the date. The admin response PDF also has the note and signer IP. The user history PDF has no note. The native path passes `provisionalSignature` to `buildConsentPdfHtml`, and both mobile isolated tests assert it (extend their `@terreno/ui` mocks). Non-provisional output is unchanged. Each generator exports the template-data builder it uses, for T10.
214
+ - Verification: `bun test --only-failures admin-frontend/src`
215
+ - Skills: terreno:building-admin-interfaces
216
+
217
+ - [x] **T10** — Add print buttons and provisional display to admin and user consent views
218
+ - Depends on: T9
219
+ - Files: admin-frontend/src/ConsentResponseViewer.tsx, admin-frontend/src/ConsentResponseViewer.test.tsx, admin-frontend/src/ConsentHistory.tsx, admin-frontend/src/ConsentHistory.test.tsx
220
+ - Criteria: `ConsentResponseViewer` (admin) shows a "Provisional" badge, signer, date, note, and void state. `ConsentHistory` (the user's own) shows the badge, signer, and date, with no note. `consent-response-print-button` and per-entry `consent-history-print-{entryId}` call `printConsentHtml` with HTML containing the provisional block. Tests assert the HTML string. Existing download buttons still work.
221
+ - Verification: `bun test --only-failures admin-frontend/src/ConsentResponseViewer.test.tsx admin-frontend/src/ConsentHistory.test.tsx`
222
+ - Skills: terreno:building-admin-interfaces, terreno:terreno-ui
223
+
224
+ - [x] **T11** — Print and void from ProvisionalConsentSigner
225
+ - Depends on: T7, T8
226
+ - Files: ui/src/useProvisionalConsent.ts, ui/src/useProvisionalConsent.test.ts, ui/src/ProvisionalConsentSigner.tsx, ui/src/ProvisionalConsentSigner.test.tsx
227
+ - Criteria: `provisional`, `signed`, and `declined` rows, plus the success state, show `provisional-consent-print-{formId}`. It calls `printConsentHtml` with HTML from `buildConsentPdfHtml` containing the form content and, for provisional rows, the provisional block with the note. `provisional` rows show `provisional-consent-void-{formId}`, which asks for a reason in a `Modal`, POSTs `/void`, and refetches, so the row returns to `Pending`.
228
+ - Verification: `bun test --only-failures ui/src/ProvisionalConsentSigner.test.tsx ui/src/useProvisionalConsent.test.ts`
229
+ - Skills: terreno:terreno-ui
230
+
231
+ - [x] **T12** — Wire provisional signing into the example apps with E2E coverage
232
+ - Depends on: T4, T5, T11
233
+ - Files: example-backend/src/server.ts, example-frontend/app/consent-sign/[userId].tsx (new), example-frontend/app/admin/consent-responses/[id].tsx, example-frontend/e2e/provisionalConsent.spec.ts (new), example-frontend/e2e/helpers/consentForms.ts, example-frontend/e2e/fixtures/testUsers.ts
234
+ - Criteria: `ConsentApp` gets `accessControl: access`. `/consent-sign/[userId]` (outside `/admin`, so non-admin staff can reach it) renders `ProvisionalConsentSigner`. The admin response detail links to it. E2E cases:
235
+ 1. Admin opens a pending form, fills the note, and submits. The row shows provisional.
236
+ 2. The test user logs in and sees the gate with `consent-form-provisional-notice` and no note text. They agree and reach the app.
237
+ 3. Admin reloads, and the row shows signed.
238
+ 4. Setup creates a role with `consentResponse:["signOnBehalf"]` through the RBAC API and assigns it to a non-admin staff user, who signs provisionally from the same route.
239
+ 5. A non-admin user without the role sees the error state.
240
+
241
+ Screenshots of the signer, the notice, and the print HTML go in `/opt/cursor/artifacts/`.
242
+ - Verification: `bun run frontend:e2e -- provisionalConsent.spec.ts`; terreno:verify-ui-changes evidence
243
+ - Skills: terreno:verify-ui-changes, accessibility-testing
244
+
245
+ - [x] **T13** — Document provisional consent signing and printing
246
+ - Depends on: T10, T12
247
+ - Files: docs/reference/ui.md, docs/reference/admin-frontend.md, docs/how-to/sign-consent-on-behalf.md (new), docs/how-to/README.md, docs/explanation/README.md, changelog/unreleased/provisional-consent-signing.md (new)
248
+ - Criteria: `ui.md` documents `ProvisionalConsentSigner`, `useProvisionalConsent`, `printConsentHtml`, and the gate notice. `admin-frontend.md` documents the print buttons and provisional display. The how-to covers granting `consentResponse:signOnBehalf` (including the existing-admin-role merge on upgrade), passing `accessControl` to `ConsentApp`, mounting the component, using `getConsentStatuses`, voiding, and printing. Both README indexes link the new pages. There is an `Added` changelog fragment.
249
+ - Verification: `bun run check:changelog`; `bun run check:knip`; the docs-audit skill reports no undocumented new exports
250
+ - Skills: terreno:update-docs, docs-audit
251
+
252
+ ## Assumptions
253
+
254
+ - Endpoints are `collectionActions`/`instanceActions` on the existing `/consent-responses` modelRouter, not new hand-written `/consents` routes (repo rule: no new `router.post`).
255
+ - Permission lists are ANDed (`checkPermissions`, `api/src/permissions.ts:110`), so the actions build their own list from `accessControl` instead of using `action.access`.
256
+ - `ConsentApp` mounts routers without a path, so `TerrenoApp`'s `accessControl` does not reach them automatically. It is passed as an option.
257
+ - The target user is loaded with `mongoose.model("User")`. `resolveConsentForms` receives that document.
258
+ - The provisional signer does not tick checkboxes or draw a signature. The note is the only box (D3). Required checkboxes stay the user's to tick.
259
+ - `ConsentNavigator` needs no change. It renders whatever `/consents/pending` returns, and T5 adds the notice inside `ConsentFormScreen`.
260
+ - Build notes: `useProvisionalConsent` takes `{api, baseUrl?, userId}` (RORO), not
261
+ `(api, baseUrl)`. The success state's print button is `provisional-consent-success-print`,
262
+ so it never shares `provisional-consent-print-{formId}` with the refetched row. The admin
263
+ PDF template builders live in `admin-frontend/src/consentPdfTemplates.ts`.
264
+ - The user's own decline still clears the gate, as today. A decline after a provisional sign makes the status `declined`, not signed.
265
+
266
+ ## Open questions (recommendation assumed)
267
+
268
+ | ID | Question | Recommendation | Why | If answered differently | Tasks affected |
269
+ | --- | --- | --- | --- | --- | --- |
270
+ | Q4 | What does "print consent forms" cover? | A user's form with its signatures, from the admin response view, the user's history, and the signer component | Matches "make sure the provisional is included" | Also print blank forms from the form list (X1) | T8–T11 |
271
+ | Q5 | When organizations are on, limit signers to users in their org? | No, RBAC only, as chosen | You picked RBAC with no relationship check; reads and writes are audited | Add an org-membership check to sign, status, and void | T1, T3, T4 |
272
+
273
+ ## Expansions (follow up later)
274
+
275
+ | ID | Idea | Why it came up | Rough size | Depends on |
276
+ | --- | --- | --- | --- | --- |
277
+ | X1 | Print blank consent forms from the admin form list | "print consent forms" could mean blank forms | S | T8 |
278
+ | X3 | App hook `canSignOnBehalf(signer, target, form)` for relationship checks (guardian, care team) | RBAC has no target-user check | M | T1 |
279
+ | X4 | Bulk provisional sign of all pending forms for a user | Intake usually covers several forms | S | T7 |
280
+
281
+ ## Decisions
282
+
283
+ | ID | Question asked | Answer | What it changes |
284
+ | --- | --- | --- | --- |
285
+ | D1 | Who may provisionally sign for another user? | RBAC permission | New `consentResponse:signOnBehalf` statement; no app hook or relationship check |
286
+ | D2 | What does the user see after a provisional signature? | Same gate + notice | Provisional responses don't clear `/consents/pending`; `ConsentFormScreen` shows a notice; the user must still agree |
287
+ | D3 | What is the box the provisional signer fills in? | Note only | Required `provisionalNote` text; no drawn signature from the signer |
288
+ | D5 | Can a provisional signature made in error be undone? | 1a: yes, void action that keeps the record | T4, void button in T11 |
289
+ | D6 | Should the user see the signer's note? | 2a: no, name and date only | Note only in admin, audit, printouts (T2, T5, T9, T10) |
290
+ | D7 | Without RBAC, fall back to `user.admin`? | 3a: yes, with a warning log | T1, T3, T4 permission matrix |
291
+ | D8 | Signing on declined forms: hide it, or let a newer provisional signature outrank an older decline? (asked after build: T7 offered sign on declined rows, but the status rules let the user's decline win) | ok → hide | `ProvisionalConsentSigner` offers "Sign on behalf" on pending rows only |
292
+ | D4 | (follow-up message) Printing | "add in the ability to print consent forms and make sure the provisional is included" | T8–T11 |
293
+
294
+ ## Sign-off
295
+
296
+ Status: approved 2026-10-23
297
+ Cut: 1 round, 12 findings: 11 fixed (F1 status rules + `declined`, F2 existing-admin-role merge, F3 note hidden from user, F4 signer IP/UA in own fields, F5 `getConsentStatuses` export, F6 resolver check on sign, F7 audit events, F9 real verification commands, F10 mobile isolated tests, F11 per-entry testIDs + view naming, F12 RBAC staff E2E), 1 moved to questions (F8 → Q1, built on its recommendation as T4), 0 rebutted
@@ -0,0 +1,68 @@
1
+ # Fix MCP OAuth: loopback ports and machine clients
2
+
3
+ Native MCP clients such as Claude Code bind an ephemeral loopback port, and `GET /oauth/authorize` rejects that redirect unless the port matches the registered URI exactly. Headless clients cannot use OAuth at all: the token endpoint only accepts `authorization_code` and `refresh_token` for public PKCE clients.
4
+
5
+ Selected: https://github.com/TerrenoLabs/terreno/issues/1655 https://github.com/TerrenoLabs/terreno/issues/1654 https://github.com/TerrenoLabs/terreno/issues/1653, resolved 2026-10-29T21:42:00Z
6
+
7
+ ## Issues
8
+
9
+ | Issue | Title | Disposition | Root cause | Task |
10
+ | --- | --- | --- | --- | --- |
11
+ | #1655 | MCP OAuth: loopback redirect URIs should match any port | fix | `api/src/mcp/oauth.ts` authorize uses exact `redirectUris.includes` | T1 |
12
+ | #1653 | MCP OAuth has no machine grant | fix | token route and metadata only allow public authorization-code clients | T2 |
13
+ | #1654 | MCP OAuth consent endpoints require JWT/Better Auth | docs | `authenticateMiddleware()` already continues when `req.user` is set | docs |
14
+
15
+ ## E2E tests
16
+
17
+ None — proved by supertest HTTP tests in `api/src/mcp/oauth.test.ts`. The defects are authorization-server responses, not a browser or device flow.
18
+
19
+ ## Regression cases
20
+
21
+ None — no hard-to-automate change.
22
+
23
+ ## User guide
24
+
25
+ None — operator steps stay in `docs/how-to/enable-mcp-oauth.md`. No end-user guide page describes this flow.
26
+
27
+ ## Tasks
28
+
29
+ - [x] **T1** — Accept any port on a registered loopback redirect URI (#1655)
30
+ - Depends on: none
31
+ - Files: `api/src/mcp/oauth.ts`, `api/src/mcp/oauth.test.ts`, `api/src/models/mcpOAuthClient.ts`, `docs/how-to/enable-mcp-oauth.md`, `docs/reference/api.md`, `changelog/unreleased/mcp-oauth-loopback-redirect.md`
32
+ - Root cause: `api/src/mcp/oauth.ts` authorize requires `client.redirectUris.includes(redirectUri)`, so a registered `http://127.0.0.1:33418/callback` rejects `http://127.0.0.1:<other>/callback`
33
+ - Proof: `api/src/mcp/oauth.test.ts` "sends the authorization code to the requested loopback port" fails on the base with `400 invalid_request` and passes on the branch with a redirect to that port
34
+ - Closes: #1655
35
+
36
+ - [x] **T2** — Issue client_credentials access tokens for confidential MCP clients (#1653)
37
+ - Depends on: T1
38
+ - Files: `api/src/mcp/oauth.ts`, `api/src/mcp/oauthMetadata.ts`, `api/src/mcp/oauth.test.ts`, `api/src/models/mcpOAuthClient.ts`, `api/src/models/mcpOAuthToken.ts`, `api/src/types/mcpOAuth.ts`, `api/src/index.ts`, `docs/how-to/enable-mcp-oauth.md`, `docs/reference/api.md`, `changelog/unreleased/mcp-oauth-client-credentials.md`
39
+ - Root cause: `SUPPORTED_GRANT_TYPES` and `/oauth/token` only accept authorization-code and refresh-token grants for public clients
40
+ - Proof: `api/src/mcp/oauth.test.ts` "issues an access token for client_credentials and no refresh token" fails on the base with `unsupported_grant_type` and passes on the branch; `/mcp` calls succeed as the bound user
41
+ - Closes: #1653
42
+
43
+ ## Assumptions
44
+
45
+ - Loopback matching follows the issue: scheme, hostname, path, and query must match; the port is ignored. `localhost`, `127.0.0.1`, and `[::1]` do not match each other. No per-client opt-out.
46
+ - The redirect stored on the authorization request is the URI the client sent, so the code returns to the ephemeral port.
47
+ - `private_key_jwt` is deferred.
48
+ - The access token's `credential` stays `"oauth"`.
49
+ - Dynamic client registration stays public-only. Confidential clients are created with `McpOAuthClient.issueConfidential`, which returns the secret once.
50
+ - Both `client_secret_basic` and `client_secret_post` are accepted, and only the method stored on the client.
51
+ - Client credentials tokens have no refresh token. The document still stores a unique hash of a discarded secret so the existing unique index on `refreshTokenHash` keeps working, and `hasRefreshToken: false` makes that secret unusable for refresh.
52
+ - Revoking or deleting the client revokes every token family issued to it.
53
+ - The bound user's `disabled` flag and `tokenEpoch` apply the same way they do for authorization-code tokens.
54
+
55
+ ## Open questions (recommendation assumed)
56
+
57
+ | Question | Recommendation | Why |
58
+ | --- | --- | --- |
59
+ | Should dynamic registration mint confidential clients? | No. Only `issueConfidential`. | Registration is unauthenticated. A public registration endpoint that mints secrets widens who can create a machine identity. |
60
+ | `credential: "oauth"` or `"oauth-client"`? | `"oauth"` | The issue allows either. `/mcp` already treats `mcpat_` tokens as `oauth`, including scope checks and epoch revocation. |
61
+
62
+ ## Consent (#1654)
63
+
64
+ Documented the existing `req.user` short-circuit in `docs/how-to/enable-mcp-oauth.md` and `docs/reference/api.md`. `resolveConsentUser` and a built-in consent page stay out. No changelog fragment: the middleware behavior was already shipped.
65
+
66
+ ## Sign-off
67
+
68
+ Status: approved 2026-10-29 (tap: #1655 #1653; #1654 documented `req.user` short-circuit)
@@ -5,6 +5,7 @@ Technical reference for Terreno packages and APIs. Information-oriented, precise
5
5
  ## Packages
6
6
 
7
7
  - [@terreno/api](api.md) — modelRouter, auth, permissions, setupServer
8
+ - [Account security](account-security.md) — `AccountSecurityApp`: per-user security summary, recovery actions (2FA reset, passkeys, providers, sessions, password reset, disable, sign-in links), security audit events
8
9
  - [@terreno/ui](ui.md) — Components, theming, layout
9
10
  - [@terreno/syncdb](syncdb.md) — Local-first data layer (reads, writes, offline sync)
10
11
  - [@terreno/ai](ai.md) — AI service, GPT routes, Langfuse integration