create-ailk 0.24.0 → 0.26.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 (638) hide show
  1. package/README.md +53 -22
  2. package/component-catalog.json +3 -3
  3. package/component-catalog.md +3 -3
  4. package/dist/add-module.js +4 -2
  5. package/dist/add-route.d.ts +12 -1
  6. package/dist/add-route.js +31 -3
  7. package/dist/cli.js +12 -7
  8. package/dist/derive-architecture.d.ts +3 -4
  9. package/dist/derive-architecture.js +65 -29
  10. package/dist/lib/design-checks.d.ts +9 -4
  11. package/dist/lib/design-checks.js +13 -7
  12. package/dist/lib/extract-module-bundle.js +29 -2
  13. package/dist/lib/fetch-template-snapshot.d.ts +2 -1
  14. package/dist/lib/fetch-template-snapshot.js +8 -3
  15. package/dist/lib/import-graph.d.ts +1 -1
  16. package/dist/lib/import-graph.js +4 -4
  17. package/dist/lib/module-license-gate.d.ts +12 -1
  18. package/dist/lib/module-license-gate.js +36 -15
  19. package/dist/lib/module-tier.d.ts +2 -8
  20. package/dist/lib/module-tier.js +13 -12
  21. package/dist/module-architecture.d.ts +20 -3
  22. package/dist/module-architecture.js +129 -552
  23. package/dist/paid-sections.d.ts +25 -1
  24. package/dist/paid-sections.js +88 -3
  25. package/dist/starter-grid.d.ts +6 -6
  26. package/dist/starter-grid.js +12 -5
  27. package/dist/surfaces.js +39 -2
  28. package/dist/template-record.d.ts +21 -3
  29. package/dist/template-record.js +38 -7
  30. package/dist/topology.d.ts +16 -3
  31. package/dist/topology.js +54 -23
  32. package/dist/update.d.ts +10 -1
  33. package/dist/update.js +715 -21
  34. package/package.json +3 -3
  35. package/release-scope.json +2 -2
  36. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +8 -2
  37. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +2 -2
  38. package/site-templates/marketing-site/ailk-template.json +7 -1
  39. package/site-templates/marketing-site/provenance.json +7 -1
  40. package/site-templates/saas-product/ailk-template.json +2 -1
  41. package/site-templates/saas-product/provenance.json +1 -0
  42. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  43. package/templates/.claude/hooks/template-guard.sh +137 -0
  44. package/templates/.claude/rules/extending.md +106 -0
  45. package/templates/.claude/rules/loop-contract.md +2 -2
  46. package/templates/.claude/settings.json +11 -0
  47. package/templates/.env.example +20 -5
  48. package/templates/.github/workflows/ci.yml +7 -4
  49. package/templates/CLAUDE.md +11 -1
  50. package/templates/CONVENTIONS.md +8 -10
  51. package/templates/README.md +14 -13
  52. package/templates/apps/api/.env.example +23 -2
  53. package/templates/apps/api/CLAUDE.md +119 -83
  54. package/templates/apps/api/jest.config.cjs +15 -5
  55. package/templates/apps/api/package.json +16 -20
  56. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  57. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  58. package/templates/apps/api/src/index.ts +1 -1
  59. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +16 -0
  60. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -3
  61. package/templates/apps/api/src/modules/commerce.ts +10 -0
  62. package/templates/apps/api/src/modules/content-write.ts +10 -0
  63. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  64. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  65. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  66. package/templates/apps/api/src/modules/workspace.ts +10 -0
  67. package/templates/apps/api/src/server.ts +117 -1882
  68. package/templates/apps/api/vercel.json +2 -2
  69. package/templates/apps/mcp/CLAUDE.md +90 -26
  70. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  71. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  72. package/templates/apps/mcp/jest.config.cjs +6 -1
  73. package/templates/apps/mcp/package.json +5 -13
  74. package/templates/apps/mcp/src/app.ts +8 -7
  75. package/templates/apps/mcp/src/index.ts +6 -34
  76. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  77. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  78. package/templates/apps/mcp/src/site.ts +12 -0
  79. package/templates/apps/web/.env.example +1 -1
  80. package/templates/apps/web/CLAUDE.md +31 -3
  81. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  82. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +43 -4
  83. package/templates/apps/web/app/[locale]/(auth)/legal-footnote.tsx +35 -0
  84. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +117 -8
  85. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +14 -37
  86. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +6 -6
  87. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +20 -35
  88. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +108 -0
  89. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +111 -5
  90. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +78 -16
  91. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +132 -12
  92. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +53 -1
  93. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +38 -0
  94. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +4 -2
  95. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +47 -24
  96. package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +23 -4
  97. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +7 -3
  100. package/templates/apps/web/app/[locale]/layout.tsx +33 -13
  101. package/templates/apps/web/app/[locale]/meet/confirmed/BookingHandOff.tsx +128 -0
  102. package/templates/apps/web/app/[locale]/meet/confirmed/__tests__/BookingHandOff.test.tsx +126 -0
  103. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  105. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  106. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  107. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  108. package/templates/apps/web/auth.ts +10 -6
  109. package/templates/apps/web/i18n/request.ts +49 -1
  110. package/templates/apps/web/jest.config.cjs +1 -0
  111. package/templates/apps/web/lib/__tests__/account-config.test.ts +7 -0
  112. package/templates/apps/web/lib/__tests__/auth-email.test.ts +276 -0
  113. package/templates/apps/web/lib/auth-email.ts +103 -21
  114. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +72 -23
  115. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +94 -70
  116. package/templates/apps/web/lib/prisma-for-auth.ts +5 -2
  117. package/templates/apps/web/lib/relay.ts +2 -2
  118. package/templates/apps/web/lib/release-scope.ts +47 -0
  119. package/templates/apps/web/lib/server-api.ts +1 -1
  120. package/templates/apps/web/lib/sign-up-agreements.ts +291 -28
  121. package/templates/apps/web/lib/site-contributions.ts +18 -0
  122. package/templates/apps/web/messages/en.json +186 -20
  123. package/templates/apps/web/messages/site/en.json +1 -0
  124. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  125. package/templates/customizations.yaml +37 -0
  126. package/templates/database/prisma.config.ts +10 -14
  127. package/templates/database/seed-test.ts +2 -2
  128. package/templates/database/seed.ts +5 -5
  129. package/templates/package.json +12 -6
  130. package/templates/pnpm-workspace.yaml +0 -1
  131. package/templates/project.yaml +16 -8
  132. package/templates/tools/jest/reporters.cjs +65 -0
  133. package/templates/tools/lint-architecture.ts +553 -7
  134. package/templates/tsconfig.json +1 -2
  135. package/versions.json +49 -36
  136. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  137. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  138. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  139. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  140. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  141. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  142. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  143. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  144. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  145. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  146. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  147. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  148. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  149. package/templates/apps/api/src/bin/spokes.ts +0 -198
  150. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  151. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  152. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  153. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  154. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  155. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  156. package/templates/apps/api/src/config/index.ts +0 -596
  157. package/templates/apps/api/src/config/modules.ts +0 -83
  158. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  159. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  160. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  161. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  162. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  163. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  164. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  165. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  166. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  167. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  168. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  169. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  170. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  171. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  172. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  173. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  174. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  175. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  176. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  177. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -308
  178. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  179. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  180. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  181. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  182. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  183. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  184. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  185. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  186. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  187. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  188. package/templates/apps/api/src/lib/errors.ts +0 -95
  189. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  190. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  191. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  192. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  193. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  194. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  195. package/templates/apps/api/src/lib/locales.ts +0 -201
  196. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  197. package/templates/apps/api/src/lib/prisma.ts +0 -21
  198. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  199. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  200. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  201. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  202. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  203. package/templates/apps/api/src/lib/site.ts +0 -257
  204. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  205. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  206. package/templates/apps/api/src/lib/stripe.ts +0 -321
  207. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  208. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  209. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  210. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  211. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  212. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  213. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  214. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  215. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  216. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  217. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  218. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  219. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  220. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  221. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  222. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  223. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  224. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  225. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  226. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  227. package/templates/apps/api/src/middleware/auth.ts +0 -864
  228. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  229. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  230. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -391
  231. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  232. package/templates/apps/api/src/openapi/index.ts +0 -18
  233. package/templates/apps/api/src/openapi/spec.ts +0 -2148
  234. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  235. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  236. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  237. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  238. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  239. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  240. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  241. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  242. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  243. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  244. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  245. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  246. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  247. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -332
  248. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  249. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1426
  250. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  251. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  252. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  253. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  254. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  255. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  256. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  257. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  258. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  259. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  260. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  261. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  262. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  263. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  264. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  265. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  266. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  267. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  268. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  269. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  270. package/templates/apps/api/src/routes/content/get-page.ts +0 -183
  271. package/templates/apps/api/src/routes/content/index.ts +0 -49
  272. package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
  273. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  274. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  275. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  276. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  277. package/templates/apps/api/src/routes/content/search.ts +0 -100
  278. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  279. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  280. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  281. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  282. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  283. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  284. package/templates/apps/api/src/routes/leads/index.ts +0 -639
  285. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  286. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  287. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  288. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  289. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -491
  290. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
  291. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  292. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  293. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  294. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  295. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  296. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  297. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  298. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  299. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  300. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  301. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  302. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  303. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  304. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  305. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  306. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  307. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  308. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  309. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  310. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  311. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  312. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  313. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -449
  314. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  315. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  316. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  317. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  318. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  319. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  320. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  321. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  322. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -809
  323. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  324. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  325. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  326. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2028
  327. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
  328. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  329. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  330. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  331. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  332. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  333. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  334. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  335. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -877
  336. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  337. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  338. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  339. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  340. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  341. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  342. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  343. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  344. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  345. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1204
  346. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
  347. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
  348. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
  349. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  350. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  351. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  352. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  353. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  354. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  355. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  356. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  357. package/templates/apps/api/src/services/abandon-email.ts +0 -336
  358. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  359. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  360. package/templates/apps/api/src/services/audit-log.ts +0 -961
  361. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  362. package/templates/apps/api/src/services/consent.ts +0 -834
  363. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  364. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -750
  365. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  366. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  367. package/templates/apps/api/src/services/entitlements.ts +0 -338
  368. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  369. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  370. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  371. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  372. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  373. package/templates/apps/api/src/services/listing-config.ts +0 -178
  374. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  375. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -348
  376. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  377. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -975
  378. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  379. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -400
  380. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
  381. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  382. package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
  383. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
  384. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  385. package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
  386. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  387. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  388. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  389. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  390. package/templates/apps/api/src/services/person.ts +0 -1211
  391. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  392. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  393. package/templates/apps/api/src/services/project-listings.ts +0 -2085
  394. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  395. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  396. package/templates/apps/api/src/services/revalidation.ts +0 -235
  397. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  398. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  399. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  400. package/templates/apps/api/src/services/verified-email.ts +0 -71
  401. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  402. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  403. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  404. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  405. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  406. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  407. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  408. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  409. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  410. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  411. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  412. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  413. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  414. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  415. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  416. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  417. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1232
  418. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  419. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  420. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  421. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -70
  422. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
  423. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  424. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  425. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -180
  426. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  427. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
  428. package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
  429. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  430. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  431. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +0 -189
  432. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
  433. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
  434. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
  435. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  436. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  437. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -222
  438. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
  439. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  440. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  441. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  442. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  443. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  444. package/templates/apps/mcp/src/abuse/index.ts +0 -349
  445. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  446. package/templates/apps/mcp/src/boot.ts +0 -167
  447. package/templates/apps/mcp/src/config/modules.ts +0 -43
  448. package/templates/apps/mcp/src/http-transport.ts +0 -564
  449. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  450. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  451. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -116
  452. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
  453. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  454. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  455. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  456. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  457. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  458. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  459. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  460. package/templates/apps/mcp/src/lib/site-config.ts +0 -53
  461. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  462. package/templates/apps/mcp/src/request-context.ts +0 -52
  463. package/templates/apps/mcp/src/server.ts +0 -343
  464. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  465. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  466. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -66
  467. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  468. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  469. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  470. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  471. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  472. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  473. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  474. package/templates/apps/mcp/src/tools/index.ts +0 -138
  475. package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
  476. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  477. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  478. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  479. package/templates/apps/mcp/src/tools/read_content.ts +0 -81
  480. package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
  481. package/templates/apps/mcp/src/tools/search_content.ts +0 -63
  482. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
  483. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  484. package/templates/database/CHANGELOG.md +0 -981
  485. package/templates/database/LICENSE +0 -202
  486. package/templates/database/NOTICE +0 -2
  487. package/templates/database/README.md +0 -243
  488. package/templates/database/__tests__/agents.db.test.ts +0 -320
  489. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  490. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  491. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  492. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  493. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  494. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  495. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  496. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  497. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  498. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  499. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  500. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  501. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  502. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  503. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  504. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  505. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  506. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  507. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  508. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  509. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  510. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  511. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  512. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  513. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  514. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  515. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  516. package/templates/database/__tests__/person.db.test.ts +0 -586
  517. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  518. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  519. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  520. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  521. package/templates/database/__tests__/seed.test.ts +0 -272
  522. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  523. package/templates/database/__tests__/tenancy.test.ts +0 -141
  524. package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
  525. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  526. package/templates/database/_gitignore +0 -8
  527. package/templates/database/auth/.gitkeep +0 -0
  528. package/templates/database/auth/schema.prisma +0 -641
  529. package/templates/database/config/.gitkeep +0 -0
  530. package/templates/database/config/schema.prisma +0 -441
  531. package/templates/database/content/.gitkeep +0 -0
  532. package/templates/database/content/schema.prisma +0 -339
  533. package/templates/database/eslint.config.mjs +0 -69
  534. package/templates/database/flows/listing-diagnostic.ts +0 -182
  535. package/templates/database/flows/presets.ts +0 -315
  536. package/templates/database/inbox/.gitkeep +0 -0
  537. package/templates/database/inbox/README.md +0 -3
  538. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  539. package/templates/database/inbox/schema.prisma +0 -1447
  540. package/templates/database/jest.config.cjs +0 -36
  541. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  542. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  543. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  544. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  545. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  546. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  547. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  548. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  549. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  550. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  551. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  552. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  553. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  554. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  555. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  556. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  557. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  558. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  559. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  560. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  561. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  562. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  563. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  564. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  565. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  566. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  567. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  568. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  569. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  570. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  571. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  572. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  573. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  574. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  575. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  576. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  577. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  578. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  579. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  580. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  581. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  582. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  583. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  584. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  585. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  586. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  587. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  588. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  589. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  590. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  591. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  592. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  593. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  594. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  595. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  596. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  597. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  598. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  599. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  600. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  601. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  602. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  603. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  604. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  605. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  606. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  607. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  608. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  609. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  610. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  611. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  612. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  613. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  614. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  615. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  616. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  617. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  618. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  619. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  620. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  621. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  622. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  623. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  624. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  625. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +0 -28
  626. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
  627. package/templates/database/migrations/migration_lock.toml +0 -3
  628. package/templates/database/ops/.gitkeep +0 -0
  629. package/templates/database/ops/schema.prisma +0 -43
  630. package/templates/database/package.json +0 -89
  631. package/templates/database/schema.prisma +0 -71
  632. package/templates/database/scripts/db-generate-locked.sh +0 -80
  633. package/templates/database/scripts/person-backfill.sql +0 -132
  634. package/templates/database/tenancy.ts +0 -183
  635. package/templates/database/tsconfig.build.json +0 -16
  636. package/templates/database/tsconfig.flows.json +0 -15
  637. package/templates/database/tsconfig.json +0 -16
  638. package/templates/database/tsconfig.tenancy.json +0 -14
@@ -2,6 +2,22 @@
2
2
 
3
3
  Inherits root `CLAUDE.md`. This file adds API-specific rules.
4
4
 
5
+ **The API is packages since #9564 and #9565 (#9563 D8).** `@working-theory/api`
6
+ (`packages/api/`) carries the base routes, services, middleware and stores and
7
+ exports `buildServer({ modules, topology, config })`; the module routes and
8
+ services are the six paid `@working-theory/api-<module>` packages
9
+ (`packages-paid/api-{multi-tenant,workspace,scheduling,signal-collectors,commerce,content-write}/`),
10
+ each exporting one module object whose `readClaim()` reads the licence through
11
+ `checkLicense()`. This app is the boot — `src/server.ts` loads the six objects
12
+ through `src/modules/<name>.ts` (one-statement re-exports) and hands them to
13
+ the package, which registers the rows of the modules `AILK_ENABLED_MODULES`
14
+ selects whose claim answered `claimed` (a selected, unclaimed module's routes
15
+ answer 403 `feature_required`). A `src/…` path cited below resolves under
16
+ `packages/api/src` for a base file and `packages-paid/api-<module>/src` for a
17
+ module file; the registration table (`API_MODULE_TABLE`,
18
+ `packages/api/src/config/modules.ts`) is the one list of which module registers
19
+ what, under which predicate.
20
+
5
21
  ## Stack
6
22
 
7
23
  - Fastify, TypeScript
@@ -10,7 +26,7 @@ Inherits root `CLAUDE.md`. This file adds API-specific rules.
10
26
 
11
27
  ## Deploy path
12
28
 
13
- **The supported deploy path is plain `tsc` output, traced and compiled by `@vercel/node`.** `vercel.json` sets `framework: null` and routes every request to `/api/index.ts`; `build:vercel` runs `prisma generate` and two package builds and does NOT compile `apps/api` — Vercel's own builder does, through `nodeFileTrace`. That tracer is what decides which files ship.
29
+ **The supported deploy path is plain `tsc` output, traced and compiled by `@vercel/node`.** `vercel.json` sets `framework: null` and routes every request to `/api/index.ts`; `build:vercel` runs `prisma generate` and the package builds — `@working-theory/validation`, then `@working-theory/api` (#9564: the package's `dist` must exist before the app's, in the monorepo; a scaffold, which carries no `packages/api`, uses the installed `dist`) — and does NOT compile `apps/api` — Vercel's own builder does, through `nodeFileTrace`, tracing through the workspace link into `packages/api/dist`. That tracer is what decides which files ship.
14
30
 
15
31
  **Bundling `apps/api` is NOT a supported deploy path** (#5334). This is a property of the analysers, not a policy:
16
32
 
@@ -20,7 +36,7 @@ Inherits root `CLAUDE.md`. This file adds API-specific rules.
20
36
 
21
37
  If a dependency is forcing you toward a bundler (an extensionless relative import in a published `dist`, say), fix the dependency's packaging — don't bundle `apps/api`. If you bundle anyway, `loadSlice()` (`src/lib/slice-load.ts`) makes the failure say so at boot instead of failing mute; it does not make bundling work.
22
38
 
23
- Three gates hold this together. Run them from the repo root, with `apps/api/dist` built:
39
+ Three gates hold this together. Run them from the repo root, with `packages/api/dist` built (`pnpm --filter @working-theory/api build`) and then `apps/api/dist` (`pnpm --filter @working-theory/app-api exec tsc`):
24
40
 
25
41
  ```bash
26
42
  pnpm slice-reachability:check # every slice specifier is reachable to @vercel/nft
@@ -36,7 +52,7 @@ Every endpoint here has a 1:1 MCP tool in `apps/mcp`. When you add, change, or r
36
52
 
37
53
  The root index sits outside `/v1/*`:
38
54
 
39
- - `/` (GET) — the anonymous root index (`src/server.ts`): `{ name, version, description, modules, endpoints }`. `name` and `description` introduce the site from `_site.mdx` (#5307); `endpoints` is derived at request time from the routes this process registered (#5314). **`modules` (#7501)** is the sorted list of optional modules this process registered routes for — `servedModules()` filtering the `OPTIONAL_MODULES` vocabulary (`commerce`, `content-write`, `multi-tenant`, `scheduling`, `signal-collectors`, `workspace`) against `AILK_ENABLED_MODULES`, both in `src/config/modules.ts`. It is computed once in `build()` (at boot), before any module route registers, so it reports what registered rather than re-reading the env per request; filtering to the vocabulary keeps the anonymous index from echoing whatever an operator typed into the env var. `docs` is deliberately absent from the vocabulary: no apps/api route registers under it, so reporting it would break "modules this process registered routes for". `content-write` is reported only alongside `workspace` or `multi-tenant` — the predicate `server.ts` registers the editor's content routes under; it stays a literal there for the slice-registration lint, is repeated in `servedModules()`, and `src/__tests__/root-modules.test.ts` asserts the two agree. The Admin reads `modules` to hide the surfaces whose module is off; a failed read, or an older API with no `modules` field, hides nothing. No MCP tool mirrors this index.
55
+ - `/` (GET) — the anonymous root index (`src/server.ts`): `{ name, version, description, modules, endpoints }`. `name` and `description` introduce the site from `_site.mdx` (#5307); `endpoints` is derived at request time from the routes this process registered (#5314). **`modules` (#7501)** is the sorted list of optional modules this process registered routes for — `servedModules()` filtering the `OPTIONAL_MODULES` vocabulary (`commerce`, `content-write`, `multi-tenant`, `scheduling`, `signal-collectors`, `workspace`) against `AILK_ENABLED_MODULES`, both in `src/config/modules.ts`. It is computed once in `build()` (at boot), before any module route registers, so it reports what registered rather than re-reading the env per request; filtering to the vocabulary keeps the anonymous index from echoing whatever an operator typed into the env var. `docs` is deliberately absent from the vocabulary: no apps/api route registers under it, so reporting it would break "modules this process registered routes for". `content-write` is reported only alongside `workspace` or `multi-tenant` — the predicate `server.ts` registers the editor's content routes under; it is the `workspaceContentRoutes` row's predicate in `API_MODULE_TABLE`, is repeated in `servedModules()`, and `root-modules.test.ts` (`packages-paid/api-multi-tenant/src/__tests__/`) asserts the two agree. The Admin reads `modules` to hide the surfaces whose module is off; a failed read, or an older API with no `modules` field, hides nothing. No MCP tool mirrors this index.
40
56
 
41
57
  Routes are versioned under `/v1/*`:
42
58
 
@@ -52,27 +68,31 @@ Routes are versioned under `/v1/*`:
52
68
  - `/v1/deliverables/open/:slug` (GET) — an UNGATED file deliverable's bytes, keyed by the flow slug (#8848 D5, `deliverableOpenRoute` in `src/routes/deliverables/index.ts`). Anonymous; the workspace is the caller's (`resolveCallerWorkspace`, keyless through the one-workspace predicate as the flows bullet says; a refused spoke key or an ambiguous deployment `503 tenant_unresolved`), never a request value; no `DATABASE_URL` is `503 deliverables_not_configured`. The flow must be live and carry `deliverable` with `kind: "file"` and `gated: false`; the bytes are its `deliverables` default entry's `Asset`, through `lib/deliverable-asset-store.ts` (a client's file refused), never the legacy store, never a `playbook:` key; served with the grant route's `asset_` headers (`attachment`, pinned extension, `nosniff`, the sandbox CSP, `private, no-store`). Every miss is the file's one uniform 404. Registered in its own encapsulated `deliverableopen` rate-limit scope (60 a minute) with the visitor forward-key digests, and on `FORWARDED_VISITOR_ROUTES`, so a site's proxied reads (`/reports/<slug>/file`) are counted per visitor; the grant route's own budget is unchanged. Module: `signal-collectors`. No MCP twin — a byte download on the grant route's footing, recorded in `tests/parity/README.md`.
53
69
  - `/v1/content/{types,search,offerings,pricing,shown-offerings,:type,:type/:slug}` — public content reads. `GET /v1/content/search`, `GET /v1/content/:type` and `GET /v1/content/:type/:slug` are `search_content`'s, `list_pages`' and `read_content`'s twins (#7167, #7168, #7169): each parses its request with the `@working-theory/validation` exports in `content-reads.ts` (`contentSearchQuerySchema`; `contentListParamsSchema` + `contentListQuerySchema`; `contentPageParamsSchema` + `contentPageQuerySchema`) and answers through the shared builder in `@working-theory/content-adapters` the tool also calls (`searchContentItemList`, `listPagesItemList`, `readContentDocument`), so the pair takes the same arguments and returns the same result — the tool adopted the API; the `/v1` wire shapes are unchanged. `GET /v1/content/offerings` (`?type=&locale=`) is `get_offerings`' twin (#7177), answering `{ ok: true, data: { offerings } }` through `listOfferings` in `@working-theory/content-adapters`, the seam the tool also calls. `GET /v1/content/pricing` (`?slug=&locale=`) is `get_pricing`'s twin (#7178), answering `{ ok: true, data: { tiers } | null }` through `getPricingTiers`. `GET /v1/content/shown-offerings` is the `/pricing` page's Offerings read (#8924), answering `{ ok: true, data: { status: "shown", shownSet } | { status: "unconnected" } | { status: "empty" } }` behind the `commerce` claim, refreshing the snapshot on the bounded non-forced path, with no MCP twin. On a connected workspace with a current shown set, `GET /v1/content/pricing` (the `pricing` page) and `GET /v1/content/offerings` (unless `type=service`) answer that set through `readShownOfferings` (exported from `shown-offerings.ts`, the one read all three share, registered with one `shownSetRouteOptions` in `server.ts`) and `@working-theory/content-adapters`' shown-set mapping (`pricingTiersFromShownSet`, `listOfferingsWithShownSet`), with major-unit prices, `interval` and the full `lookupKey` (offerings: the Service pages, then the shown Products in place of the Product pages, each with `prices`); every other case — no connection, no claim, an expired or empty set, a failed read or mapping (one `warn { outcome: "shown_set_failed", errName }`) — answers page content as before; their MCP twins read the same answer through `apiClient.offerings.shown` (#8933).
54
70
  - `/v1/auth/tokens`, `/v1/user/{profile,data/:key}` — local auth + user data
55
- - `/v1/hub-sign-in/codes` (POST), `/v1/hub-sign-in/token` (POST), `/v1/hub-sign-in/revoke` (POST), `/v1/hub-sign-in/me` (GET) — the hub signs in its spokes (#7894, `src/routes/auth/hub-sign-in.ts` over `src/services/hub-sign-in.ts`; spec `docs/specs/7894-the-hub-signs-in-its-spec.md`): an authorization code with PKCE between a spoke (a confidential client whose `OAuthClient.clientId` IS its `Spoke.id`) and this hub. `codes` is in `authScope` and answers a COOKIE session only (a personal `ApiToken` is 403, an agent key 403 at the preHandler, a hub credential 404): it mints a single-use 60 s code only for an email-verified person and a callback in the client's registered `redirectUris` (exact match), 30 a user per 10 minutes. `token` and `revoke` are in their OWN scope outside `authScope` (per-IP `rateLimitMiddleware`, scope `hubsignin`, 60/min; plus a per-client bucket of 120 exchanges a minute keyed on the VERIFIED client): the caller authenticates with `Authorization: Basic clientId:secret` AND `x-ailk-spoke-key`, which must resolve to the unrevoked spoke whose id is that client — either alone is `401 invalid_client`. `token` consumes the code first and exactly once, then checks the client, the callback and the PKCE verifier (`400 invalid_grant` for any failure, the code gone either way) and issues `ailk_hst_…`, a 24 h credential stored only as a SHA-256 digest in `OAuthAuthorization` (one row per person and spoke, so a new sign-in rotates it); it writes no `Session` row and sets no cookie. `revoke` ends a credential or burns an unexpired code of that client (`{ revoked }`, never an error). `me` answers the credential's person, read live, and is reachable only by that credential. **The credential is admitted by one branch of `middleware/auth.ts`** — only beside the same spoke's key, only while its grant is unrevoked and unexpired, its client enabled and its person's email verified, and only on `HUB_SESSION_ROUTES`, an exact list of 26 method + registered-URL strings, never a prefix (#8220): `GET /v1/hub-sign-in/me`, #7877's three member reads, #8169's eight member reads, #8170's six member writes, #8435's two contact routes, #8438's two consent routes, #8765's account-agreements pair and #8515's onboarding read and write; every other route answers `404` with no handler run (the status the account area's `read()` loads as "nothing for this caller"), and the list is extended only by a reviewed edit. **The per-person budget (#8220 D2, SEC-8169-4):** each person at each spoke gets 60 requests and 10 writes a minute and 30 writes an hour (a fixed hour window from the first admitted write, so up to twice that across a window edge), keyed on the VERIFIED spoke and person and charged only after admission (a refused credential or an off-list route never gets a bucket); over budget is `429 rate_limited` with `Retry-After`. The write limits refuse only writes, and a refused request is not counted. Every line the request logger writes for an admitted request after admission carries `spokeId` (never the key or the credential); a helper logging through the module-level `logger` carries no request binding. The server-wide per-IP limiter still counts all of one spoke's members together — a named residual whose follow-up (the visitor forward key on member calls) must land before Phase 3 (#8163) ships to a live spoke. A bearer write skips the origin check, so the cross-site-request protection for the member writes is the spoke's: its server actions that send a sealed credential carry it (Phase 3, #8163; SEC-8220-11). A completed password reset (`apps/web/auth.ts` `onPasswordReset`) revokes every credential of that person. The operator registers a spoke's client with `spokes sign-in-client --id <spoke_id> --redirect-uri <url>` (`bin/spokes.ts`; `--disable` switches it off) — `lib/hub-sign-in-store.ts` is the only file that names `prisma.oAuthClient` and `prisma.oAuthAuthorization`. No MCP twin for any of the four — a credential exchange between a site and its hub is not an agent-facing call (#3591 exception, the `/v1/auth/tokens` footing). The browser step lives on the hub's web app: `apps/web/app/api/auth/hub/authorize/route.ts`.
56
- - `/v1/checkout/sessions` — start a Stripe checkout. **Two paths (#8931, `src/routes/checkout/sessions.ts` over `src/services/offerings/checkout-authorization.ts`; spec `docs/specs/8931-offerings-checkout-authorization-server-side-spec.md`; contract A1–A11, A13).** The path comes from `resolvePublicWorkspace()` and the workspace's own Organization alone — no body field names either: a resolved workspace whose Organization has a Stripe connection takes the **Offerings path**; a presented `x-ailk-spoke-key` the resolver refuses is `503 { error: "tenant_unresolved" }` with no Stripe call; a corrupt connection, or a database read that fails while choosing, is `502 checkout-unavailable`; everything else (keyless, `ambiguous`, off the database adapter, no Organization, no connection) takes the **env path**, today's code. **The env path is the hub's own-products path only, never a spoke's (#9142):** a spoke sells only through its `apps/web` Offerings buy route, which calls this route only for a lookup key its connected workspace's shown set holds, so a spoke's buy always takes the Offerings path; the spoke's own env-key buy route and Stripe webhook are retired. The Offerings path charges `chargeWriteBudget` scope `offeringscheckout` before the body is read — 10 a minute per `request.ip` and 60 per workspace (`OFFERINGS_CHECKOUT_PER_WORKSPACE_PER_MINUTE`), charged one at a time with the IP first, so a request its IP bucket refuses never spends the workspace's, whatever the process's mode — answering `429 { error: "rate_limited", retryAfterSeconds }` with `Retry-After`. It parses `productCode` (the full lookup key) by the one grammar first, builds the connection's own client (`stripeClientForOrganization`), reads org-enabled ∩ workspace-shown from the rows (`getSellableProductIds`, never the snapshot), looks the key up live on that client, and sells only when the Price's own Product is in the set — and, for a workspace saved per Price (#9070 D5), the Price itself is one of its stored shown Prices (`getSellableProductIds`' `priceIds`; `null` for a row saved per Product) — and `normalizeCatalog` keeps that Price under that key and period. Every miss — ungrammatical, not enabled, not shown, moved, archived, nonexistent — is the one `404 { error: "unknown-product-code" }`, never echoing the key; Stripe, the rows or the connection failing is `502`, never the snapshot or the env key. The session is anonymous (no `customer`, no signed-in buyer, so no `ailk_user_id`), sells the authorized Price id, allows promotion codes, `customer_creation: "always"` in payment mode, and is stamped by the server `metadata` and `subscription_data.metadata` `{ site: <workspaceId>, ailk_offering: <lookup key> }`; it is recorded through `recordCheckoutSession` before the route answers, and a failed record expires it on the same client and answers `502`. **The env path refuses an Offerings product (A9):** `createCheckoutSession` gets `{ refuseProduct: isOfferingsProduct }`, so a Price whose Product carries `metadata.ailk_group` (or whose Product did not expand) is the same `404`. **The Offerings path refuses the hub's own plans (#9338 DD5):** right after the grammar parse, a key whose plan is `ailk-pro` or `ailk-pro-plus` (`isPlanPurchaseKey`, any period) is the same `404`, before any read or Stripe call, for every Organization — such a sale would stamp `ailk_offering`, which no billing path mints a licence for. A hub plan under another slug is not covered (the named residual). The request schema and the `201 { sessionId, url }` body are unchanged, so the `start_purchase` parity row is too. **`GET /v1/checkout/sessions/:id` (A13):** when `DATABASE_URL` is set, a session `readCheckoutSessionRecord` holds is read with that record's Organization's own client, only in the record's mode; its line-item names pass the catalog's T1 `normalizeText`; a record that cannot be read, or a connection gone, unusable or in another mode, is `502`, never the env path; no record is today's env read. Logs carry the workspace or Organization, an 8-character session prefix and a fixed reason only.
71
+ - `/v1/hub-sign-in/codes` (POST), `/v1/hub-sign-in/token` (POST), `/v1/hub-sign-in/revoke` (POST), `/v1/hub-sign-in/me` (GET) — the hub signs in its spokes (#7894, `packages-paid/api-hub/src/routes/auth/hub-sign-in.ts` over `packages-paid/api-hub/src/services/hub-sign-in.ts` — `@working-theory/api-hub` since #9571, which owns the spoke registry and the sign-in trio and hands them to `buildServer({ hub })` through `packages/api/src/lib/hub.ts`; spec `docs/specs/7894-the-hub-signs-in-its-spec.md`): an authorization code with PKCE between a spoke (a confidential client whose `OAuthClient.clientId` IS its `Spoke.id`) and this hub. `codes` is in `authScope` and answers a COOKIE session only (a personal `ApiToken` is 403, an agent key 403 at the preHandler, a hub credential 404): it mints a single-use 60 s code only for an email-verified person and a callback in the client's registered `redirectUris` (exact match), 30 a user per 10 minutes. `token` and `revoke` are in their OWN scope outside `authScope` (per-IP `rateLimitMiddleware`, scope `hubsignin`, 60/min; plus a per-client bucket of 120 exchanges a minute keyed on the VERIFIED client): the caller authenticates with `Authorization: Basic clientId:secret` AND `x-ailk-spoke-key`, which must resolve to the unrevoked spoke whose id is that client — either alone is `401 invalid_client`. `token` consumes the code first and exactly once, then checks the client, the callback and the PKCE verifier (`400 invalid_grant` for any failure, the code gone either way) and issues `ailk_hst_…`, a 24 h credential stored only as a SHA-256 digest in `OAuthAuthorization` (one row per person and spoke, so a new sign-in rotates it); it writes no `Session` row and sets no cookie. `revoke` ends a credential or burns an unexpired code of that client (`{ revoked }`, never an error). `me` answers the credential's person, read live, and is reachable only by that credential. **The credential is admitted by one branch of `middleware/auth.ts`** — only beside the same spoke's key, only while its grant is unrevoked and unexpired, its client enabled and its person's email verified, and only on `HUB_SESSION_ROUTES`, an exact list of 26 method + registered-URL strings, never a prefix (#8220): `GET /v1/hub-sign-in/me`, #7877's three member reads, #8169's eight member reads, #8170's six member writes, #8435's two contact routes, #8438's two consent routes, #8765's account-agreements pair and #8515's onboarding read and write; every other route answers `404` with no handler run (the status the account area's `read()` loads as "nothing for this caller"), and the list is extended only by a reviewed edit. **The per-person budget (#8220 D2, SEC-8169-4):** each person at each spoke gets 60 requests and 10 writes a minute and 30 writes an hour (a fixed hour window from the first admitted write, so up to twice that across a window edge), keyed on the VERIFIED spoke and person and charged only after admission (a refused credential or an off-list route never gets a bucket); over budget is `429 rate_limited` with `Retry-After`. The write limits refuse only writes, and a refused request is not counted. Every line the request logger writes for an admitted request after admission carries `spokeId` (never the key or the credential); a helper logging through the module-level `logger` carries no request binding. The server-wide per-IP limiter still counts all of one spoke's members together — a named residual whose follow-up (the visitor forward key on member calls) must land before Phase 3 (#8163) ships to a live spoke. A bearer write skips the origin check, so the cross-site-request protection for the member writes is the spoke's: its server actions that send a sealed credential carry it (Phase 3, #8163; SEC-8220-11). A completed password reset (`apps/web/auth.ts` `onPasswordReset`) revokes every credential of that person. The operator registers a spoke's client with `spokes sign-in-client --id <spoke_id> --redirect-uri <url>` (`packages-paid/api-hub/src/bin/spokes.ts`; `--disable` switches it off) — `packages-paid/api-hub/src/lib/hub-sign-in-store.ts` is the only file that names `prisma.oAuthClient` and `prisma.oAuthAuthorization`. **The `hub` claim (#9571 D4; #9568 D5):** read only inside `@working-theory/api-hub` (`src/claim.ts`), through `checkLicense()`, registry first — with no `DATABASE_URL` or no registered spoke the gate answers `not-serving` and never reads the licence; with a spoke and the claim, `claimed`; with a spoke and no claim, `unclaimed`, and `codes`, `token`, `me` (a verified credential) and `clients/:clientId` answer `403 feature_required` naming `hub`; an unreadable licence is `undetermined`, `503 license_undetermined` with `Retry-After: 300`. `revoke` is never gated; a definite answer is held 300 s, `not-serving` 30 s, `undetermined` never; `spokes register` refuses without the claim (`hub-claim-missing`). Lead capture, the resolver and magnet completion never read it, and a boot without the package answers as an empty registry (the four routes `404`). No MCP twin for any of the four — a credential exchange between a site and its hub is not an agent-facing call (#3591 exception, the `/v1/auth/tokens` footing). The browser step lives on the hub's web app: `apps/web/app/api/auth/hub/authorize/route.ts`.
72
+ - `/v1/checkout/sessions` — start a Stripe checkout. **Two paths (#8931, `src/routes/checkout/sessions.ts` over `src/services/offerings/checkout-authorization.ts`; spec `docs/specs/8931-offerings-checkout-authorization-server-side-spec.md`; contract A1–A11, A13).** The path comes from `resolvePublicWorkspace()` and the workspace's own Organization alone — no body field names either: a resolved workspace whose Organization has a Stripe connection takes the **Offerings path**; a presented `x-ailk-spoke-key` the resolver refuses is `503 { error: "tenant_unresolved" }` with no Stripe call; a corrupt connection, or a database read that fails while choosing, is `502 checkout-unavailable`; everything else (keyless, `ambiguous`, off the database adapter, no Organization, no connection) takes the **env path**, today's code. **The env path is the hub's own-products path only, never a spoke's (#9142):** a spoke sells only through its `apps/web` Offerings buy route, which calls this route only for a lookup key its connected workspace's shown set holds, so a spoke's buy always takes the Offerings path; the spoke's own env-key buy route and Stripe webhook are retired. The Offerings path charges `chargeWriteBudget` scope `offeringscheckout` before the body is read — 10 a minute per `request.ip` and 60 per workspace (`OFFERINGS_CHECKOUT_PER_WORKSPACE_PER_MINUTE`), charged one at a time with the IP first, so a request its IP bucket refuses never spends the workspace's, whatever the process's mode — answering `429 { error: "rate_limited", retryAfterSeconds }` with `Retry-After`. It parses `productCode` (the full lookup key) by the one grammar first, builds the connection's own client (`stripeClientForOrganization`), reads org-enabled ∩ workspace-shown from the rows (`getSellableProductIds`, never the snapshot), looks the key up live on that client, and sells only when the Price's own Product is in the set — and, for a workspace saved per Price (#9070 D5), the Price itself is one of its stored shown Prices (`getSellableProductIds`' `priceIds`; `null` for a row saved per Product) — and `normalizeCatalog` keeps that Price under that key and period. Every miss — ungrammatical, not enabled, not shown, moved, archived, nonexistent — is the one `404 { error: "unknown-product-code" }`, never echoing the key; Stripe, the rows or the connection failing is `502`, never the snapshot or the env key. The session is anonymous (no `customer`, no signed-in buyer, so no `ailk_user_id`), sells the authorized Price id, allows promotion codes, `customer_creation: "always"` in payment mode, and is stamped by the server `metadata` and `subscription_data.metadata` `{ site: <workspaceId>, ailk_offering: <lookup key> }`; it is recorded through `recordCheckoutSession` before the route answers, and a failed record expires it on the same client and answers `502`. **The env path refuses an Offerings product (A9):** `createCheckoutSession` gets `{ refuseProduct: isOfferingsProduct }`, so a Price whose Product carries `metadata.ailk_group` (or whose Product did not expand) is the same `404`. **The Offerings path refuses the hub's own plans (#9338 DD5):** right after the grammar parse, a key whose plan is `ailk-pro` or `ailk-pro-plus` (`isPlanPurchaseKey`, any period) is the same `404`, before any read or Stripe call, for every Organization but the one whose fulfilment hook mints licences (#9383 DD5, `fulfilmentFor(organizationId)?.mintsLicences`, read with no Stripe call) — anywhere else such a sale would stamp `ailk_offering` with no licence minted for it. A hub plan under another slug is not covered (the named residual). The request schema and the `201 { sessionId, url }` body are unchanged, so the `start_purchase` parity row is too. **`GET /v1/checkout/sessions/:id` (A13):** when `DATABASE_URL` is set, a session `readCheckoutSessionRecord` holds is read with that record's Organization's own client, only in the record's mode; its line-item names pass the catalog's T1 `normalizeText`; a record that cannot be read, or a connection gone, unusable or in another mode, is `502`, never the env path; no record is today's env read. Logs carry the workspace or Organization, an 8-character session prefix and a fixed reason only.
57
73
  - `/v1/workspaces/:workspaceId/{theme,identity,seo}` (GET/PUT) — per-workspace site config (multi-tenant module). PUT needs `workspace-settings` (owner, admin — `@working-theory/validation`'s permission table (#7636 D2)), checked before the body parse; GET stays open to every member.
58
- - `/v1/workspaces/:workspaceId/meetings` (GET/PUT) — the operator's enabled-slug gate for meeting types (#6183 S9b, `src/routes/workspaces/meetings.ts`). Beside the three siblings above in URL shape and tenant-isolation posture, but registered in its OWN scope, gated on `isModuleEnabled("scheduling")` ALONE — not the `workspace`/`multi-tenant` scope its siblings share, because its GET reaches `services/scheduling/` (see below), which a scaffold without that module does not carry (`tools/lint-slice-registration.ts`, #5701). PUT needs `meeting-types-manage` (owner, admin — `@working-theory/validation`'s permission table (#7636 D2)) because this blob decides which pages the public site serves. GET answers `meeting-types-manage` with the full join and `meeting-types-read` (every role, a member included) with the ENABLED rows only (#7636 D6) — since it does NOT just return the stored blob, it echoes the deployment's whole provider catalog, including the rows the operator chose not to publish (security review, 2026-09-19), which a member never sees. GET is the one segment in this file's family that does NOT just return the stored blob: it ALSO lists the scheduling provider's own bookable event types (`listEventTypes`, #6182), joined with `enabled: boolean`; an unconfigured/unreachable provider answers the same `SchedulingErrorKind` outcome `/v1/schedule/*` uses, distinct from an empty list. **`customerSlugs` (#8171 D5)** — who may book from the member area, independent of `enabledSlugs`: `GET ?include=customers` adds `customer: boolean` per row, and without it the body is unchanged; the PUT carries `customerSlugs`, and a PUT without it keeps the stored list (a read then a write, last-writer-wins), so the public toggle never erases it. `enabledSlugs` keeps its whole-list meaning, so a writer of `customerSlugs` sends the `enabledSlugs` it read. **`include` is a comma list (#9128)** — `?include=customers,private`, in either order, or a repeated `include` key; an unknown value is ignored, so `?include=customers` alone reads as before. **`private` adds `private: boolean` per row** — membership of the stored `privateSlugs` (#8843 D2), the Admin meeting page's Private switch — on the `meeting-types-manage` full join only: a `meeting-types-read` caller never receives it, whatever it asks. The PUT carries `privateSlugs` whole-list and keeps a stored list a body leaves out, as it does `customerSlugs`. An agent's PUT is audited `meetings.set_customers` when it carries `customerSlugs`, else `meetings.set_enabled`. No MCP twin — identity-bound tenant route (#3591 exception).
74
+ - `/v1/workspaces/:workspaceId/meetings` (GET/PUT) — the operator's enabled-slug gate for meeting types (#6183 S9b, `src/routes/workspaces/meetings.ts`). Beside the three siblings above in URL shape and tenant-isolation posture, but registered in its OWN scope, gated on `isModuleEnabled("scheduling")` ALONE — not the `workspace`/`multi-tenant` scope its siblings share, because its GET reaches `services/scheduling/` (see below), which a scaffold without that module does not carry (#5701; since #9565 the package boundary — `@working-theory/api-scheduling` owns both files). PUT needs `meeting-types-manage` (owner, admin — `@working-theory/validation`'s permission table (#7636 D2)) because this blob decides which pages the public site serves. GET answers `meeting-types-manage` with the full join and `meeting-types-read` (every role, a member included) with the ENABLED rows only (#7636 D6) — since it does NOT just return the stored blob, it echoes the deployment's whole provider catalog, including the rows the operator chose not to publish (security review, 2026-09-19), which a member never sees. GET is the one segment in this file's family that does NOT just return the stored blob: it ALSO lists the scheduling provider's own bookable event types (`listEventTypes`, #6182), joined with `enabled: boolean`; an unconfigured/unreachable provider answers the same `SchedulingErrorKind` outcome `/v1/schedule/*` uses, distinct from an empty list. **`customerSlugs` (#8171 D5)** — who may book from the member area, independent of `enabledSlugs`: `GET ?include=customers` adds `customer: boolean` per row, and without it the body is unchanged; the PUT carries `customerSlugs`, and a PUT without it keeps the stored list (a read then a write, last-writer-wins), so the public toggle never erases it. `enabledSlugs` keeps its whole-list meaning, so a writer of `customerSlugs` sends the `enabledSlugs` it read. **`include` is a comma list (#9128)** — `?include=customers,private`, in either order, or a repeated `include` key; an unknown value is ignored, so `?include=customers` alone reads as before. **`private` adds `private: boolean` per row** — membership of the stored `privateSlugs` (#8843 D2), the Admin meeting page's Private switch — on the `meeting-types-manage` full join only: a `meeting-types-read` caller never receives it, whatever it asks. The PUT carries `privateSlugs` whole-list and keeps a stored list a body leaves out, as it does `customerSlugs`. An agent's PUT is audited `meetings.set_customers` when it carries `customerSlugs`, else `meetings.set_enabled`. No MCP twin — identity-bound tenant route (#3591 exception).
59
75
  - `/v1/workspaces/:workspaceId/meetings/connection` (GET, PUT, DELETE) — the workspace's own scheduling connection (#7533, `src/routes/workspaces/meetings.ts`; data access and the cipher in `src/services/scheduling/connections.ts`, the only file that touches `prisma.schedulingConnection`). Same registration, scope and tenant guard as the meetings route above. GET, PUT and DELETE all need `scheduling-connection` (owner, admin — `@working-theory/validation`'s permission table (#7636 D2), the operator's 2026-10-04 decision overturning SEC-7533-11's owner-only write). GET answers `null` or a summary — `provider`, `baseUrl`, `webhookPath`, `createdAt`, `updatedAt` — with no credential, secret, ciphertext or hint of one. PUT takes a strict `{ provider, apiKey, baseUrl? }`; create and replace are one operation, and every PUT mints a new webhook token and secret, returning the secret this once (`Cache-Control: no-store`) so the operator can paste it into the provider — a replace cuts off the previous entry point. `baseUrl` must be `https://api.cal.com` or the deployment's own `CAL_API_BASE_URL` origin, with no path, query or userinfo (`400 base_url_not_allowed`); only the bare origin is stored. The credential and the secret are AES-256-GCM ciphertext under `SCHEDULING_CREDENTIAL_KEY`, bound to the workspace and the column; with the key unset, PUT answers `503 credential_key_not_configured` before any write. DELETE is idempotent (`{ revoked }`) and unconfigures the workspace at once. Because a connection replaces the account every provider call uses, every successful PUT, and every DELETE that revoked a row, writes one `recordAgentAction` audit row (`scheduling.connection.{connect,replace,remove}`; a PUT is `replace` whenever the workspace was served by any provider before it — its own row, or the env credential on a one-workspace deployment) and emails every OTHER owner of the organization through `@working-theory/email`'s `sendSchedulingConnectionChangedEmail` (#7636 D7). Neither step can fail the write: a failed audit is logged at `error`, and the notify step runs in its own `try`, so the one-time secret is always returned; with no sender address or no Resend key it is skipped with a `warn`. Logs carry the recipient count, never an address. The meetings GET above, and the three anonymous `/v1/schedule/*` provider routes below, resolve their provider through `src/services/scheduling/resolve.ts`: a workspace's own row always wins; `CAL_API_KEY` serves a workspace only on a one-workspace deployment; otherwise `not_configured`. No MCP twin — identity-bound tenant routes (#3591 exception).
76
+ - `/v1/workspaces/:workspaceId/slack-connection` (GET, POST, DELETE) — the Organization's own Slack connection (#9748, `packages-paid/api-workspace/src/routes/workspaces/slack-connection.ts`; data access, the Slack calls and the cipher in `packages/api/src/services/slack/connection.ts`, the only file that touches `prisma.slackConnection`). One `TENANCY` row in the `tenant:settings` scope, so it loads on single-tenant Pro, multi-tenant Pro Plus and the hub, and a selected but unlicensed build answers `403 feature_required`; the tenant guard refuses another Organization's `:workspaceId`, and every route acts on `request.tenant.organizationId` only. GET needs any tenant (an agent included) and answers exactly `{ connected: false }` or `{ connected: true, teamName, connectedAt }` — a missing row, a row failing its grammar and a value that does not decrypt all read as not connected, never a throw (there is no environment credential). POST and DELETE need `billing-manage` (the owner alone) and refuse an agent tenant (`403`). POST's order: tenant → owner → write budget (`slackconn`, 10 an hour per Organization, 20 per person, `429` with `Retry-After`) → `SLACK_CONNECTION_KEY` (`503 credential_key_not_configured`) → the strict `{ botToken, signingSecret }` body (4 KiB, `400 validation_failed`) → Slack: `auth.test` (validity, a bot token of one workspace, the granted scopes) then `team.billing.info` (the plan), one 10 s budget, the token only in the `Authorization` header with redirects refused. Refusals: `400 slack_token_refused` (`reason` `token_refused`, `not_bot_token` or `enterprise_install`), `400 slack_scopes_missing` naming every missing one of the five bot scopes (`conversations.connect:write`, `groups:write`, `channels:manage`, `chat:write`, `team.billing:read`), `400 slack_paid_plan_required` on Slack's free plan, `502 slack_unavailable`, `409 slack_team_connected_elsewhere` (one Slack team, one Organization) and `409 connection_changed` (the row moved during verification) — each storing nothing. Success answers the status shape; the token and secret are AES-256-GCM ciphertext under `SLACK_CONNECTION_KEY` (`_PREVIOUS` decrypts during a rotation), bound to the Organization, the Slack team and the column. DELETE needs no key, touches no prospect-channel settings or channel record, and answers `{ removed }` — it does NOT revoke the token at Slack, which stays valid until the owner rotates it or uninstalls the app. Connect, replace and remove each write one `integrations.slack_connection.{connect,replace,remove}` audit row (summary: the team id) and email every owner, the actor included, through `sendSlackConnectionChangedEmail`; neither can change the answer. No response, log line, audit row or email carries the token, the secret or a ciphertext. No MCP twin for connect or disconnect — a person's act, recorded as no-tool rows in `tests/parity/README.md`; the status read's twin is the signed-in `get_slack_connection_status` (#9749), for which GET parses its `:workspaceId` (`400 validation_failed` when malformed).
77
+ - `/v1/workspaces/:workspaceId/prospect-channel` (GET, PUT) — the workspace's prospect-channel settings (#9749, spec `docs/specs/9749-workspace-prospect-channel-settings-get-spec.md`; `packages-paid/api-scheduling/src/routes/workspaces/meetings.ts`, beside the meetings routes, so the same `SCHEDULING` row and `tenant:schedule` scope; data access only through `tenantDb(tenant).prospectChannelSettings`). The blob is `prospectChannelSettingsSchema` (#9747): `enabled`, `memberIds` (Slack member ids invited in addition to the owner who connected Slack, who the channel step always invites), `welcomeTemplate?`, `channelPrefix`, `archiveDays`. GET needs `meeting-types-read` and answers the schema's defaults (`{ enabled: false, memberIds: [], channelPrefix: "prospect", archiveDays: 30 }`) when no row — or a row the schema refuses, logged `settings_unreadable` — is stored. PUT needs `meeting-types-manage` (owner, admin, or an agent an owner or admin granted it; a member is `403` before the body is read), replaces the whole blob after the strict schema accepts the body (32 KiB limit; an omitted field returns to its default; last writer wins), and is refused `409 slack_not_connected` whenever `getSlackConnectionStatus(tenant.organizationId)` answers not connected, whatever the body. An agent's PUT may change `enabled`, `channelPrefix` and `archiveDays` only: a `memberIds` or `welcomeTemplate` different from what is stored is `403 prospect_channel_needs_person`. Every saved PUT writes one `scheduling.prospect_channel.update` audit row naming its actor, person or agent, with the saved `enabled` and `memberIds` and whether the welcome changed (never its text). Every answer is `Cache-Control: private, no-store`; an unexpected error is `500 internal`, logged by class name. Twins: the signed-in `get_prospect_channel_settings` and `set_prospect_channel_settings`, served from `@working-theory/mcp-scheduling`.
60
78
  - `/v1/workspaces/:workspaceId/general` (GET/PUT) — the workspace's own display name, the Pro `workspace` module's general-settings surface (#5371). Registered under `isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")` alongside the site-config routes above, behind the same tenant guard; the scope is always `request.tenant.workspaceId`, never the path param (the guard 403s a mismatch before any handler runs). PUT takes a strict `{ name }` body (1–200 chars, trimmed) and answers `{ name }`; GET answers `{ name, emailFrom }` (#7519 D6), where `emailFrom` is the deployment's sender (`deliverableSenderAddress()` — `AILK_DELIVERABLE_EMAIL_FROM ?? AILK_LEAD_NOTIFICATION_FROM`, the resolver the fulfillment service sends with) or `null` when neither is set, so the Admin's magnet wizard can show it read-only. Role floor on PUT: `workspace-settings` (owner, admin — `@working-theory/validation`'s permission table (#7636 D2)) — renaming is an org-level settings write; GET stays open to every member. No MCP twin — identity-bound tenant route (#3591 exception, same shape as `/v1/billing/*`).
61
- - `/v1/workspaces/:workspaceId/messages` (GET, `?limit=`), `/v1/workspaces/:workspaceId/messages/:leadId` (GET, PATCH) — the Inbox message API (#6216 S7e, `src/routes/workspaces/messages.ts`): the workspace's unhandled messages newest first, the item read (resolves a message handled or not, serving `handledAt`), and the one-way idempotent handled stamp over #6204's `markMessageHandled`. Registered under `isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")`, the same scope as the two routes above. A message is a `Lead` whose `source` is in #6204's closed `MESSAGE_LEAD_SOURCE_LIST` allowlist and that is not an order (#9170, the orders bullet below) — both checked only in `tenantDb(tenant).leads`, never in this file; a non-message lead is a 404 on the item read and the PATCH and absent from the list, the SAME 404 body as an absent id, another workspace's id, or an unbackfilled `workspaceId`. **One served row across all four routes** — `id`, `name`, `email`, `source`, `createdAt`, `handledAt`, `personId`, `body`, `excerpt` — and nothing else (#9349, superseding the 2026-09-19 six-key ruling): `personId` is the lead's Person or `null`; `body` is `payload.message` alone — cut to 16384 code units, line breaks normalized, control, bidi, zero-width and tag characters stripped (`\t` and `\n` kept), trimmed and cut at 4096 code points (`inbox-messages.ts`'s `readMessageBody`), else `null`; `excerpt` is its first line, cut at 140 code points. `payload` itself and every other payload key never cross, and every success body is re-parsed against `inboxMessageSchema` (`inboxMessageListSchema` for the list) — a mismatch is `500 internal_error` logged with issue codes and paths only. `leads-inbox.ts`'s `GET /v1/leads` row is unchanged. PATCH takes `{ "handled": true }` only, `.strict()`; there is no un-handle path (#6204 D4), so `{ "handled": false }` is a 400. The stamp records the actor via `tenant.userId` in `Lead.handledBy` — written, not served. Role floor: `inbox-read` on the GETs and `inbox-act` on PATCH and accept (`@working-theory/validation`'s permission table (#7636 D2) — every role, a member included), the SAME rows `services/inbox-messages.ts` re-checks, checked before any read or write. The role is not self-servable: since #7523 `multi-tenant`'s null-binding fallback grants the default tenant only to the pinned, verified operator, the same check the base `workspace`-only guard applies (see the `AILK_OPERATOR_EMAIL` paragraph below). No anonymous MCP twin — identity-bound tenant routes, and the anonymous `/mcp` carries no caller identity. Each route's twin is a signed-in tool on `/mcp/workspace` (#3591 D13), which forwards the caller's agent key here: `list_inbox_messages` (list), `get_inbox_message` (item read), `mark_inbox_message_done` (PATCH) and `accept_inbox_message` (`POST …/:leadId/accept`).
79
+ - `/v1/workspaces/:workspaceId/messages/threads` (GET, `?view=&page=`), `/v1/workspaces/:workspaceId/messages` (GET, `?limit=`), `/v1/workspaces/:workspaceId/messages/:leadId` (GET, PATCH) — the Inbox message API (#6216 S7e, `src/routes/workspaces/messages.ts`): the thread list (#9686 — a client with a message, or a message with no client, newest first, 25 to a page, with a true count per view `open`/`resolved`/`all`/`archived`; twin `list_inbox_threads`), the workspace's messages whose thread is not archived newest first, the item read (resolves a message handled or not, serving `handledAt`), and Done, which archives the message's thread (#9686 D4). Registered under `isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")`, the same scope as the two routes above. A message is a `Lead` whose `source` is in #6204's closed `MESSAGE_LEAD_SOURCE_LIST` allowlist and that is not an order (#9170, the orders bullet below) — both checked only in `tenantDb(tenant).leads`, never in this file; a non-message lead is a 404 on the item read and the PATCH and absent from the list, the SAME 404 body as an absent id, another workspace's id, or an unbackfilled `workspaceId`. **One served row across all four routes** — `id`, `name`, `email`, `source`, `createdAt`, `handledAt`, `personId`, `body`, `excerpt` — and nothing else (#9349, superseding the 2026-09-19 six-key ruling): `personId` is the lead's Person or `null`; `body` is `payload.message` alone — cut to 16384 code units, line breaks normalized, control, bidi, zero-width and tag characters stripped (`\t` and `\n` kept), trimmed and cut at 4096 code points (`inbox-messages.ts`'s `readMessageBody`), else `null`; `excerpt` is its first line, cut at 140 code points. `payload` itself and every other payload key never cross, and every success body is re-parsed against `inboxMessageSchema` (`inboxMessageListSchema` for the list) — a mismatch is `500 internal_error` logged with issue codes and paths only. `leads-inbox.ts`'s `GET /v1/leads` row is unchanged. PATCH takes `{ "handled": true }` only, `.strict()`, so `{ "handled": false }` is a 400 — Unarchive is #9681's `PATCH …/people/:personId/thread`. **Since #9686 Done is Archive:** it archives the message's client thread through `tenantDb.people.setThreadState` (idempotent) and writes no `handledAt` — that column is read-only legacy, loaded as Archived by the `20261030120000_inbox_thread_state_backfill` migration — and a message with no client is `409 no_thread`; accept no longer marks the message handled. Role floor: the inbox gate `inboxAllowed` (`services/inbox-messages.ts`) — `inbox-read` on the GETs and `inbox-act` on PATCH and accept (`@working-theory/validation`'s permission table (#7636 D2) — every role, a member included), and an AGENT also needs the per-agent `inbox-act` grant on every inbox route, the member-area `…/people/:personId/messages` pair included (#9686 D9, closing SEC-9679-1); checked before any read or write. An Admin listing decision that changes the listing resolves the applicant's thread (`services/thread-auto-resolve.ts`, #9686 D8). The role is not self-servable: since #7523 `multi-tenant`'s null-binding fallback grants the default tenant only to the pinned, verified operator, the same check the base `workspace`-only guard applies (see the `AILK_OPERATOR_EMAIL` paragraph below). No anonymous MCP twin — identity-bound tenant routes, and the anonymous `/mcp` carries no caller identity. Each route's twin is a signed-in tool on `/mcp/workspace` (#3591 D13), which forwards the caller's agent key here: `list_inbox_messages` (list), `get_inbox_message` (item read), `mark_inbox_message_done` (PATCH) and `accept_inbox_message` (`POST …/:leadId/accept`). **Agent-session audit (#9683, spec `docs/specs/9683-agent-session-audit-knowledge-spec.md`):** an AGENT's call to any of these four routes, or to the member-area `GET`/`POST …/people/:personId/messages`, writes exactly one audit row whatever its outcome (`inbox.list_messages`, `inbox.get_message`, `inbox.mark_message_done`, `inbox.accept_message`, `people.message.list`, `people.message.create`; a `403`/`400` is `refused` against the workspace), carrying `metadata.agentSession` `{ sessionId, threadId, reason }` from `X-Agent-Session` / `X-Agent-Reason` through `api-workspace/src/lib/agent-session.ts`. `threadId` is derived by the route, never a header: `hub:<personId>` when the call resolved that Person in this workspace, else `workspace:<workspaceId>`. A person's call writes what it wrote before — nothing on these routes (#3591 § 2.3) — except Done, which is a state change and writes one `inbox.mark_message_done` row for every actor (#9686 D10); the thread list writes `inbox.list_threads` per agent call. **Accept links, never creates (#9690 D5, closing #8601):** `POST …/messages/:leadId/accept` stamps `promotedUserId` with the user the lead's Person is already linked to, and only when `personAddressProved` holds; a sender with no proved account is `409 no_account` ("Invite them to sign up"), nothing written — no code path creates a user from an inbox message, and nothing reads the deployment-wide `User` table by a typed address.
62
80
  - `/v1/workspaces/:workspaceId/submissions` (GET, `?limit=&state=unhandled&magnetId=`), `/v1/workspaces/:workspaceId/submissions/:leadId` (GET, PATCH) — the Inbox submissions API (#7535, `src/routes/workspaces/submissions.ts` over `src/services/inbox-submissions.ts`): every flow completion and waitlist signup of the caller's active workspace, newest first and bounded (`?limit=` 1–200, default 50), the item read (handled or not), and the one-way idempotent handled stamp (`{ "handled": true }` only, `.strict()`; a second PATCH returns the first `handledAt`). Same registration, scope and tenant guard as the messages bullet above. A submission is a `Lead` whose `source` is a fixed submission source (`SUBMISSION_FIXED_SOURCE_LIST` in `@working-theory/validation`) or `flow:<slug>` for a NON-newsletter magnet of THIS workspace — decided only in `tenantDb(tenant).leads`, never in the route; a `flow:` row naming no magnet of the workspace never lists. The list row is seven keys — the message row's six plus `magnet: { slug, title } | null` — and never `payload`. `?magnetId=` narrows to one magnet (limit 1–50) and adds `answers`, `answersTruncated` and `deliverableSentAt` per row; the item read serves the same three, plus `personId` — the lead's Person or `null` — on the item read and the PATCH alone (#9348, `inboxSubmissionItemSchema`); no list row carries it. `answers` is `payload` narrowed by `toAnswers`: server-owned keys dropped, control and bidi characters stripped, every key, value and the entry count bounded. `?state=unhandled` filters `handledAt IS NULL` in the query (the Admin's For-you read); any other `state` is a 400. Every miss on the item read and the PATCH — a malformed, absent or foreign id, a message, an application, a newsletter-magnet lead — is the SAME `404 No such submission`; a `?magnetId=` that is absent, foreign or a newsletter magnet is `404 No such magnet`. Every success body is re-parsed against its own `.strict()` schema before it is sent. Role floor: `inbox-read` on the GETs and `inbox-act` on PATCH (`@working-theory/validation`'s permission table (#7636 D2) — every role, a member included), checked before any read or write. The handlers and the service never log a lead id, email, name or answer. No anonymous MCP twin — identity-bound tenant routes. Each route's twin is a signed-in tool on `/mcp/workspace` (#3591 D13): `list_inbox_submissions` (list), `get_inbox_submission` (item read) and `mark_inbox_submission_done` (PATCH).
63
81
  - `/v1/workspaces/:workspaceId/subscribers` (GET, `?limit=`) — the workspace's newsletter signups, read-only (#7535, `src/routes/workspaces/subscribers.ts`): `{ count, items }`, where `count` is every signup and `items` the newest (1–200, default 50), each `{ id, email, source, subscribedAt, magnet: { slug, title } | null }`. A signup is a `Subscriber` row of this workspace or the `Lead` a newsletter-kind magnet of this workspace wrote, both from `tenantDb(tenant).subscribers`, called directly. No item route and no write: a subscriber is never an item to open or mark handled. Same registration, scope, tenant guard and `inbox-read` floor as the submissions bullet above. No MCP twin — identity-bound tenant route (#3591 exception).
64
82
  - `/v1/workspaces/:workspaceId/orders` (GET, `?limit=`), `/v1/workspaces/:workspaceId/orders/:leadId` (GET) — the Inbox orders read (#9170, `src/routes/workspaces/orders.ts` over `src/services/inbox-orders.ts`): the purchases of the caller's active workspace, newest first and bounded (`?limit=` 1–200, default 50, the messages list's own query), and the item read. Same registration, scope, tenant guard and `inbox-read` floor as the messages bullet above. An order is a `Lead` the hub recorded as a purchase — `spokeCheckout`, or `source: "web"` with `payload.page: "checkout"` (the Offerings webhook's and a spoke's checkout leads, marked or not) — decided by ONE predicate in `tenantDb(tenant).leads` (`ORDER_LEAD_WHERE`), whose exact complement (`NON_ORDER_LEAD_WHERE`, written out so a missing `page` is never SQL NULL) every message read and write carries, so an order is never a message, never stamped handled or accepted, and never a For-you item. The hub's own flow checkout writes no lead of its own (its lead is the flow's `flow:<slug>` submission), so it is not an order. The list row is `id`, `name`, `email`, `offering` (the checkout's lookup key or legacy offering code, stripped and bounded), `planName` (the workspace's own listed name for it — `listPlanOfferings`, matched with any leading `<site>_` removed, one read per request bounded at 1.5 s, else `null`), `amount` (a positive integer of minor units, else `null`), `currency` (three letters upper-cased, else `null`), `createdAt` and `source`; the item adds `personId`. Never `payload`. Every success body is re-parsed against its own `.strict()` schema. Every miss on the item read — a malformed, absent or foreign id, a lead that is not an order — is the SAME `404 No such order`. There is no handled stamp: an order is not an ask. No MCP twin, recorded in `tests/parity/README.md`.
65
- - `/v1/workspaces/:workspaceId/people` (GET, `?limit=&cursor=`), `/v1/workspaces/:workspaceId/people/:personId` (GET), `/v1/workspaces/:workspaceId/people/:personId/contact` (PATCH) — the workspace's clients (#852, `src/routes/workspaces/people.ts` over `tenantDb(tenant).people`; the Person writer is `src/services/person.ts`). A **Person** is one client of one workspace, keyed on a lowercased, trimmed email unique per workspace and created only by `findOrCreatePerson` — called by lead capture, the hub's own Stripe checkout webhook (a live-mode event's `session.customer` only), appointment creation and the inbox accept; `Person.userId` is stamped only on a verified-email match (`stampPersonUser`), never overwritten. The list serves `{ items, nextCursor }` — five keys per row (`id`, `email`, `name`, `planOffering`, `createdAt`), newest first, `limit` 1–200 (default 50); a cursor that does not resolve to a Person of this workspace is `400 invalid_cursor`. The record serves `{ person, timeline, timelineTruncated }`: the Person (`userId` projected to `hasAccount`, never served; the billing pair, and `stripeCustomerUrl` — the Stripe Dashboard link to the customer, built from `billingCustomerId` by `services/person.ts`'s `stripeCustomerDashboardUrl` in the Organization's stored connection's mode, else the environment key's for the Organization that owns it, else `null` — served only to a caller holding `billing-read`; every other caller, an agent key included, gets `billingProvider`, `billingCustomerId` and `stripeCustomerUrl` as `null` and no connection read runs (#9349 DD5/DD6); a failed connection read is a `null` link, the record still `200`, and one `{ event: "person-stripe-link", outcome: "connection_unavailable" }` warn) over a timeline derived at read time — leads and appointments by `personId`, flow sessions, checkouts, deliverable grants and project listings by the Person's newest 200 lead ids, plus an exact-normalized-email fallback for lead-less flow sessions and listings older than the Person backfill, (since #7877) the Person's member-area messages as `message` entries carrying the subject, `hasFile` and `readAt` only, and (since #8171 D4) the Person's inbound member rows as `request` entries (`requestKind`, `subject`) and the versions delivered to them as `version` entries (`pieceId`, `version`, `review`; `at` is `deliveredAt`) — keyed by `personId`, never the email grouping, and never a body, link, quote answer, note or review comment — newest first, at most 50 entries; a malformed, absent or foreign id is one `404 not_found`. Never selected or served: a grant id, a flow-session id, a checkout id, a lead `payload`, a permalink, a Stripe session id, an attendee email, a founder email. Role floor: `people-read` (owner, admin and, since #8514, member — `@working-theory/validation`'s permission table) via `allows`, checked before any read, so an agent key granted `people-read` (the sixth `AGENT_GRANTABLE_PERMISSIONS` entry, clamped to its person's role) reads both in its one workspace. **Agents are withheld (#8514 D3, D4):** `people-read`, `people-write` and `member-pieces-manage` are `AGENT_WITHHELD_PERMISSIONS` — a member holds them by role, an agent (every agent acts at member) never does: `roleAllows` and `allows` read the `agent: true` marker `agentTenant` sets on every agent tenant, so an agent holds the grantable two only by an explicit grant and `people-write` never, and both tenant guards refuse an agent request whose tenant lacks the marker (403, one `agent-tenant-unmarked` error line — fail closed). An operator read only: a Person groups rows by an unverified email, so no client-facing read may key on that grouping (#7877's feed keys on a verified link). **The contact details (#8435, spec `docs/specs/8435-contact-details-on-the-client-spec.md`):** the Person also carries a given name, a family name, an E.164 telephone, a six-part address (the magnet's `addressLine1`/`addressLine2`/`city`/`region`/`postalCode`/`country`, stored whole or not at all), a company name and a job title, plus `contactConfirmedAt`/`contactConfirmedBy` — who last saved the contact by hand. The record serves them as `person.contact` (six keys, `null` where blank) and `person.contactConfirmed` (`{ by: "operator" | "client", at }`, `null` while only captures have written it). **A capture fills a BLANK field of an UNCURATED Person only** (`findOrCreatePerson`'s `contact`: no linked user and never saved by hand; every guarded `updateMany` repeats that condition): a stored value is never overwritten and a cleared field is never refilled. A magnet field's `mapsTo` tag now names the contact field it fills (`email`, `name`, `givenName`, `familyName`, `phone`, `address`, `company`, `jobTitle`); the flow engine hands the visible steps' tagged answers to `routeLead` as `options.contact` and the lead's `payload` keeps every raw answer. A subscriber and a callback with a storable email (a callback's is `payload.email`; at confirm, `recordPaymentLink` links the buyer email when capture had none) now carry a `personId` — a composite foreign key onto `people(id, workspace_id)`, `RESTRICT`. `PATCH …/people/:personId/contact` needs `people-write` (owner, admin, member — by role; never an agent) and takes any of the six keys (`null` clears; an address is whole or `null`; `400 telephone_invalid` for a telephone that is not E.164, `400 validation_failed` otherwise, an 8 KiB body bound), stamps `contactConfirmedBy: "operator"`, answers `{ contact }` and writes one `people.contact.update` audit row naming the actor and the Person, never a value. **The private notes (#8514, spec `docs/specs/8514-redesign-the-person-record-page-spec.md` D5–D8):** `GET`/`PUT …/people/:personId/notes` — the team's one note on a client (`notes`, ≤ 5000 code points), who last saved it and when, and which agent when one did; never selected by any other read and never served to the client (`lib/member-store.ts` names no notes column). The GET needs a person's `people-read` role or an agent's `member-pieces-manage` grant (never a `people-read` grant alone); the PUT needs `member-pieces-manage` through `allows`, takes `{ notes, ifUpdatedAt }` (both required; `null` clears; `ifUpdatedAt` is the `updatedAt` read, a stale one `409 notes_changed` with nothing written; `400 notes_too_long`), and writes one `people.notes.set` audit row whose summary is `{ ok, cleared }`, never the text. Both answer `{ notes: { text, updatedAt, updatedBy: { name, viaAgent } | null } }` with `Cache-Control: private, no-store`, through `tenantDb(tenant).people.notes`/`setNotes`; a malformed, absent or foreign id is one `404 not_found`. Same registration, scope and tenant guard as the routes above (`workspace || multi-tenant`). The notes pair has MCP twins on `/mcp/workspace`: `get_person_notes` and `set_person_notes` (#8514 D8). The other People routes have no MCP twin, by decision — People MCP tools are a later phase of the CRM nucleus (#852 AC 6), recorded in `tests/parity/README.md`.
83
+ - `/v1/workspaces/:workspaceId/people` (GET, `?limit=&cursor=`), `/v1/workspaces/:workspaceId/people/:personId` (GET), `/v1/workspaces/:workspaceId/people/:personId/contact` (PATCH) — the workspace's clients (#852, `src/routes/workspaces/people.ts` over `tenantDb(tenant).people`; the Person writer is `src/services/person.ts`). A **Person** is one client of one workspace, keyed on a lowercased, trimmed email unique per workspace and created only by `findOrCreatePerson` — called by lead capture, the hub's own Stripe checkout webhook (a live-mode event's `session.customer` only), appointment creation and the inbox accept; `Person.userId` is stamped only on a verified-email match (`stampPersonUser`), never overwritten. The list serves `{ items, nextCursor }` — five keys per row (`id`, `email`, `name`, `planOffering`, `createdAt`), newest first, `limit` 1–200 (default 50); a cursor that does not resolve to a Person of this workspace is `400 invalid_cursor`. The record serves `{ person, timeline, timelineTruncated }`: the Person (`userId` projected to `hasAccount`, never served; the billing pair, and `stripeCustomerUrl` — the Stripe Dashboard link to the customer, built from `billingCustomerId` by `services/person.ts`'s `stripeCustomerDashboardUrl` in the Organization's stored connection's mode, else the environment key's for the Organization that owns it, else `null` — served only to a caller holding `billing-read`; every other caller, an agent key included, gets `billingProvider`, `billingCustomerId` and `stripeCustomerUrl` as `null` and no connection read runs (#9349 DD5/DD6); a failed connection read is a `null` link, the record still `200`, and one `{ event: "person-stripe-link", outcome: "connection_unavailable" }` warn) over a timeline derived at read time — leads and appointments by `personId`, flow sessions, checkouts, deliverable grants and project listings by the Person's newest 200 lead ids, plus an exact-normalized-email fallback for lead-less flow sessions and listings older than the Person backfill, (since #7877) the Person's member-area messages as `message` entries carrying the subject, `hasFile` and `readAt` only, and (since #8171 D4) the Person's inbound member rows as `request` entries (`requestKind`, `subject`) and the versions delivered to them as `version` entries (`pieceId`, `version`, `review`; `at` is `deliveredAt`) — keyed by `personId`, never the email grouping, and never a body, link, quote answer, note or review comment — newest first, at most 50 entries; a malformed, absent or foreign id is one `404 not_found`. Never selected or served: a grant id, a flow-session id, a checkout id, a lead `payload`, a permalink, a Stripe session id, an attendee email, a founder email. Role floor: `people-read` (owner, admin and, since #8514, member — `@working-theory/validation`'s permission table) via `allows`, checked before any read, so an agent key granted `people-read` (the sixth `AGENT_GRANTABLE_PERMISSIONS` entry, clamped to its person's role) reads both in its one workspace. **Agents are withheld (#8514 D3, D4):** `people-read`, `people-write` and `member-pieces-manage` are `AGENT_WITHHELD_PERMISSIONS` — a member holds them by role, an agent (every agent acts at member) never does: `roleAllows` and `allows` read the `agent: true` marker `agentTenant` sets on every agent tenant, so an agent holds the grantable two only by an explicit grant and `people-write` never, and both tenant guards refuse an agent request whose tenant lacks the marker (403, one `agent-tenant-unmarked` error line — fail closed). An operator read only: a Person groups rows by an unverified email, so no client-facing read may key on that grouping (#7877's feed keys on a verified link). **The contact details (#8435, spec `docs/specs/8435-contact-details-on-the-client-spec.md`):** the Person also carries a given name, a family name, an E.164 telephone, a six-part address (the magnet's `addressLine1`/`addressLine2`/`city`/`region`/`postalCode`/`country`, stored whole or not at all), a company name and a job title, plus `contactConfirmedAt`/`contactConfirmedBy` — who last saved the contact by hand. The record serves them as `person.contact` (six keys, `null` where blank) and `person.contactConfirmed` (`{ by: "operator" | "client", at }`, `null` while only captures have written it). **A capture fills a BLANK field of an UNCURATED Person only** (`findOrCreatePerson`'s `contact`: no linked user and never saved by hand; every guarded `updateMany` repeats that condition): a stored value is never overwritten and a cleared field is never refilled. A magnet field's `mapsTo` tag now names the contact field it fills (`email`, `name`, `givenName`, `familyName`, `phone`, `address`, `company`, `jobTitle`); the flow engine hands the visible steps' tagged answers to `routeLead` as `options.contact` and the lead's `payload` keeps every raw answer. A subscriber and a callback with a storable email (a callback's is `payload.email`; at confirm, `recordPaymentLink` links the buyer email when capture had none) now carry a `personId` — a composite foreign key onto `people(id, workspace_id)`, `RESTRICT`. `PATCH …/people/:personId/contact` needs `people-write` (owner, admin, member — by role; never an agent) and takes any of the six keys (`null` clears; an address is whole or `null`; `400 telephone_invalid` for a telephone that is not E.164, `400 validation_failed` otherwise, an 8 KiB body bound), stamps `contactConfirmedBy: "operator"`, answers `{ contact }` and writes one `people.contact.update` audit row naming the actor and the Person, never a value. **The private notes (#8514, spec `docs/specs/8514-redesign-the-person-record-page-spec.md` D5–D8):** `GET`/`PUT …/people/:personId/notes` — the team's one note on a client (`notes`, ≤ 5000 code points), who last saved it and when, and which agent when one did; never selected by any other read and never served to the client (`lib/member-store.ts` names no notes column). The GET needs a person's `people-read` role or an agent's `member-pieces-manage` grant (never a `people-read` grant alone); the PUT needs `member-pieces-manage` through `allows`, takes `{ notes, ifUpdatedAt }` (both required; `null` clears; `ifUpdatedAt` is the `updatedAt` read, a stale one `409 notes_changed` with nothing written; `400 notes_too_long`), and writes one `people.notes.set` audit row whose summary is `{ ok, cleared }`, never the text. Both answer `{ notes: { text, updatedAt, updatedBy: { name, viaAgent } | null } }` with `Cache-Control: private, no-store`, through `tenantDb(tenant).people.notes`/`setNotes`; a malformed, absent or foreign id is one `404 not_found`. Same registration, scope and tenant guard as the routes above (`workspace || multi-tenant`). The notes pair has MCP twins on `/mcp/workspace`: `get_person_notes` and `set_person_notes` (#8514 D8). The record, contact and consents routes have no MCP twin, by decision — People MCP tools are a later phase of the CRM nucleus (#852 AC 6), recorded in `tests/parity/README.md`. **The changed mode, the agent reply and the address proof (#9714, spec `docs/specs/9714-hub-side-agent-thread-spec.md`):** `GET …/people?changedSince=<ISO 8601 with offset>&limit=` serves `{ items: [{ id, lastInboundAt }], more }` — each Person whose thread gained an inbound item (a message lead, never an order, or an inbound member request) strictly after the instant, ascending by that item's instant then id, through `tenantDb(tenant).people.changed` over `database/tenancy.ts`'s `PEOPLE_CHANGED_SQL`; a page never ends inside a millisecond it shares with the next Person, so a poller passes back the last `lastInboundAt`; `changedSince` with `cursor` is `400 validation_failed`; every call in this mode writes one `people.list.changed` audit row (counts only) carrying an agent's session, and the plain list still writes none. The list in both modes has its MCP twin, `list_people`. The record's `person.emailVerified` is `services/person.ts`'s `personAddressProved`: linked to a verified User whose email is still the Person's and who holds an `Account` or `Session` row — never `userId` alone and never the operator's contact stamp. `POST …/people/:personId/messages` accepts an agent holding `people-read` with `inbox-act` as well as `member-pieces-manage` (plus the inbox gate); an agent's message stores its id in `interactions.created_by_agent_id` (outbound-only, CHECKed), and the thread's `message` entry serves `messageId` and `sentBy: { kind: "user" | "agent", name }`, its `lead` entry `senderProved: false` and its `request` entry `senderProved: true` (working-theory-agents#565 — a form lead never proves its sender). **The thread, its state, its pointers and the erasure (#9681, spec `docs/specs/9681-person-thread-read-spec.md`; `packages-paid/api-workspace/src/routes/workspaces/people-thread.ts`):** `GET …/people/:personId/thread` (`?limit=` 1–100, default 50, `&cursor=`) pages the whole thread newest first with no cap — every timeline kind plus `external_thread` pointers and `memory` events, never a body or a draft — each page carrying `state` (`readAt`, `archivedAt`, `resolvedAt`); a cursor it did not mint is `400 invalid_cursor`. `PATCH …/thread` takes `{ field, on, ifUpdatedAt }` (`ifUpdatedAt` is that field's own value as read; a stale one is `409 thread_state_changed`, nothing changed). `GET …/external-threads` lists the Person's email and Slack thread pointers (newest first, at most 200, `truncated`); `POST …/external-threads` upserts one by channel and ref (`201` new, `200` refreshed) — channel, ref (a Gmail thread id or a Slack `<channel>/<ts>`, never a URL), a ≤ 280-character summary (`400 summary_too_long`), `lastSeenAt` — and refuses any other key, so no message text is stored. Reads need `people-read`; writes need `people-read` AND `inbox-act` (an agent holds both only by grant). `DELETE …/people/:personId` erases the client in one transaction through `services/person-erasure.ts` (`people.erase`, #9680) — a person-tenant owner only, never an admin, a member or any agent key, checked before any read; `503 erasure_unavailable` with nothing deleted when it cannot run. Every request to these five routes writes one `people.*` audit row before its response (reads included; the erasure's success row names the Person id and table counts only); an agent's row carries `metadata.agentSession` `{ sessionId, threadId: hub:<personId>, reason }` from `X-Agent-Session` / `X-Agent-Reason` (`api-workspace/src/lib/agent-session.ts`; a bad header is dropped by the writer, never refused). Four MCP twins on `/mcp/workspace` — `get_person_thread`, `set_person_thread_state`, `list_external_threads`, `add_external_thread`; the erasure has no tool, by decision (recorded in `tests/parity/README.md`). **The client's memory (#9682, spec `docs/specs/9682-person-memory-routes-spec.md`; `packages-paid/api-workspace/src/routes/workspaces/people-memory.ts` over `services/person-memory.ts` and `tenantDb(tenant).people.memories`):** nine routes under `…/people/:personId/memories` — `GET` the index (an agent: live, unexpired entries only, filtered in the store; a person: every status, `expired` computed), `GET …/:path` (stamps `last_read_at`, so a person's read restores an expired document), `POST` a proposal (`path` in the body; `201` a new draft, `200` a draft's next version or a live document's pending version — never `live`; the `sourceRef` must name a row of this workspace's Person, else `400 invalid_source`), and six person-only routes — `PATCH …/:path` (`ifVersion` required), `DELETE …/:path`, `POST …/:path/confirm` (`{ version }`, which must be the highest), `POST …/:path/redact` (the document and every version, terminally), `GET …/:path/versions`, `POST …/:path/versions/:n/restore` (appends a copy). Reads need `people-read` or `people-memory-propose`; a proposal needs `people-memory-propose`; the person-only routes refuse every agent key whatever its grants, and an agent body carrying `status` is `403` before it is parsed. Every write takes the document's row lock; a stale `ifVersion` or confirm is `409 memory_changed`; a redacted or expired path `409 path_unavailable`; 200 topics, 50 drafts or 100 versions `409 memory_full`. Expiry is `people.memory_expiry_days` (180) in `project.yaml`, read through `PERSON_MEMORY_EXPIRY_DAYS`; nothing is deleted by it. Every request writes one `people.memory*` audit row (ids, enums and counts only, never a path, title or body). Three MCP twins — `list_person_memories`, `get_person_memory`, `propose_person_memory`; the six person-only routes have no tool, by decision (recorded in `tests/parity/README.md`).
66
84
  - `/v1/workspaces/:workspaceId/people/:personId/pieces` (GET, POST), `…/people/:personId/pieces/:pieceId` (PATCH), `/v1/workspaces/:workspaceId/piece-stages` (GET, PUT), `…/people/:personId/plan` (PUT), `…/people/:personId/messages` (GET, POST) — the member area as an operator keeps it (#7877, `src/routes/workspaces/{pieces,plan,messages-out}.ts` over `tenantDb(tenant).memberArea`; spec `docs/specs/7877-the-member-area-contract-a-spec.md`). A **Piece** is one deliverable for one client: a title (1–200), a **stage** drawn from the workspace's OWN declared list (`WorkspacePieceStages`, one row per workspace; no row is the product default `requested, in-progress, in-review, delivered`, which lives only in `lib/tenant-db.ts`'s `DEFAULT_PIECE_STAGES` — there is no stage enum anywhere), a `needsClient` flag and `updatedBy`. The **plan** is `Person.planOffering` (#852's field — never a Plan model), written only by `services/person.ts`'s `setPersonPlanOffering`. A **message** is an `Interaction` on the `member` channel, direction `outbound`: a plain-text subject (1–200) and body (1–10000), both stripped of control and bidi characters (the body keeps `\n`, `\t`), and an optional attachment that must be an `Asset` of THIS workspace tagged `private` with a raster-image or PDF type (never SVG) — `400 asset_not_found` / `400 asset_not_attachable` otherwise. Both records reference a Person of the same workspace through a composite `(person_id, workspace_id)` foreign key. Role floors from `@working-theory/validation`'s permission table: the two list reads need `people-read`, the stage PUT `workspace-settings`, the stage GET the guard alone, each checked by role (`roleAllows(tenant, …)`) before any read, so an agent's `people-read` grant does not reach pieces or message bodies; the four writes — piece POST and PATCH, plan PUT, message POST — need `member-pieces-manage` (owner, admin, member since #8514), checked with `allows(tenant, "member-pieces-manage")` before any read, so an agent key granted it (#7878, the seventh `AGENT_GRANTABLE_PERMISSIONS` entry) keeps them in its one workspace, attaching only an asset the message's own checks admit; attaching a file to a message or delivering one as a version also needs `assets-write` (#8514 D19), so a member sends text and link versions, never a private file. `GET …/plan-offerings` (#8514 D9, `people-read`) lists plan keys for the Admin's picker from one of two sources (#8934, `services/plan-offerings.ts`): with the `commerce` claim, a connected workspace with a current Offerings snapshot lists one item per sellable Price in its shown set (`getWorkspaceShownSet`, never Stripe) — `key` the full lookup key, `name` the product name and its billing word, `type` `plan` or `addon`; an unclaimed workspace, or one with no connection or no selection, lists its own Service then Product pages (`services/public-site-resolver.ts`'s `workspaceContentAdapter`: off the database adapter only the default workspace has content, every other workspace `[]`). An undetermined licence or an expired or empty snapshot is a failed read (`{ offerings: [] }` and one warn line), and so the magnet checkout check and the purchase name fail closed. `people-write` (owner, admin, member since #8514) is never agent-grantable and is no longer read by these routes — the Admin person page reads it to render its forms. A malformed, absent or foreign Person or piece is one `404 not_found`; a stage outside the declared list `400 invalid_stage`; a stage-list PUT that drops a stage a piece still holds `409 stage_in_use` with nothing written — piece writes and stage changes take one per-workspace advisory lock first, in one transaction. Lists hold at most 50 rows, newest first. Every write that reaches the store records one `recordAuditAction` row naming the actor (`people.piece.create`, `people.piece.update`, `people.stages.set`, `people.plan.set`, `people.message.create`); no audit summary or log line carries a title, subject, body or offering. The operator's message read never stamps `readAt`, and serves an attachment by its sanitized download name, never its asset id. **The checkout plan rule:** a `POST /v1/leads/submit` from a registered spoke (`viaSpoke`) whose `Idempotency-Key` is a LIVE Checkout Session id (`cs_live_…`), with `source: "web"`, `payload.page: "checkout"`, a string `payload.offering` and `payload.site`, and an `amountTotal` that is absent, `null` or positive, names a plan — the offering minus a leading `<site>_` — but never writes one (#8793): its email is what the buyer typed at Stripe Checkout, so the lead HOLDS the plan (`services/lead-routing.ts`, one `{ event: "checkout-plan", outcome: "held" | "skipped", siteId }` line, no Person call; a request relayed for a visitor never reaches it). `services/person.ts`'s `releaseHeldCheckoutPlan` releases it once, when `resolveMemberPersonId` (a member read, or `GET /v1/hub-sign-in/me`) first links the Person to the signed-in user whose verified email is the Person's own (a fresh `stamped` only — never a Person linked earlier, never the Inbox accept's link): into a blank plan only, through `setPersonPlanOffering`'s link guard, and only when the Person has at most 50 marked leads and those under that email all name one key — two keys, or more than 50 marked leads, release nothing and the operator sets the plan. Never on a replay, never refusing or changing the lead, the purchase message, the purchase email or the consent grant, and with no spoke change. Same registration, scope and tenant guard as the routes above (`workspace || multi-tenant`). **Proposals (#8516, spec `docs/specs/8516-proposals-as-an-artifact-kind-spec.md`):** a piece now carries a `kind` — `deliverable` (every piece before #8516, the column default) or `proposal` — served on every piece row and never set or changed through the piece routes (a body naming it is `400 validation_failed`), plus a proposal's optional price (minor units of `usd`/`eur`/`gbp`/`cad`/`aud`). `GET …/people/:personId/proposals` (a person's `people-read` role or an agent's `member-pieces-manage` grant) lists the Person's proposals with their price, approval, paid stamp and latest version, and `payments` (`on-approval` or `off`); `POST …/people/:personId/proposals` (`member-pieces-manage`; a file version also `assets-write`) is Send proposal — the proposal (`needsClient: true`, the first declared stage unless named) and its version 1 in one transaction under the piece lock, one `people.proposal.create` audit row whose summary is `{ priced }` alone. **Only a person prices (C4, the money gate):** an agent's create with a price, and an agent's PATCH or Deliver a version on a priced proposal, are `403 proposal_needs_person` with nothing written. Both read and write through `tenantDb(tenant).memberArea`. The proposals pair has MCP twins on `/mcp/workspace`: `list_proposals` and `create_proposal` (#8516 D7). **Start onboarding (#8515 D5, D6):** `POST …/people/:personId/onboarding` (`member-pieces-manage` through `allows`) takes `{ subject, body }` (the message bounds, no attachment — a body naming `assetId` is `400 validation_failed`) and writes one outbound member message whose `payload` is `{ action: "onboarding" }` — the one outbound payload `interactions_outbound_shape` admits — which the client's own site renders as a link to its `/account/onboarding`; it reads the workspace's onboarding magnet through `tenantDb(tenant).flows.bySlug` and never seeds one: no `diagnostic` row at that slug with `customersEnabled` is `409 onboarding_off` with nothing written. A malformed, absent or foreign Person is the same `404 not_found`; one `people.onboarding.start` audit row, no summary. The message POST never carries the action. The other member-area routes have no MCP twin, by decision — member-area MCP tools are a later phase of the CRM nucleus, recorded in `tests/parity/README.md`.
85
+ - `…/people/:personId/messages` (POST) emails, and `/v1/workspaces/:workspaceId/people/:personId/invitation` (GET, POST) invites (#9690, spec `docs/specs/9690-invite-and-reply-spec.md`): a reply is saved, then emailed in full with a link to the member area (`/account/inbox/message/:id`) when the Person's address is proved, else to sign-up — `201 { message, delivery }`, where a failed email never fails the 201 and an agent's reply goes only to a proved address (`withheld` otherwise); it writes no thread state. The invitation pair (a person only — every agent is `403` with one `refused` row) reports `{ hasAccount, lastInvitedAt }` and, on `{ "invite": true }`, emails the site's sign-in link and creates nothing; a repeat is a Resend, a proved Person is `409 has_account`. Both emails come from `AILK_INBOX_FROM`, else `AILK_LEAD_NOTIFICATION_FROM`, through `@working-theory/api/services/inbox-mail.js`. Per-caller send caps (`api-workspace/src/services/send-caps.ts`: 20 sends an hour per caller, 10 replies an hour and 3 invitations a day per caller to one Person) are counted from the rows each send writes before its email, under a per-workspace advisory lock (`tenantDb.inboxSends`); a capped send is `429` with `Retry-After` and the named reason, a lock or count failure `503 send_cap_unavailable`. No MCP twin for the invitation pair (it refuses agents; `tests/parity/README.md`).
67
86
  - `/v1/member/pieces` (GET), `/v1/member/messages` (GET), `/v1/member/messages/:id/file` (GET) — the member reads: a client's OWN pieces and messages, served to the spoke that signed them in (#7877, `src/routes/member/{pieces,messages}.ts`). Registered in `authScope` only (auth, NOT tenant-guarded — a client holds no membership; an agent key is 403 at the preHandler) under the same `workspace || multi-tenant` predicate, at the exact URLs `HUB_SESSION_ROUTES` admits a hub credential on. Who and where come only from `services/member-area.ts`'s `resolveMember`, before any member read: the caller through `middleware/auth.ts`'s `memberCaller` — the hub's own cookie session or a hub credential, never a personal `ApiToken` (`401 { error: "unauthorized" }`); the workspace from the calling spoke's `x-ailk-spoke-key` (a keyless call only on a one-workspace deployment that is not a hub), and a hub credential must name that same workspace, else `503 tenant_unresolved`; an unverified email `403 email_unverified`. The Person is the one linked by `Person.userId`, else the one whose email is the caller's verified email, linked on that first read by `stampPersonUser`'s rule; no Person is `items: []`. Every read goes through the named boundary `src/lib/member-store.ts`, scoped by that resolved workspace AND Person: at most 50 rows, newest first; a message serves `{ id, subject, body, file: { filename, contentType } | null, createdAt, readAt }` — never an asset, creator, person or workspace id — and the first read stamps `readAt` ("shown to the client's spoke", not "read"; served as it stood before the stamp). The file read streams the attachment only when the message is the caller's own and its stored type is attachable — any other case is one `404 not_found` — `attachment` with a sanitized, extension-pinned name, `nosniff`, the sandbox `Content-Security-Policy` and `Cache-Control: private, no-store`. `@working-theory/api-client`'s `member.*` is the spoke's caller; a spoke that re-serves the bytes sends those five headers unchanged. No MCP twin, by decision — identity-bound client reads behind a hub credential; the anonymous MCP surface is identity-blind, recorded in `tests/parity/README.md`.
68
87
  - `/v1/member/purchases`, `/v1/member/appointments`, `/v1/member/artifacts`, `/v1/member/artifacts/:pieceId/versions/:version/file`, `/v1/member/meeting-types`, `/v1/member/availability`, `/v1/member/request-form`, `/v1/member/billing` (all GET) — the Phase 1 member reads (#8169, `src/routes/member/{purchases,appointments,artifacts,meeting-types,availability,request-form,billing}.ts`; spec `docs/specs/8169-member-reads-spec.md`). Same caller seam, refusals and registration as the bullet above, each read through one `src/lib/member-store.ts` function, each success body re-parsed against its `@working-theory/validation` `member.ts` schema. All eight are in `HUB_SESSION_ROUTES` (#8220). Appointments, artifacts and the file read are scoped by the resolved workspace AND Person (≤ 50, never an attendee's name, email or time zone; never a version or asset id; the file read is one `404 not_found` for every miss, a version above the 32-bit column included, and sends `messages.ts`'s five headers). **Purchases and billing read at organization grain** (the operator's #8140 ruling): only Persons whose `userId` is ALREADY the caller, inside the calling workspace's organization — they link nothing, so a caller with no Person here still sees a linked Person elsewhere. That grain rests on one precondition nothing enforces (SEC-8169-2, stated at `lib/member-store.ts`'s `linkedPersonsInOrganization` and pinned by `src/__tests__/member-isolation.test.ts`): every workspace of one organization belongs to one owner, so a deployment must never put two businesses' workspaces in one organization. A purchase is a completed hub `FlowCheckout` whose lead carries its Person's own email, or (#8185) a `Lead` the hub marked `spokeCheckout` when it was written — a registered spoke's unrelayed (`x-ailk-relayed` absent), LIVE paid checkout, the #7877 plan rule's own predicate evaluated in `services/lead-routing.ts` before the create and never updated, or (#8932) the per-Organization Stripe webhook's live, recorded, paid Offerings session, marked by that predicate's Offerings twin — served with the same Person-workspace and Person-email rules, its offering kept only if it is a plan-offering key (SEC-8169-6); an anonymous, relayed, test-mode or pre-#8185 lead is never marked and never served (no backfill), and `tests/architecture/idempotency-key-producers.test.ts` fails if any spoke-side file, MCP tool or client wrapper starts sending an `Idempotency-Key`. **Billing answers a hub credential only**: any other caller gets `404 not_found` before `resolveMember` and before any read, and no log line ever names a `cus_` id; a hub credential reads it at the organization grain, under that precondition. Meeting types and availability join the workspace's `customerSlugs` (independent of `enabledSlugs`) to the scheduling provider — a provider failure is its `SCHEDULING_ERROR_STATUS`, never an empty list; a slug outside `customerSlugs` is `404 meeting_type_not_offered` with no provider call — and register under `scheduling && (workspace || multi-tenant)`, because only the `scheduling` module delivers `services/scheduling/`. The request form is the `request-quote` contact row while `customersEnabled`, whatever its status, else `{ form: null }`. **`GET /v1/member/onboarding` (#8515 D7)** serves the standard onboarding magnet — the `diagnostic` row at slug `onboarding` while `customersEnabled`, whatever its status — reduced to display keys (a field's name, input type, `required`, select options and label/help/placeholder; a choice question's name, options, `multi` and `exclusiveValues`; never a `showIf`, `mapsTo`, `purposes`, field rule, `segments`, `whenHidden` or `resultsReveal`), else `{ form: null }`. `GET /v1/member/messages` items carry `action: "onboarding" | null` (#8515 D6). No MCP twin, by decision — the same reason, recorded in `tests/parity/README.md`.
69
88
  - `/v1/member/requests` (POST), `/v1/member/messages` (POST), `/v1/member/bookings` (POST), `/v1/member/artifacts/:pieceId/review` (POST), `/v1/member/profile` (PATCH), `/v1/member/profile/contact` (GET, PATCH), `/v1/member/deletion-request` (POST) — the Phase 1 member writes (#8170, the contact pair #8435, `src/routes/member/{requests,messages,bookings,artifact-review,profile,deletion-request}.ts`; spec `docs/specs/8170-member-writes-spec.md`). Same caller seam and refusals as the reads, and a cookie write must carry a trusted `Origin` (the preHandler's 401). Nothing in a body, path or query names a person, user or workspace; every body parses its `@working-theory/validation` `member.ts` schema (`400 invalid_input`, or `400 link_not_https` when a link is why) and every success body is re-parsed before it is sent. **Requests, messages and reviews are each ONE inbound `Interaction` on the caller's own Person** — channel `member`, direction `inbound`, `createdBy` the caller — and never a `Lead`: a request is checked against the `request-quote` magnet's CURRENT field steps (`400 answers_invalid` for a key the form does not ask, a missing required answer, an array answer or a failed field rule; `409 request_form_off` when the form is off or the caller has no Person) and stores `{ flowSlug, items }` with each label as the form shows it; a message stores `{ link }` when one is sent (`404 not_found` with no Person). **The cap:** every inbound write runs in one transaction whose first statement is the Person's advisory lock (`ailk:member-inbound:<personId>`), then counts the Person's inbound rows of the last hour — 30 or more is `429 too_many_requests` with nothing written. **The review** lands only on the piece's LATEST version (`409 version_not_latest`), only once per version and never after `Piece.approvedAt` is set (`409 review_closed`); a missing, foreign or approved piece is refused from one read before any lock, otherwise the Person lock then the workspace's piece lock (`lib/tenant-db.ts`'s `pieceStagesLockKey`, the key the operator's piece writes take), and it writes the version's `review`, `reviewComment`, `reviewedAt`, the piece's `approvedAt` on an approval and the `review` Interaction in that one transaction — never a title, stage, link, asset or note; asking for changes stays open on every new latest version (#8140 disposition 3). **The booking** books an offered `customerSlugs` meeting type at `<handle>/<slug>` as the caller (verified email, own name), refuses a Person holding 10 upcoming non-cancelled appointments `429` before any provider call, answers a provider failure through `SCHEDULING_ERROR_STATUS`, and binds a spoke caller's booking to its workspace as `/v1/schedule/book` does; it registers under `scheduling && (workspace || multi-tenant)`. **The profile** writes only the caller's own `User.name` / `User.image` (https only) and answers `{ name, image }`. **The contact** (`profile/contact`, #8435) is the caller's contact details on THIS site, never on the hub `User` and never synced to another site: the GET serves the contact of the caller's linked Person only once a human saved it (`contactConfirmedAt` set — a value only an anonymous capture wrote is `{ contact: null }`, SEC-8435-B); the PATCH takes all six keys, each nullable and required, and REPLACES the whole contact (so no value the client never saw survives), stamping `contactConfirmedBy: "client"`. With no Person yet it finds or creates the Person for the caller's VERIFIED email and links it through `stampPersonUser`'s rule; any outcome but linked is `409 contact_unavailable`, nothing written. Only the caller's own linked Person (`{ id, workspaceId, userId }`) is ever written; `400 invalid_input` or `400 telephone_invalid` for a refused body, an 8 KiB body bound, and `{ event: "member-contact", outcome }` as its one log line. **The deletion request** is filed through `routeLead` as a `web` message lead (kind `deletion`, the caller's verified email, no idempotency key) into `resolveSiteWorkspaceId()` — the deployment's own site workspace, so a spoke caller's request reaches the hub operator (`sentTo: "hub-operator"`), never the spoke's. Every data write goes through `src/lib/member-store.ts`; every write logs only `{ event, outcome }` and a `ref` of at most 8 characters (`services/member-area.ts`'s `memberWriteLog`), never a subject, body, answer, link, note, name, email or full id. All six (and the two contact routes) are in `HUB_SESSION_ROUTES` (#8220): a spoke's deletion request reaches the hub operator, and a spoke's profile write changes the name and photo the hub's `me` answers. **The onboarding submission (#8515 D8–D10):** `POST /v1/member/onboarding` takes the request body's `{ answers }` (`plainText`, ≤ 64), checks them against the served onboarding form (every key a served field or question; a required field non-blank; `email`, https `url`, `select` options and `minLength` as the request checker applies them; a single choice one option value; a `multi` choice 1…options distinct option values, an exclusive value only alone — `400 answers_invalid` / `link_not_https`; `409 onboarding_off` with no Person or no form) and writes ONE `flow:onboarding` `Lead` — never an `Interaction` — on the caller's own Person with the Person's email, the answers, `flowSlug`, `locale` and the server-written `submittedBy` (the caller's user id, a server-owned key the Inbox never shows as an answer), so it is an Inbox submission and a `lead` Activity entry with no reader change. Under the Person's inbound lock, at most five of the caller's OWN submissions an hour (`429 too_many_requests`) — only leads carrying the caller's `submittedBy` count, so an anonymous `POST /v1/leads/submit` with that source and the client's email never fills the allowance. Both onboarding routes are in `HUB_SESSION_ROUTES`. **A proposal (#8516):** the artifacts read and the review answer carry `kind`, `price` and `payment` (`{ status: "due" }` for an approved, priced, unpaid proposal where payment runs; `{ status: "paid", paidAt }` once paid; else `null`), and a proposal's review — the same write, which is how a proposal is accepted or sent back — also writes one `people.proposal.client_review` audit row as the client (`recordWorkspaceAuditAction`). **`POST /v1/member/artifacts/:pieceId/payment`** opens the client's Stripe Checkout page for their own approved, priced, unpaid proposal and answers `{ url }` — a `checkout.stripe.com` URL only (`isStripeCheckoutUrl`, checked before anything is stored), `Cache-Control: private, no-store`. It sits behind the commerce claim (`requireCommerceClaim`, the module-disabled 404 before any read) and its own strict per-IP limiter (`proposalpay`); it is NOT on `HUB_SESSION_ROUTES`. **Same site, sandbox only:** `services/proposal-payments.ts`'s `proposalPaymentsFor` is `on-approval` only with the claim, on a one-workspace deployment whose workspace is the proposal's (the deployment's one Stripe account is provably that seller's), with a key `assertSandboxKey` admits and `FRONTEND_URL` set — and never for a call carrying a spoke key or a hub credential; anywhere else approval records acceptance only and the pay route is `404 not_found`. The session is card only, expires in an hour, carries a fixed product name and the stored amount (never a request value), and is reused while it has more than five minutes left; a concurrent second session is expired. `409 proposal_not_approved` / `409 proposal_paid` / `502 payment_unavailable`; each opened session writes one `people.proposal.checkout` audit row. No MCP twin, by decision — identity-bound client writes, recorded in `tests/parity/README.md`.
70
89
  - `/v1/consent/cookie` (POST), `/v1/consent/unsubscribe` (POST), `/v1/consent/sign-up` (GET), `/v1/consent/account` (GET, POST), `/v1/consent/platform` (GET, POST), `/v1/member/consents` (GET, POST), `/v1/workspaces/:workspaceId/people/:personId/consents` (GET) — the consent ledger's routes (#8438, spec `docs/specs/8438-consent-registry-and-ledger-step-spec.md`; #8765, spec `docs/specs/8765-sign-up-refuses-without-the-spec.md`). `consent_grants` is THE ledger: append-only (the database refuses any change to a stored row, and any truncation, on it and on `consent_visitor_links`), every row naming its `source`, its `policyVersion` and, when known, its Person; it has two writers — `src/services/consent.ts` (`recordConsent`, `recordPersonConsent`, `recordCookieChoice`, `linkConsentVisitor`, `recordCheckoutConsent`; every source) and the web's sign-up hook `apps/web/lib/sign-up-agreements.ts` (#8765 D4), which writes only `sign_up` rows (subject `user`) of a user it creates in the same Better Auth transaction, from the evidence `GET /v1/consent/sign-up` built with `consent.ts`'s own functions — and every reader is `src/lib/consent-store.ts` (`DISTINCT ON` current-state reads, never a window). **Two readings of a Person's rows:** the OWN reading (`sign_up`, `account_settings`, `email_unsubscribe` — a verified caller or a signed link) is what the member's screens and any send read; the ANY-SOURCE reading (adds `magnet`, `api`, `cookie_banner`, `checkout`, and linked visitors' cookie rows) is the operator's, marked `unverified` — an anonymous form's row never changes a member's own answer. **The checkout grant (#8765 D13, D14):** `checkout` is the eighth source (the `consent_grants_source_known` CHECK names all eight). The hub webhook's `checkout.session.completed` arm, after the flow-checkout Person step, writes one `account_terms` row (subject `person`, source `checkout`, `sourceRef` the Checkout Session id, locale `en`, the current statement and version) through `recordCheckoutConsent` — one transaction under the `ailk:consent:<workspaceId>:checkout:<sessionId>` lock, nothing written when that Person already holds that session's row — only when `consent.terms_of_service` is `accepted` and `metadata.ailk_terms` is `account_terms@<current version>`; the Person is `services/checkout-buyer.ts`'s `checkoutBuyerPerson` (a proposal session → the piece's Person in its `ailk_workspace` through `lib/proposal-paid-store.ts`'s `proposalPersonId`; a flow checkout → the Person the Person step's `resolveCheckoutPerson` found or created, never created here; a signed-in buyer's `ailk_user_id` → the Person of that user's verified email in the site workspace, test mode included; else, for a paid live-mode session only, `customer_details.email`'s Person). **A spoke purchase records the same grant (#8778):** a spoke's checkout runs on the spoke's own Stripe account, so the hub webhook never sees it; instead the first write of a `spokeCheckout` lead with a Person (`services/lead-routing.ts`) whose `payload.termsAccepted` equals the current `account_terms@<version>` stamp (`currentAccountTermsStamp`) calls `recordCheckoutConsent` for the lead's Person, `sourceRef` the lead's `Idempotency-Key` (the Checkout Session id) — a `null` or absent value writes nothing (`consent_absent`), any other value nothing (`terms_unstamped`), a replay never reaches it (and `termsAccepted` is left out of the replay comparison, so a redelivery across a spoke's update still replays), and the store's per-session guard keeps it to one row; it logs `{ event: "checkout-consent", outcome, siteId }` only and never changes the lead's answer. A `checkout` row is unverified: the payer is not a signed-in user, so it never satisfies the account interstitial. It logs `{ event: "checkout-consent", outcome, eventIdPrefix }` only (`consent_absent`, `terms_unstamped`, `person_unresolved`, `recorded`, `unchanged`, `version_changed` — the site registry has moved past the stamped version, so nothing is written — and `failed`, which adds the error's `errName`) and never changes the 200. A write appends only when the answer changes the reading it is checked against, under an advisory lock on the (subject, purpose) pair. `POST /v1/consent/cookie` takes `{ visitorId, choice, locale }` and never a workspace (it comes from `resolvePublicWorkspace`); `POST /v1/consent/unsubscribe` takes a token signed with `AILK_CONSENT_LINK_SECRET` (≥ 32 bytes, optional; unset ⇒ `503 link_secret_not_configured`) and answers every defect as one `400 invalid_link`. Both are top-level, outside every module gate, in their own rate-limit scopes (`consent`, `unsubscribe`) and on `FORWARDED_VISITOR_ROUTES`. **A sign-up agreement is the account's (#8765 C3, D5, D6):** a `signUp` purpose (`account_terms`) is recorded once per `User`, subject `user` / the user id in the deployment's own site workspace (`resolveSiteWorkspaceId()`, the owner platform consents' footing, read by `userPlatformRows`), never per Person, so the hub and every spoke signed in through it read one row. `GET /v1/consent/sign-up` is anonymous: `{ workspaceId, agreements: [{ purposeId, policyVersion, locale, statement, statementHash }] }` for the site workspace (`locale` default `en`, `null` workspace `503 tenant_unresolved`, `Cache-Control: no-store`), top level beside the cookie route in its own rate-limit scope `signupevidence`, outside every module gate. **Its budget is per site key (#8805):** a request presenting a valid `x-ailk-spoke-key` (`resolveCallerSite` → a registered, unrevoked spoke) is counted against that key's `site,<spokeId>` bucket at `DEFAULT_SIGNUPEVIDENCE_PER_KEY_PER_MINUTE` (120; `AILK_RATE_LIMIT_SIGNUPEVIDENCE_PER_KEY_PER_MINUTE` overrides) and let through the server-wide per-IP ceiling, so a spoofed `X-Forwarded-For` cannot spend it; an invalid or absent key, or a failed lookup, keeps `DEFAULT_PER_IP_PER_MINUTE_STRICT` per IP. The key picks the bucket only — the workspace stays `resolveSiteWorkspaceId()` (`middleware/rate-limit.ts`'s `siteKeyReader`, `SITE_KEYED_ROUTES`). `GET /v1/consent/account` answers `{ agreements: [{ id, statement, policyVersion, acceptedVersion, acceptedAt, current }], complete }` for the caller (`memberCaller` — a cookie session or a hub credential; a personal token `401`); `POST` takes `{ purposeIds, locale? }`, refuses any id that is not a `signUp` agreement `404 purpose_unknown` before writing anything, writes each through `recordPersonConsent` (source `account_settings`, `sourceRef: spoke:<id>` for a hub credential) and answers the GET body; both register in `authScope` unconditionally and log `{ event: "account-agreements", outcome }` only. The member pair reads and writes that same row for a `signUp` purpose (no Person is created) and is unchanged for every other purpose; a `null` site workspace keeps the old per-Person reading; a hub credential's `via: "sign_up"` is `400 sign_up_window_closed` with nothing written (SEC-8765-B). The member pair and the account pair are in `HUB_SESSION_ROUTES` (26 entries since #8765); the anonymous `GET /v1/consent/sign-up` is not, and neither is `/v1/consent/platform` — it is the deployment owner's own consent to Working Theory, a cookie session holding `platform-consents`, under `multi-tenant`. The banner's visitor id travels in `x-ailk-consent-visitor` (only a v4 UUID counts) on the lead and flow routes and the member write, linking the visitor to the Person; the header is never echoed or logged. A `consent` flow field now stores `{ [purposeId]: boolean }` and writes the ledger at completion (source `magnet`, `sourceRef` the flow slug). The operator's `…/consents` answers `{ own, anySource, history, nextCursor }` (history 500 a page, `Cache-Control: private, no-store`, never a subject id). No MCP twin for any of them, recorded in `tests/parity/README.md`.
71
- - `/v1/workspaces/:workspaceId/requests` (GET, `?limit=`), `/v1/workspaces/:workspaceId/people/:personId/pieces/:pieceId/versions` (POST) — the provider routes (#8171, `src/routes/workspaces/{requests,pieces}.ts` over `tenantDb(tenant).memberArea`; spec `docs/specs/8171-provider-routes-the-workspace-requests-spec.md`). **The Requests read** lists the workspace's inbound member rows (#8170's messages, quote requests and reviews), newest first, `limit` 1–50 (default 50, no cursor), behind `inbox-read` (every role) checked with `can(tenant.role, …)`. It is tiered: a role holding `people-read` (owner, admin) gets the full row — the subject, body and the kind's own fields (a message's `link`, a quote's `items`, a review's `pieceId`, `version`, `review`); every other caller, every agent key included, gets `{ id, kind, person, createdAt }` and the content is never selected. A row whose payload fails its kind's schema, or whose Person is gone, is left out and counted in one `warn`. **Deliver a version** numbers version n+1 under the piece lock (`withPieceStagesLock`), behind `member-pieces-manage` through `allows` (an agent granted it delivers); the body is exactly one of an `https` link (`isHttpsLink`, else `400 link_not_https`) and a private attachable asset of this workspace (`400 asset_not_found` / `asset_not_attachable`), plus an optional note; `version`, `deliveredBy` and `deliveredAt` are the server's (a body naming one is `400 validation_failed`); a piece with `approvedAt` set is `409 piece_approved`; a malformed, absent or foreign Person or piece is `404 not_found`. There is no round count, limit or remaining count (#8140 disposition 3). It answers `201 { version: { id, pieceId, version, file, link, note, deliveredAt } }` — the file's download name, never an asset id or `deliveredBy` — and every call that reaches the store writes one `people.version.create` audit row. Same registration, scope and tenant guard as the routes above (`workspace || multi-tenant`). No MCP twin, by decision — member-area MCP tools are a later phase of the CRM nucleus, recorded in `tests/parity/README.md`.
90
+ - `/v1/workspaces/:workspaceId/requests` (GET, `?limit=`), `/v1/workspaces/:workspaceId/requests/:requestId` (GET), `/v1/workspaces/:workspaceId/people/:personId/pieces/:pieceId/versions` (POST) — the provider routes (#8171, `src/routes/workspaces/{requests,pieces}.ts` over `tenantDb(tenant).memberArea`; spec `docs/specs/8171-provider-routes-the-workspace-requests-spec.md`). **The Requests read** lists the workspace's inbound member rows (#8170's messages, quote requests and reviews), newest first, `limit` 1–50 (default 50, no cursor), behind `inbox-read` (every role) checked with `can(tenant.role, …)`. It is tiered: a role holding `people-read` (owner, admin) gets the full row — the subject, body and the kind's own fields (a message's `link`, a quote's `items`, a review's `pieceId`, `version`, `review`); every other caller, every agent key included, gets `{ id, kind, person, createdAt }` and the content is never selected. A row whose payload fails its kind's schema, or whose Person is gone, is left out and counted in one `warn`. **Deliver a version** numbers version n+1 under the piece lock (`withPieceStagesLock`), behind `member-pieces-manage` through `allows` (an agent granted it delivers); the body is exactly one of an `https` link (`isHttpsLink`, else `400 link_not_https`) and a private attachable asset of this workspace (`400 asset_not_found` / `asset_not_attachable`), plus an optional note; `version`, `deliveredBy` and `deliveredAt` are the server's (a body naming one is `400 validation_failed`); a piece with `approvedAt` set is `409 piece_approved`; a malformed, absent or foreign Person or piece is `404 not_found`. There is no round count, limit or remaining count (#8140 disposition 3). It answers `201 { version: { id, pieceId, version, file, link, note, deliveredAt } }` — the file's download name, never an asset id or `deliveredBy` — and every call that reaches the store writes one `people.version.create` audit row. **The single Requests read (#9847, spec `docs/specs/9847-agent-reads-member-request-spec.md`):** `GET /v1/workspaces/:workspaceId/requests/:requestId` answers one inbound member row of the caller's workspace in the list's full shape (`workspaceRequestSchema`, `{ ok: true, data: <row> }`) through `tenantDb(tenant).memberArea.getRequest`. Gate: `inboxAllowed(tenant, "people-read")` before any read — a person whose role holds `people-read`, an agent holding BOTH the `people-read` and `inbox-act` grants (the reply grant, #9714 D5; `member-pieces-manage` is not an alternative); a refusal is `403`. A malformed, absent, outbound, other workspace's or unservable id (payload failing its kind's schema, Person gone — logged `dropped: 1` at `warn`) is one `404 not_found`. The subject, body and each quote answer pass `inbox-messages.ts`'s `stripHiddenText` (zero-width, bidi, BOM and tag characters, SEC-9847-1); the stored row is untouched. Every agent call writes exactly one `people.request.read` audit row — `refused`/`failed` against the workspace, `succeeded` against the interaction with summary `{ kind }` and thread `hub:<personId>` — carrying its session; a person's call writes none. The list's agent view stays the summary row. The thread's `request` entry carries `requestId`, the row's `intx_` id, so an agent can find it. Same registration, scope and tenant guard as the routes above (`workspace || multi-tenant`). No MCP twin, by decision — member-area MCP tools are a later phase of the CRM nucleus, recorded in `tests/parity/README.md`.
72
91
  - `/v1/workspaces/:workspaceId/callbacks` (GET, `?limit=`) — the Inbox callbacks read (#9254, `src/routes/workspaces/callbacks.ts` over `tenantDb(tenant).callbacks`; spec `docs/specs/9254-callback-rows-inbox-spec.md`): the workspace's request-and-confirm `Callback` rows (#5970), newest first, `limit` 1–50 (default 50, the Requests read's own query; any other key `400 validation_failed`), behind `inbox-read` (every role, a member and an agent key included) through `permitted`, checked before any read. Same registration, scope and tenant guard as the Requests read (`workspace || multi-tenant`); the scope is always `request.tenant.workspaceId`. The row is `id`, `name`, `phone`, `preferredTime` (the window asked for, as typed), `personId` (`null` for a callback captured with no email), `confirmedAt`, `confirmedTime`, `paidAt` and `createdAt` — read through the fixed `CALLBACK_ROW_SELECT`, so never `payload`, the confirm digest or expiry, `buyerEmail`, `amountMinor`, `currency` or a Stripe id. The confirm state is the reader's to derive (`paidAt` → paid, else `confirmedAt` → confirmed, else awaiting). No item read, no cursor, no stamp, no audit row; the body is re-parsed against its own `.strict()` schema. The Admin Inbox lists each row under Interested. No MCP twin, recorded in `tests/parity/README.md`.
73
- - `/v1/workspaces/:workspaceId/submissions/:leadId/trial` (POST), `/v1/workspaces/:workspaceId/people/:personId/trial` (GET), `/v1/workspaces/:workspaceId/people/:personId/trial/extend` (POST) — the operator's Start, Get and Extend trial (#9287 C3, `src/routes/workspaces/trial.ts` over `src/services/trial.ts`; spec `docs/specs/9287-operator-started-trials-spec.md` DD2–DD7; Start trial since #9342, spec `docs/specs/9342-start-trial-needs-customers-and-spec.md` DD1–DD7). Same registration, scope and tenant guard as the callbacks read (`workspace || multi-tenant`); the scope is always `request.tenant`. All three need `billing-manage` by role — the owner alone; an admin, a member and every agent key are `403` before any read or Stripe call — and the two POSTs charge a write budget of 30 an hour per person (`chargeWriteBudget` scope `trial`, `429 rate_limited` with `Retry-After`) before their strict body (`bodyLimit` 4 KiB, `400 validation_failed`). The Stripe client is the tenant's own Organization's, `stripeClientForOrganization` only, never the env key (`409 stripe_not_connected`; unusable, or any thrown Stripe error, a fixed `502 stripe_unavailable`, logged by class name only). **Start trial** is a hosted Checkout trial (#9342): it accepts a lead only when it is a `flow:trial` submission of this workspace whose `trial` magnet is a `diagnostic` and names a Person (else one `404 not_found`); the `{ lookupKey }` must be a `plan` `listPlanOfferings` lists (`400 plan_unknown`; a failed read `503 offerings_unavailable`); with no usable `FRONTEND_URL` it is `503 checkout_not_configured` before any Stripe call. It then makes exactly three Stripe calls, all within the Offerings key contract (#8879 §3.3, unchanged): `prices.list` for exactly one active recurring Price under the key (`409 plan_unavailable`); `checkout.sessions.list` of the Person's email's completed sessions, where one tagged `ailk_origin: ailk-trial` is `409 trial_exists` with nothing created; and one session through `lib/stripe.ts`'s `createTrialCheckoutSession` (`@working-theory/stripe-client`) — subscription mode, `trial_period_days: 14`, `payment_method_collection: "if_required"`, `trial_settings.end_behavior.missing_payment_method: "pause"`, the terms box required (`409 terms_not_configured` when the account has no Terms of service URL), `customer_email` the Person's, `subscription_data.metadata` `{ ailk_origin: "ailk-trial", ailk_workspace, ailk_person, site }` with `site` the workspace id (`TRIAL_ORIGIN_TAG`, the tag ailk-billing's trial mint reads), returning to `<FRONTEND_URL origin>/trial`, keyed `trial-checkout:<leadId>:<lookupKey>`. It calls no Customers or Subscriptions endpoint and writes nothing to the Person: Stripe creates the customer and the trialing subscription when the person completes Checkout, and ailk-billing mints from `customer.subscription.created`. A session URL `isStripeCheckoutUrl` refuses is `502 stripe_unavailable` with nothing sent. The person is then emailed the link (`@working-theory/email`'s `sendTrialCheckoutEmail`, from `deliverableSenderAddress()`); a failed send is one `warn` with its kind and still a success. The answer is `201 { checkout: { url, emailed } }` with `Cache-Control: private, no-store`, re-parsed against `startTrialResponseSchema`. **Get trial** lists the customer's ten newest subscriptions (the stored id, or — with none, or one missing on this client — the first customer whose email is the Person's) and serves the newest tagged `ailk-trial` as `{ subscriptionId, status, trialEnd, lookupKey }`, or `null`. **Extend trial** moves that trial's `trial_end` with `proration_behavior: "none"` only while it is `trialing` (`409 trial_not_active`), to a `trialEnd` strictly later than the current one and no later than 90 days after the request (`400 trial_end_invalid`), nothing written on a refusal. **Get and Extend trial need Customers and Subscriptions access, which a key that keeps §3.3 lacks** (#9342 DD7): on such a key Stripe refuses, the route answers `502 stripe_unavailable` and the Person page hides its Trial box; they work on a key that grants that access. Each success writes one `recordAuditAction` row naming the actor and the Person — `people.trial.start` (summary `{ emailed }`, `"true"` or `"false"`) and `people.trial.extend`; a refusal writes none, and Start trial does not mark the lead handled. No log line or audit summary carries the Checkout URL, and no response, log line or audit summary carries an email, a key or a `cus_`/`price_` id (the one `cs_` id served is inside Start trial's URL); Get and Extend's bodies are re-parsed against `trialResponseSchema` (`@working-theory/validation`'s `trial.ts`). No MCP twin — owner-only, recorded in `tests/parity/README.md`.
92
+ - `/v1/workspaces/:workspaceId/submissions/:leadId/trial` (POST), `/v1/workspaces/:workspaceId/people/:personId/trial` (GET), `/v1/workspaces/:workspaceId/people/:personId/trial/extend` (POST) — the operator's Start, Get and Extend trial (#9287 C3, `src/routes/workspaces/trial.ts` over `src/services/trial.ts`; spec `docs/specs/9287-operator-started-trials-spec.md` DD2–DD7; Start trial since #9342, spec `docs/specs/9342-start-trial-needs-customers-and-spec.md` DD1–DD7). Same registration, scope and tenant guard as the callbacks read (`workspace || multi-tenant`); the scope is always `request.tenant`. All three need `billing-manage` by role — the owner alone; an admin, a member and every agent key are `403` before any read or Stripe call — and the two POSTs charge a write budget of 30 an hour per person (`chargeWriteBudget` scope `trial`, `429 rate_limited` with `Retry-After`) before their strict body (`bodyLimit` 4 KiB, `400 validation_failed`). The Stripe client is the tenant's own Organization's, `stripeClientForOrganization` only, never the env key (`409 stripe_not_connected`; unusable, or any thrown Stripe error, a fixed `502 stripe_unavailable`, logged by class name only). **Start trial** is a hosted Checkout trial (#9342): it accepts a lead only when it is a `flow:trial` submission of this workspace whose `trial` magnet is a `diagnostic` and names a Person (else one `404 not_found`); the `{ lookupKey }` must be a `plan` `listPlanOfferings` lists (`400 plan_unknown`; a failed read `503 offerings_unavailable`); with no usable `FRONTEND_URL` it is `503 checkout_not_configured` before any Stripe call. It then makes exactly three Stripe calls, all within the Offerings key contract (#8879 §3.3, unchanged): `prices.list` for exactly one active recurring Price under the key (`409 plan_unavailable`); `checkout.sessions.list` of the Person's email's completed sessions, where one tagged `ailk_origin: ailk-trial` is `409 trial_exists` with nothing created; and one session through `lib/stripe.ts`'s `createTrialCheckoutSession` (`@working-theory/stripe-client`) — subscription mode, `trial_period_days: 14`, `payment_method_collection: "if_required"`, `trial_settings.end_behavior.missing_payment_method: "pause"`, the terms box required (`409 terms_not_configured` when the account has no Terms of service URL), `customer_email` the Person's, `subscription_data.metadata` `{ ailk_origin: "ailk-trial", ailk_workspace, ailk_person, site }` with `site` the workspace id (`TRIAL_ORIGIN_TAG`), returning to `<FRONTEND_URL origin>/trial`, keyed `trial-checkout:<leadId>:<lookupKey>`. It calls no Customers or Subscriptions endpoint and writes nothing to the Person: Stripe creates the customer and the trialing subscription when the person completes Checkout, and the hub's licensing module (`@working-theory/api-hub`'s `src/licensing/`, #9383 DD3) mints from `customer.subscription.created` only for a subscription traced to the session Start trial recorded. A session URL `isStripeCheckoutUrl` refuses is `502 stripe_unavailable` with nothing sent. Before anyone is sent the link the session is recorded (`recordCheckoutSession`, #9383) with the workspace, the Organization, the Price and key it read and the connection's mode; a failed record expires the session on the same client and is `502 stripe_unavailable` with nothing sent, and a `conflict` whose stored record is equal in every field is the idempotent replay and succeeds. The person is then emailed the link (`@working-theory/email`'s `sendTrialCheckoutEmail`, from `deliverableSenderAddress()`); a failed send is one `warn` with its kind and still a success. The answer is `201 { checkout: { url, emailed } }` with `Cache-Control: private, no-store`, re-parsed against `startTrialResponseSchema`. **Get trial** lists the customer's ten newest subscriptions (the stored id, or — with none, or one missing on this client — the first customer whose email is the Person's) and serves the newest tagged `ailk-trial` as `{ subscriptionId, status, trialEnd, lookupKey }`, or `null`. **Extend trial** moves that trial's `trial_end` with `proration_behavior: "none"` only while it is `trialing` (`409 trial_not_active`), to a `trialEnd` strictly later than the current one and no later than 90 days after the request (`400 trial_end_invalid`), nothing written on a refusal. **Get and Extend trial need Customers and Subscriptions access, which a key that keeps §3.3 lacks** (#9342 DD7): on such a key Stripe refuses, the route answers `502 stripe_unavailable` and the Person page hides its Trial box; they work on a key that grants that access. Each success writes one `recordAuditAction` row naming the actor and the Person — `people.trial.start` (summary `{ emailed }`, `"true"` or `"false"`) and `people.trial.extend`; a refusal writes none, and Start trial does not mark the lead handled. No log line or audit summary carries the Checkout URL, and no response, log line or audit summary carries an email, a key or a `cus_`/`price_` id (the one `cs_` id served is inside Start trial's URL); Get and Extend's bodies are re-parsed against `trialResponseSchema` (`@working-theory/validation`'s `trial.ts`). No MCP twin — owner-only, recorded in `tests/parity/README.md`. **The managed-service licence** (#9402, spec `docs/specs/9402-managed-service-trials-deliver-the-spec.md` DD5): `/v1/workspaces/:workspaceId/people/:personId/trial/licence` (GET) and `…/trial/licence/reveal` (POST) sit in the same plugin and, like the trial routes, need `billing-manage` by role (#9841, narrowing #9402 DD5's staff audience) — the owner alone; an admin, a member and every agent key are `403` before any read. They read a managed-service trial's licence through the fulfilment seam's optional `readManagedLicence` (`services/offerings/fulfilment-hooks.ts`, built with `fulfilmentContextFor` from the tenant's own client): the GET answers `{ licence }` — `null`, `{ live: false }` or `{ live: true, tier, expiresAt }`, never a key — and the POST charges its own write budget (scope `licencereveal`, 30 an hour per person), takes a strict empty body, and answers `{ key }` only after one `people.trial.licence_reveal` audit row is written (`503 audit_unavailable` and no key when it is not). `none`/`not_live` on the reveal, and an Organization whose hook has no staff read, are `404 licence_not_found`; a read that cannot be made is `502 licence_unavailable`. Both answer `Cache-Control: private, no-store`; no log line or audit row carries the key. No MCP twin, recorded in `tests/parity/README.md`.
74
93
  - `/v1/workspaces/:workspaceId/members` (GET) — the org's member roster (`userId`, `name`, `email`, `role`), the Pro `workspace` module's Team tab (#5371). Read-only by design: granting a Membership is the invite flow, which is its own spec with a mandatory security review (§2.3), so no POST/PUT/DELETE is routed at this path. Scoped to `request.tenant.organizationId` — Membership is org-level — server-resolved from the validated session. Role floor: `members-read` (owner, admin — `@working-theory/validation`'s permission table (#7636 D2)): the roster carries every member's email, and who the other members are is not a member's job. No MCP twin — identity-bound tenant route (#3591 exception).
75
- - `/v1/workspaces/:workspaceId/agents` (GET `?userId=`, POST), `/v1/workspaces/:workspaceId/agents/:agentId/key` (POST), `/v1/workspaces/:workspaceId/agents/:agentId` (PATCH `{ enabled }`), `PUT /v1/workspaces/:workspaceId/agents/:agentId/grants` (`{ grants }`, #7864), and `/v1/agent/context` (GET) — the agent seat (#3591, `src/routes/workspaces/agents.ts` over `src/lib/agent-store.ts`, `// oss: false`). An agent is a stored delegate of one person, bound to one workspace. The five management routes refuse an agent caller outright (403), check `agents-manage` (owner, admin) before any read, and scope every read and write to `tenant.organizationId`, never a body or query value; an agent of another organization is the same 404 as a missing one. Create returns 201 with the key shown once; re-key returns the new key once. Each successful create, re-key, switch and grant change writes an `agents.*` audit record. **Grants (#7864, `docs/specs/7864-agent-key-grants-spec.md`).** An agent acts at `member`, plus any of seven permissions an owner or admin granted it one by one — `content-edit`, `content-publish`, `assets-write`, `magnets-manage`, `meeting-types-manage`, `people-read` since #852, and `member-pieces-manage` since #7878 (`@working-theory/validation`'s `AGENT_GRANTABLE_PERMISSIONS`, held to that set by the `agents_grants_allowed` CHECK). The grant PUT replaces the whole set (an empty list removes every grant) and refuses a grant the agent's person's own role does not hold, `409 grant_exceeds_person`. The clamp is one function both tenant guards call (`services/default-tenant.ts`'s `agentTenant`): an agent holds a stored grant only while its person's role holds it, so a demoted person's agent keeps the row but not the permission. The routes for the seven check `allows(tenant, "<permission>")`, which reads the grants; every other check keeps `can(tenant.role, …)`, so no other owner or admin permission is reachable. Every agent write on content, assets and meeting types writes one `recordAgentWrite` row naming the agent and its person (magnets already did); a person's writes there stay unaudited. The member-area writes audit every caller through `recordAuditAction`, whose actor names an agent as the agent. No grant raises the ceiling. `/v1/agent/context` is the agent's own read, unchanged by grants, and needs no permission beyond the workspace guard. Same registration scope as the routes above (`workspace || multi-tenant`).
94
+ - `GET /v1/workspaces/:workspaceId/knowledge` — the workspace knowledge read (#9683, spec `docs/specs/9683-agent-session-audit-knowledge-spec.md`; `packages-paid/api-workspace/src/routes/workspaces/knowledge.ts`, `// oss: false`): what the business offers, runs and serves, as `workspaceKnowledgeSchema` — `offerings` (the shown offering set, only while the commerce claim is held), `magnets` (the newest flows page's `live` rows), `meetingTypes` (the enabled slugs minus `privateSlugs`, titled by slug), `sitesServed` (the installed hub's live spokes for this workspace, read only when the hub's own gate answers `claimed`, through the optional `HubRegistry.listLiveSpokesForWorkspace`) and `strategyDocs` (`lib/strategy-docs.ts`, `[]` until a store exists) — every key always present, each entry an id, a title and a summary (`null` when it fails its check; an entry whose id or title fails is skipped and counted). Takes no query. Gate: `allows(people-read) || allows(people-memory-propose)` — a person member holds both; an agent only by grant. Every authenticated call writes one `workspace.knowledge.read` audit row (counts only; an agent's carries its session, thread `workspace:<workspaceId>`); `Cache-Control: private, no-store`. Same registration scope as the routes above (`workspace || multi-tenant`, `tenant:settings`). MCP twin: `get_workspace_knowledge` on `/mcp/workspace`.
95
+ - `/v1/workspaces/:workspaceId/agents` (GET `?userId=`, POST), `/v1/workspaces/:workspaceId/agents/:agentId/key` (POST), `/v1/workspaces/:workspaceId/agents/:agentId` (PATCH `{ enabled }`), `PUT /v1/workspaces/:workspaceId/agents/:agentId/grants` (`{ grants }`, #7864), and `/v1/agent/context` (GET) — the agent seat (#3591, `src/routes/workspaces/agents.ts` over `src/lib/agent-store.ts`, `// oss: false`). An agent is a stored delegate of one person, bound to one workspace. The five management routes refuse an agent caller outright (403), check `agents-manage` (owner, admin) before any read, and scope every read and write to `tenant.organizationId`, never a body or query value; an agent of another organization is the same 404 as a missing one. Create returns 201 with the key shown once; re-key returns the new key once. Each successful create, re-key, switch and grant change writes an `agents.*` audit record. **Grants (#7864, `docs/specs/7864-agent-key-grants-spec.md`).** An agent acts at `member`, plus any of nine permissions an owner or admin granted it one by one — `content-edit`, `content-publish`, `assets-write`, `magnets-manage`, `meeting-types-manage`, `people-read` since #852, `member-pieces-manage` since #7878, and `people-memory-propose` and `inbox-act` since #9679 (`@working-theory/validation`'s `AGENT_GRANTABLE_PERMISSIONS`, held to that set by the `agents_grants_allowed` CHECK). The grant PUT replaces the whole set (an empty list removes every grant) and refuses a grant the agent's person's own role does not hold, `409 grant_exceeds_person`. The clamp is one function both tenant guards call (`services/default-tenant.ts`'s `agentTenant`): an agent holds a stored grant only while its person's role holds it, so a demoted person's agent keeps the row but not the permission. The routes for the nine check `allows(tenant, "<permission>")`, which reads the grants — except that the six existing inbox `inbox-act` checks still read `can(tenant.role, …)` until #9686 moves them, and `people-memory-propose` is read today only by the workspace knowledge read (#9683, beside `people-read`); every other check keeps `can(tenant.role, …)`, so no other owner or admin permission is reachable. Every agent write on content, assets and meeting types writes one `recordAgentWrite` row naming the agent and its person (magnets already did); a person's writes there stay unaudited. The member-area writes audit every caller through `recordAuditAction`, whose actor names an agent as the agent. No grant raises the ceiling. `/v1/agent/context` is the agent's own read, unchanged by grants, and needs no permission beyond the workspace guard. Same registration scope as the routes above (`workspace || multi-tenant`).
76
96
  - `/v1/workspaces/:workspaceId/content` (GET, POST), `/v1/workspaces/:workspaceId/content/:type/:slug` (GET, PATCH), `/v1/workspaces/:workspaceId/content/:type/:slug/{publish,unpublish}` (POST) — the tenant-guarded content editor's write surface (#5853, `src/routes/workspaces/content.ts`): list, create, read/save the draft tip (under `If-Match`), and publish/unpublish, all over `Artifact`/`ArtifactVersion`. `:type` is validated against the page-type registry (`z.enum(SCHEMA_ORG_TYPES)`) rather than accepted as a free string on every addressed operation — an unregistered type is a 400 before any lookup, since it is echoed back and passed to `notifyRevalidate` as the site's cache tag. Role floors from `@working-theory/validation`'s permission table (#7636 D2): publish and unpublish need `content-publish`, and create, read and save need `content-edit` (both owner, admin — an admin builds, a member operates). The list serves `content-edit` the editor's full view and a caller holding only `docs-read` (a member) a published-only projection (D4): draft rows dropped, every row `status: "published"`, `version: null`, `valid: true`, and a `published_with_draft` page's `type`/`tags` read from its PUBLISHED version — applied before every filter, so a member's `?status=draft` or a draft tip's `?tag=` matches nothing. **The GET list is a UNION of page artifacts and DOCUMENT assets** (#6093, D1/D9 — "one store, two views"): every row carries `tags: string[]` (never null, read from the draft tip's stored content for a page, from `Asset.tags` for a file) and a DERIVED `kind: "page" | "post" | "file"` (`BlogPosting` alone maps to `"post"`; an asset row is always `"file"`) — an optional `?tag=<slug>` filters the union AFTER it is built, so a file is returned under either its own tag or a shared one. The asset half of the union (`DOCUMENT_CONTENT_TYPES`, i.e. `application/pdf`) is skipped entirely when `locale`/`type` is supplied or `status` names anything but `"published"`, since a file row can never satisfy those. A foreign `:workspaceId` costs zero artifact **and** zero asset queries. No MCP twin — identity-bound tenant routes (#3591 exception, same shape as the sibling `/v1/workspaces/*` routes).
77
97
  - `/v1/workspaces/:workspaceId/assets` (GET, POST), `/v1/workspaces/:workspaceId/assets/:assetId` (PATCH, DELETE), `/v1/assets/:assetId` (GET, public/anonymous) — the workspace asset store (#5854 upload/read; #6093 list/PATCH/DELETE, `src/routes/assets/{index,upload,read,manage}.ts`). Upload accepts an allowlisted union of three named classes (`src/routes/assets/index.ts`): five RASTER images (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/avif`), one VECTOR image (`image/svg+xml`, admitted under the operator's 2026-09-18 `allow-svg-assets` ruling — issue #6093 comment `5731573595` — superseding #5854's original SVG exclusion) and one DOCUMENT type (`application/pdf`), all under the existing 20 MB cap; `?tags=<slug>[,<slug>…]` stores tags on the row. The public read serves a RASTER type `inline` exactly as before; every OTHER admitted type (SVG, PDF) is served `attachment`, with the filename's extension pinned to the stored content type (closing extension spoofing) and a five-directive `Content-Security-Policy` (`default-src 'none'; sandbox; frame-ancestors 'none'; form-action 'none'; base-uri 'none'`) that makes the bytes never a document on this origin — this is what makes admitting SVG safe with NO server-side sanitizer. `GET …/assets` (`assets-read` — every role, a member included) lists a workspace's assets, metadata only, never `bytes`, with an optional `?tag=<slug>` filter; this is what narrows the store's prior "nothing lists assets" premise to its precise form — no ANONYMOUS and no CROSS-TENANT enumeration, both still true. `PATCH …/assets/:assetId` updates `title`/`description`/`tags` (`assets-write`, owner and admin, the upload's permission too; `null` clears a text field to SQL `NULL`, `""` stores empty; the asset's `id` and public URL are untouched). `DELETE …/assets/:assetId` (`assets-write`) removes the row and the bytes at the ORIGIN only — already-cached copies elsewhere are not recallable under the year-long `immutable` promise, and no reference check protects a page that still embeds the URL; absent and foreign ids both take the same 404. Every tenant route here is identity-bound; no MCP twin (#3591 exception). The public read has no MCP twin either — it serves bytes to an `<img>`/download, the same boundary `upload.ts` records for its own sibling. **A client's file is never in the library's public life (#7877 D18):** the default `GET …/assets` listing leaves out every asset tagged `private` — only `?tag=private` lists them (the Admin's Compose) — and the public read answers its uniform 404 for an asset tagged `private`, attached to any member-area message, or (#8171 D3) named by a delivered piece version (one `pieceVersion.count`, since the tag can be removed after delivery), so such a file's bytes are served only by its owner's member reads.
78
98
  - `/v1/subscription/me` (GET) — `?include=billing` (#8171 D7), from a cookie session only, adds the optional `planName` and `paymentMethod` (brand, last four, expiry) read from Stripe through `lib/stripe.ts`'s `readCustomerBillingSummary` for the caller's own resolved customer: cached per customer for 60 s (a failure too), one read in flight per customer, bounded at 3 s; a failure leaves both keys absent, a value outside the schema's bounds is `null`, and no id is served or logged. A bearer token, another value or none answers exactly as before.
@@ -85,7 +105,7 @@ Routes are versioned under `/v1/*`:
85
105
  - `/v1/schedule/meetings` (GET) — the meetings read (#8843 D1–D3, `src/routes/schedule/index.ts`; spec `docs/specs/8843-the-meet-index-and-meeting-spec.md`): the caller's workspace's Live meetings with every fact a booking page shows, so no title, length, description or price is copied into content. Anonymous, in the same `scheduling`-gated schedule scope and strict per-IP limit as its siblings. It takes no argument (a query parameter is `400 invalid_input`, as its twin refuses one): the workspace comes from `resolvePublicWorkspace()`, the provider from `resolvePublicSchedulingProvider`, a refused spoke key, no provider or an unresolved workspace answering `503 not_configured`. The Live set is the stored row's `enabledSlugs` that pass `MeetingSlugSchema`; with no row a spoke's is empty (and no provider call is made), and a keyless caller's is every event type the account lists. Rows: each listed event type whose slug is Live, in the provider's order, `bookable: true` with `calLink` (`<account handle>/<slug>`), `title`, `durationMinutes` and the optional `description` and `price`; then each Live slug the provider does not list, `bookable: false` (a `null` handle makes every row non-bookable); each with `visibility` (`private` when the row's `privateSlugs` holds it, else `public`) and `position`, at most 20 rows. These rules live in `src/services/scheduling/meetings.ts`'s `listWorkspaceMeetings` (#9129), which the route calls with the account handle read; `GET /v1/member/meeting-types` calls the same function's `customers` list without it, so the member read makes no `/v2/me` call. **`privateSlugs` (#8843 D2)** is a fourth optional list on the meetings blob — what a site lists on `/private` rather than `/meet`; the meetings PUT keeps a stored list a body leaves out, as it keeps `customerSlugs`, and `meeting-types` ignores it. `enabledSlugs` keeps its whole-list meaning (a body without it parses to `[]`), so a writer of `privateSlugs` sends the `enabledSlugs` it read. **Private means unlisted, not access-controlled:** the read and its twin serve private rows to any caller, and `availability`, `event-meta` and `book` accept a private Live slug like a public one. The body is re-parsed against `MeetingListSchema` before it is sent (a failure is `502 provider_error`); a provider failure logs `{ route: "meetings", kind, detail }`, and nothing is logged on success. MCP twin: `list_meetings` (D5).
86
106
  - `POST /v1/webhooks/calcom` — the scheduling booking-event receiver (#6085, brief H7): verifies the provider's HMAC-SHA256 signature (`x-cal-signature-256`) over the raw body, then normalizes one of six `BOOKING_*` trigger events into an `Appointment` row (`src/services/scheduling/appointments.ts`). `CAL_WEBHOOK_SECRET` is read server-side only (`config/index.ts`); an unset secret answers `503`, never a silent accept. Registered on the same `webhooksScope` as `/v1/webhooks/stripe` (its raw-body content-type parser is what a signed receiver needs), module-conditional (`scheduling`, `src/routes/webhooks/index.ts`). No commerce-claim read: a booking event is not a money event (D-67 puts the payment on the provider's own side). No MCP twin — an inbound receiver for one named provider, not an endpoint an agent could reasonably call, the same footing `POST /v1/webhooks/stripe` already takes.
87
107
  - `POST /v1/webhooks/calcom/:connectionToken` — the same receiver for ONE workspace's own connection (#7533 D6, `calcomConnectionWebhookHandler`). The 43-character token in the path names one `SchedulingConnection` row, whose own decrypted secret verifies `x-cal-signature-256` and whose `workspaceId` the Appointment is written to — nothing in the body, query or headers reaches the workspace. A malformed or unknown token, or another provider's connection, is `404 not-found` (logged nothing); a secret that will not decrypt is `503 webhook-not-configured`; the rest of the ladder is the legacy route's, by the same code. Same module gate and `loadSlice` call as the legacy route, its own rate-limit bucket. The legacy `POST /v1/webhooks/calcom` is unchanged: the env account's receiver, writing into the `AILK_SITE_ID` workspace (SEC-7533-2, accepted). No MCP twin — the same inbound-receiver footing as the legacy route.
88
- - `POST /v1/webhooks/stripe/:connectionToken` — the per-Organization Stripe receiver (#8932, `src/routes/webhooks/stripe-offerings.ts`; spec `docs/specs/8932-the-per-organization-stripe-webhook-spec.md`; contract W1, W3–W11, G5, T8). The 43-character token names one `StripeConnection` row (`findStripeConnectionByWebhookToken`, #8907), whose own decrypted secret verifies the signature through `verifyWebhookSignatureKeyless` (`@working-theory/stripe-client`, no env key) and whose `organizationId` is the only Organization the event can act for — nothing in the body, query or headers changes it. It acts only on `checkout.session.completed` / `.async_payment_succeeded` for a session the hub's own checkout recorded (`readCheckoutSessionRecord`, #8930) for that Organization, whose signed `metadata.site` is the record's workspace, whose mode is the row's, and whose one line item — re-read with the connection's own client, never the env key — has the record's Price id; then it writes ONE lead through `routeLead` (key `"stripe-offerings " + sessionId`, a space no `Idempotency-Key` header can carry; `offeringsCheckout: true`; offering the record's full lookup key), marked `spokeCheckout` for a live session so #8793's release holds the plan, unmarked for a test one. A malformed or unknown token is `404 not-found` and logs nothing; a store failure `503 webhook-unavailable`; no usable secret `503 webhook-not-configured`; the claim, signature and livemode steps are the legacy route's; an unreadable record, Stripe read or lead write is a non-2xx that is never marked seen, so Stripe retries. Every `200` body is `{ received: true }` (the outcome is the log line's: `unrecorded`, `foreign_organization`, `offering_mismatch`, `fulfilled`, …). Dedup is its own per-Organization store (`hasSeenForOrganization`); the legacy `POST /v1/webhooks/stripe` acks any `ailk_offering`-stamped event `offerings_path` before `markSeen` and writes nothing. Log lines carry `organizationId`, `eventType`, an 8-character event-id prefix and the outcome only, and the request and error lines log the path with the token replaced whole (`lib/error-serializer.ts`'s `redactLogPath`, which also covers the calcom token route). No billing identity is bound from a connection's event (W11). Its own 100/min per-IP bucket. No MCP twin — the same inbound-receiver footing as the legacy route.
108
+ - `POST /v1/webhooks/stripe/:connectionToken` — the per-Organization Stripe receiver (#8932, `src/routes/webhooks/stripe-offerings.ts`; spec `docs/specs/8932-the-per-organization-stripe-webhook-spec.md`; contract W1, W3–W11, G5, T8). The 43-character token names one `StripeConnection` row (`findStripeConnectionByWebhookToken`, #8907), whose own decrypted secret verifies the signature through `verifyWebhookSignatureKeyless` (`@working-theory/stripe-client`, no env key) and whose `organizationId` is the only Organization the event can act for — nothing in the body, query or headers changes it. It acts only on `checkout.session.completed` / `.async_payment_succeeded` for a session the hub's own checkout recorded (`readCheckoutSessionRecord`, #8930) for that Organization, whose signed `metadata.site` is the record's workspace, whose mode is the row's, and whose one line item — re-read with the connection's own client, never the env key — has the record's Price id; then it writes ONE lead through `routeLead` (key `"stripe-offerings " + sessionId`, a space no `Idempotency-Key` header can carry; `offeringsCheckout: true`; offering the record's full lookup key), marked `spokeCheckout` for a live session so #8793's release holds the plan, unmarked for a test one. A malformed or unknown token is `404 not-found` and logs nothing; a store failure `503 webhook-unavailable`; no usable secret `503 webhook-not-configured`; the claim, signature and livemode steps are the legacy route's; an unreadable record, Stripe read or lead write is a non-2xx that is never marked seen, so Stripe retries. Every `200` body is `{ received: true }` (the outcome is the log line's: `unrecorded`, `foreign_organization`, `offering_mismatch`, `fulfilled`, …). Dedup is its own per-Organization store (`hasSeenForOrganization`); the legacy `POST /v1/webhooks/stripe` acks any `ailk_offering`-stamped event `offerings_path` before `markSeen` and writes nothing. Log lines carry `organizationId`, `eventType`, an 8-character event-id prefix and the outcome only, and the request and error lines log the path with the token replaced whole (`lib/error-serializer.ts`'s `redactLogPath`, which also covers the calcom token route). No billing identity is bound from a connection's event (W11). **The fulfilment seam (#9383 DD1, `src/services/offerings/fulfilment-hooks.ts`):** for an Organization a paid package registered a hook for (the hub's licensing module, for the one Organization it is configured to mint for), `onFulfilledSession` runs after `fulfilled` / `already_fulfilled` and the event is marked seen only once it answers without `retry`; the five licence types (`customer.subscription.{created,updated,deleted}`, `invoice.paid`, `charge.refunded`) are deduplicated after the livemode step and handed to `onLifecycleEvent` in place of `ignored_type`; a `retry` answer, an unusable connection or a throwing hook is `503 licence-unavailable`, never seen. Every other Organization answers as before. A recorded session tagged `ailk_origin: ailk-trial` writes no lead (`trial_recorded`). Its own 100/min per-IP bucket. No MCP twin — the same inbound-receiver footing as the legacy route.
89
109
  - `/v1/schedule/appointments` (GET, `?limit=&cursor=&window=`) — the SITE's own appointments (#6085), tenant-guarded and role-floored at `appointments-read` (`@working-theory/validation`'s permission table (#7636 D2) — every role, a member included; the rows carry attendee contact PII, which the operator opened to the member who works the inbox). Registered in its OWN tenant scope, gated on `isModuleEnabled("scheduling")` alone, behind `tenantPreHandlerForBuild()` — NOT the anonymous schedule scope above (a prospect booking a call has no account). Scope is ALWAYS `request.tenant.workspaceId`, NEVER `currentSiteId()` (the receiver's own write scope) — and deliberately carries NO platform-operator escape hatch, unlike `/v1/waitlist-experiments/*`: an appointment book is a seller's customer list. `window` defaults to `upcoming`; `cursor` is the previous page's last row id, and one that does not resolve within the caller's scope is a `400`, never a silent page 1 (the `/v1/waitlist-experiments/*/signups` cursor discipline). Nine fields per row and nothing more — `id`, `startTime`, `endTime`, `title`, `attendeeName`, `attendeeEmail`, `status`, `paid`, `personId` (#9348 — the Person the booking was stored against when it was created, or `null`; a later provider event that rewrites the attendee does not move it); `siteId`, `providerBookingUid`, `provider`, the three watermark columns, `paymentInitiatedAt`, `noShow`, `cancelledAt`, `createdAt` and `updatedAt` are never selected (`src/services/scheduling/appointments.ts`'s `listAppointments` is the only read path). No MCP twin — identity-bound tenant route (#3591 exception, the same shape as `/v1/waitlist-experiments/*`).
90
110
  - `/v1/aeo/score` (POST), `/v1/aeo/score/example` (GET) — anonymous instant AEO score for a respondent-supplied URL, and the bundled pre-launch example, both fed by the SSRF-guarded crawler at `@working-theory/aeo/fetch` and the pure scorer at `@working-theory/aeo` (`src/services/aeo-score.ts`, #4666). No MCP twin — recorded exception, brief D7 (exposing an arbitrary-URL crawler to anonymous agents is a security-posture decision; `apps/mcp/src/tools/audit.ts` deliberately stays same-origin-locked instead).
91
111
  - `/v1/waitlist-experiments` (GET), `/v1/waitlist-experiments/:experimentId` (GET), `/v1/waitlist-experiments/:experimentId/signups` (GET), `/v1/waitlist-experiments/:experimentId/signups/:leadId` (GET) — the operator-dashboard read API for Parallel Waitlist Validation (#4665 D7, B0): tenant-scoped list + rollup, per-waitlist comparison, paginated signups, and a signup drill-down over A1's `WaitlistExperiment` / `WaitlistScore` / `Lead` rows (`src/services/waitlist-dashboard.ts`, #4671). Role floor `waitlist-read` (`@working-theory/validation`'s permission table (#7636 D2) — every role, a member included; signup PII); the platform-operator scope (`isSentinel(organizationId)` and the `platform-read-all-sites` permission, owner only, ALSO requiring the caller's VERIFIED email to equal `AILK_OPERATOR_EMAIL` — fail-closed, PR #4687, compared to the caller since #7523) sees every site, every other caller sees only its own `siteId`. No MCP twin — identity-bound tenant routes (#3591 exception, same shape as `/v1/billing/*`).
@@ -132,8 +152,18 @@ These own every billing-ledger mutation — don't write `UsageEvent`/`UsagePerio
132
152
 
133
153
  Stateless, idempotent bins an external scheduler runs (Vercel Cron / GitHub
134
154
  Actions cron / plain crontab) — this repo has no queue or in-process timer
135
- (D1). Dev: `pnpm --filter @working-theory/api <script>`. Production:
136
- `node dist/bin/<name>.js`, on whatever cadence the deployment chooses;
155
+ (D1). Dev: `pnpm --filter @working-theory/api-<module> <script>` for a module
156
+ bin (#9565 D2 — eight are `@working-theory/api-signal-collectors`'s,
157
+ `member-request:match` is `@working-theory/api-workspace`'s; each package's
158
+ `src/bin/` holds its source and its `package.json` the script) and
159
+ `pnpm --filter @working-theory/api <script>` for a base bin (`abandon:sweep`,
160
+ `rate-limit:probe`, `spokes`, `stripe-connections:rekey` — their source is
161
+ `packages/api/src/bin/`, and the app's script of the same name runs the
162
+ installed package's compiled bin, so the one command works in the monorepo
163
+ and in a scaffold, #9564 D9). Production: `node
164
+ packages-paid/api-<module>/dist/bin/<name>.js` for a module bin (each package
165
+ builds `src/` to `dist/`) and `node node_modules/@working-theory/api/dist/bin/<name>.js`
166
+ for a base bin, on whatever cadence the deployment chooses;
137
167
  a SEQUENTIAL re-run is always a no-op for already-processed rows, via each
138
168
  sweep's own DB constraint (never a caller-side lock).
139
169
 
@@ -189,59 +219,65 @@ sweep's own DB constraint (never a caller-side lock).
189
219
  - Don't put session state in `apps/api`. It's stateless.
190
220
  - Don't bundle `apps/api` (esbuild, rollup, webpack). Slice specifiers cannot survive it — see `## Deploy path`.
191
221
 
192
- Last updated: 2026-10-20 — Inbox Reply records the message (#9348): the submissions bullet names `personId` on the item read and PATCH, and the appointments bullet names the ninth field — verified against `apps/api/src/services/{inbox-submissions,scheduling/appointments}.ts`, `apps/api/src/routes/workspaces/submissions.ts`, `apps/api/src/lib/tenant-db.ts` and `packages/validation/src/schemas/{inbox,scheduling}.ts`.
222
+ Last updated: 2026-10-24 — #9749: the prospect-channel settings routes (`GET`/`PUT /v1/workspaces/:workspaceId/prospect-channel`) and the twins of them and of the Slack status read — verified against `packages-paid/api-scheduling/src/routes/workspaces/meetings.ts`, `packages-paid/api-workspace/src/routes/workspaces/slack-connection.ts`, `packages/api/src/lib/tenant-db.ts` and `tests/parity/api-parity/kinds/site.ts`.
223
+
224
+ Prior: 2026-10-24 — #9683: the workspace knowledge read (`GET /v1/workspaces/:workspaceId/knowledge`) and its twin, and agent-session audit rows on the four Inbox message routes and the member-area messages pair — verified against `packages-paid/api-workspace/src/routes/workspaces/{knowledge,messages,messages-out}.ts`, `packages-paid/api-workspace/src/lib/agent-session.ts` and `packages/api/src/lib/hub.ts`.
225
+
226
+ Prior: 2026-10-24 — the hub mints AILK licences (#9383 U1): the per-Organization webhook bullet names the fulfilment seam and `trial_recorded` (DD1), the checkout bullet's hub-plan refusal keys on `mintsLicences` (DD5), and the trial bullet names the recorded session and the hub's licensing module as the minter (DD3) — verified against `packages/api/src/routes/webhooks/stripe-offerings.ts`, `packages/api/src/services/offerings/{fulfilment-hooks,checkout-authorization}.ts` and `packages-paid/api-workspace/src/services/trial.ts`.
227
+
228
+ Prior: 2026-10-20 — Inbox Reply records the message (#9348): the submissions bullet names `personId` on the item read and PATCH, and the appointments bullet names the ninth field — verified against `apps/api/src/services/{inbox-submissions,scheduling/appointments}.ts`, `packages-paid/api-workspace/src/routes/workspaces/submissions.ts`, `packages/api/src/lib/tenant-db.ts` and `packages/validation/src/schemas/{inbox,scheduling}.ts`.
193
229
 
194
230
  Prior: 2026-10-20 — the Inbox message row serves its body and Person (#9349): the messages bullet names the nine-key row, `readMessageBody` and the strict re-parse; the People bullet names `stripeCustomerUrl` and the `billing-read` withholding — verified against `apps/api/src/routes/workspaces/{messages,people}.ts`, `apps/api/src/services/{inbox-messages,person}.ts` and `packages/validation/src/schemas/{inbox,people}.ts`.
195
231
 
196
- Prior: 2026-10-20 — the hub's plans become Offerings plans (#9338): the plan-checkout sentence names `ailk-pro_monthly` / `ailk-pro-plus_monthly` and the plan-only `refuseProduct` (DD4), and the checkout bullet adds the Offerings path's hub-plan refusal beside A9 (DD5) — verified against `apps/api/src/routes/billing/plan-checkout.ts`, `apps/api/src/services/offerings/{catalog,checkout-authorization}.ts` and `packages/validation/src/schemas/{billing,offerings}.ts`.
232
+ Prior: 2026-10-20 — the hub's plans become Offerings plans (#9338): the plan-checkout sentence names `ailk-pro_monthly` / `ailk-pro-plus_monthly` and the plan-only `refuseProduct` (DD4), and the checkout bullet adds the Offerings path's hub-plan refusal beside A9 (DD5) — verified against `packages-paid/api-multi-tenant/src/routes/billing/plan-checkout.ts`, `apps/api/src/services/offerings/{catalog,checkout-authorization}.ts` and `packages/validation/src/schemas/{billing,offerings}.ts`.
197
233
 
198
- Prior: 2026-10-20 — Start trial becomes a hosted Checkout trial (#9342): the trial bullet names the three Stripe calls, the session's parameters, the completed-session guard, the email, the `{ checkout }` answer, the two new refusals and the `{ emailed }` audit summary, and Get and Extend's §3.3 limit — verified against `apps/api/src/services/trial.ts`, `apps/api/src/routes/workspaces/trial.ts`, `packages/stripe-client/src/checkout.ts` and `packages/validation/src/schemas/trial.ts`.
234
+ Prior: 2026-10-20 — Start trial becomes a hosted Checkout trial (#9342): the trial bullet names the three Stripe calls, the session's parameters, the completed-session guard, the email, the `{ checkout }` answer, the two new refusals and the `{ emailed }` audit summary, and Get and Extend's §3.3 limit — verified against `packages-paid/api-workspace/src/services/trial.ts`, `packages-paid/api-workspace/src/routes/workspaces/trial.ts`, `packages/stripe-client/src/checkout.ts` and `packages/validation/src/schemas/trial.ts`.
199
235
 
200
- Prior: 2026-10-20 — the lead writes become parity twins (#7171, #7172, #7173): the `/v1/leads` bullet names the three registrations, their body exports, the body gate and the unchanged raw-body persistence — verified against `apps/api/src/routes/leads/index.ts`, `packages/validation/src/schemas/leads.ts` and `tests/parity/api-parity/kinds/site.ts`.
236
+ Prior: 2026-10-20 — the lead writes become parity twins (#7171, #7172, #7173): the `/v1/leads` bullet names the three registrations, their body exports, the body gate and the unchanged raw-body persistence — verified against `packages/api/src/routes/leads/index.ts`, `packages/validation/src/schemas/leads.ts` and `tests/parity/api-parity/kinds/site.ts`.
201
237
 
202
238
  Prior: 2026-10-20 — the content reads become parity twins (#7167, #7168, #7169): the public content reads bullet names the three routes as `search_content`'s, `list_pages`' and `read_content`'s twins through the validation exports and the shared builders — verified against `apps/api/src/routes/content/{search,list-pages,get-page}.ts`, `packages/content-adapters/src/content-reads.ts` and `tests/parity/api-parity/kinds/site.ts`.
203
239
 
204
- Prior: 2026-10-18 — the Inbox orders read (#9170): added the `/v1/workspaces/:workspaceId/orders` bullet and named the order exclusion in the messages bullet — verified against `apps/api/src/routes/workspaces/orders.ts`, `apps/api/src/services/inbox-orders.ts`, `apps/api/src/lib/tenant-db.ts` and `apps/api/src/server.ts`.
240
+ Prior: 2026-10-18 — the Inbox orders read (#9170): added the `/v1/workspaces/:workspaceId/orders` bullet and named the order exclusion in the messages bullet — verified against `packages-paid/api-workspace/src/routes/workspaces/orders.ts`, `packages-paid/api-workspace/src/services/inbox-orders.ts`, `packages/api/src/lib/tenant-db.ts` and `packages/api/src/server.ts`.
205
241
 
206
242
  Prior: 2026-10-18 — the env path is the hub's own-products path only, never a spoke's (#9142): the `/v1/checkout/sessions` bullet and § BYO Stripe say a spoke sells through its Offerings buy route — verified against `src/routes/checkout/sessions.ts` and `apps/web/app/api/offerings/checkout/[lookupKey]/route.ts`.
207
243
 
208
- Prior: 2026-10-18 — one keyless-workspace predicate (#9145): the flows bullet names how `resolveCallerWorkspace` resolves a keyless call (the one-workspace predicate, not the alias) and the open deliverable bullet follows it — verified against `apps/api/src/services/public-site-resolver.ts` and `apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts`.
244
+ Prior: 2026-10-18 — one keyless-workspace predicate (#9145): the flows bullet names how `resolveCallerWorkspace` resolves a keyless call (the one-workspace predicate, not the alias) and the open deliverable bullet follows it — verified against `packages/api/src/services/public-site-resolver.ts` and `packages-paid/api-hub/src/services/__tests__/keyless-workspace-predicate.test.ts`.
209
245
 
210
- Prior: 2026-10-18 — the meetings-list assembly moves to the service tier (#9129): the meetings-read bullet names `listWorkspaceMeetings` and the member read's handle-less call — verified against `apps/api/src/services/scheduling/meetings.ts`, `apps/api/src/routes/schedule/index.ts` and `apps/api/src/routes/member/meeting-types.ts`.
246
+ Prior: 2026-10-18 — the meetings-list assembly moves to the service tier (#9129): the meetings-read bullet names `listWorkspaceMeetings` and the member read's handle-less call — verified against `packages-paid/api-scheduling/src/services/scheduling/meetings.ts`, `packages-paid/api-scheduling/src/routes/schedule/index.ts` and `packages-paid/api-scheduling/src/routes/member/meeting-types.ts`.
211
247
 
212
- Prior: 2026-10-18 — the Admin's Private switch (#9128): the workspace meetings bullet names `include` as a comma list and the manage-tier-only `private` row field — verified against `apps/api/src/routes/workspaces/meetings.ts` and `packages/site-config/src/schema/meetings-schema.ts`.
248
+ Prior: 2026-10-18 — the Admin's Private switch (#9128): the workspace meetings bullet names `include` as a comma list and the manage-tier-only `private` row field — verified against `packages-paid/api-scheduling/src/routes/workspaces/meetings.ts` and `packages/site-config/src/schema/meetings-schema.ts`.
213
249
 
214
- Prior: 2026-10-18 — a job application carries terms acceptance (#9130): the leads bullet names the consent-source set, `isConsentLeadSource`, and the replay comparison's exemption — verified against `apps/api/src/routes/leads/index.ts`, `apps/api/src/services/lead-routing.ts` and `packages/validation/src/schemas/leads.ts`.
250
+ Prior: 2026-10-18 — a job application carries terms acceptance (#9130): the leads bullet names the consent-source set, `isConsentLeadSource`, and the replay comparison's exemption — verified against `packages/api/src/routes/leads/index.ts`, `packages/api/src/services/lead-routing.ts` and `packages/validation/src/schemas/leads.ts`.
215
251
 
216
- Prior: 2026-10-18 — the standard recommender (#9071): the recommender bullet names the `offerings_offer_refused` refusal of an offer carrying `lookupKey`; the magnets bullet names `newsletter` and `recommender` in `slug_fixed`/`slug_reserved` — verified against `apps/api/src/routes/flows/recommender.ts`, `apps/api/src/routes/magnets/index.ts` and `packages/validation/src/schemas/flow.ts`.
252
+ Prior: 2026-10-18 — the standard recommender (#9071): the recommender bullet names the `offerings_offer_refused` refusal of an offer carrying `lookupKey`; the magnets bullet names `newsletter` and `recommender` in `slug_fixed`/`slug_reserved` — verified against `packages-paid/api-commerce/src/routes/flows/recommender.ts`, `packages-paid/api-signal-collectors/src/routes/magnets/index.ts` and `packages/validation/src/schemas/flow.ts`.
217
253
 
218
- Prior: 2026-10-18 — the gated-deliverable shape (#8848): the flows bullet names the `deliverable` block, its served projection and the completion key; added the `/v1/deliverables/open/:slug` bullet — verified against `apps/api/src/services/{flow-engine,deliverable-fulfillment}.ts`, `apps/api/src/routes/{flows,deliverables}/index.ts`, `apps/api/src/middleware/rate-limit.ts` and `apps/api/src/server.ts`.
254
+ Prior: 2026-10-18 — the gated-deliverable shape (#8848): the flows bullet names the `deliverable` block, its served projection and the completion key; added the `/v1/deliverables/open/:slug` bullet — verified against `apps/api/src/services/{flow-engine,deliverable-fulfillment}.ts`, `apps/api/src/routes/{flows,deliverables}/index.ts`, `packages/api/src/middleware/rate-limit.ts` and `packages/api/src/server.ts`.
219
255
 
220
- Prior: 2026-10-18 — the Billing tab's plan card (#9067): the `/v1/billing` bullet names `?include=billing`, `details`, its `billing-manage` gate and cache, `POST /v1/billing/plan-checkout` with its check order, and the webhook's proof-based Organization binding — verified against `apps/api/src/routes/billing/{read,plan-checkout,index}.ts`, `apps/api/src/services/entitlements.ts`, `apps/api/src/lib/stripe.ts` and `apps/api/src/routes/webhooks/stripe.ts`.
256
+ Prior: 2026-10-18 — the Billing tab's plan card (#9067): the `/v1/billing` bullet names `?include=billing`, `details`, its `billing-manage` gate and cache, `POST /v1/billing/plan-checkout` with its check order, and the webhook's proof-based Organization binding — verified against `apps/api/src/routes/billing/{read,plan-checkout,index}.ts`, `packages/api/src/services/entitlements.ts`, `packages/api/src/lib/stripe.ts` and `packages/api/src/routes/webhooks/stripe.ts`.
221
257
 
222
- Prior: 2026-10-18 — the workspace shown set per Stripe Price (#9070): the `/v1/workspaces/:workspaceId/offerings` bullet names the one-Price body, the `shown` answer, `price_not_offered`, the Recommended kept or cleared and never set, the Price-counted audit and `shownPriceIdsFor`; the `/v1/checkout/sessions` bullet adds the shown-Price check — verified against `apps/api/src/routes/workspaces/offerings.ts`, `apps/api/src/services/offerings/{index,store,checkout-authorization}.ts` and `packages/validation/src/schemas/offerings.ts`.
258
+ Prior: 2026-10-18 — the workspace shown set per Stripe Price (#9070): the `/v1/workspaces/:workspaceId/offerings` bullet names the one-Price body, the `shown` answer, `price_not_offered`, the Recommended kept or cleared and never set, the Price-counted audit and `shownPriceIdsFor`; the `/v1/checkout/sessions` bullet adds the shown-Price check — verified against `packages-paid/api-multi-tenant/src/routes/workspaces/offerings.ts`, `apps/api/src/services/offerings/{index,store,checkout-authorization}.ts` and `packages/validation/src/schemas/offerings.ts`.
223
259
 
224
- Prior: 2026-10-18 — the in-page form shape (#8847): the magnets bullet names the Newsletter seed, its migration and the `newsletter` slug in `slug_fixed`/`slug_reserved`; the definition bullet adds `unsubscribe` to the seedable keys and the reserved slugs — verified against `apps/api/src/lib/tenant-db.ts`, `apps/api/src/routes/{workspaces,magnets}/index.ts`, `apps/api/src/services/flow-engine.ts` and `database/migrations/20261022120000_in_page_magnets/migration.sql`.
260
+ Prior: 2026-10-18 — the in-page form shape (#8847): the magnets bullet names the Newsletter seed, its migration and the `newsletter` slug in `slug_fixed`/`slug_reserved`; the definition bullet adds `unsubscribe` to the seedable keys and the reserved slugs — verified against `packages/api/src/lib/tenant-db.ts`, `apps/api/src/routes/{workspaces,magnets}/index.ts`, `packages-paid/api-signal-collectors/src/services/flow-engine.ts` and `database/migrations/20261022120000_in_page_magnets/migration.sql`.
225
261
 
226
- Prior: 2026-10-18 — the booking pages (#8843 PR B): the meeting-types bullet no longer names a web-side env fallback, since the pages read `/v1/schedule/meetings` — verified against `apps/web/app/[locale]/meet/` and `apps/api/src/routes/schedule/meeting-types.ts`.
262
+ Prior: 2026-10-18 — the booking pages (#8843 PR B): the meeting-types bullet no longer names a web-side env fallback, since the pages read `/v1/schedule/meetings` — verified against `apps/web/app/[locale]/meet/` and `packages-paid/api-scheduling/src/routes/schedule/meeting-types.ts`.
227
263
 
228
- Prior: 2026-10-18 — a default full-screen magnet (#8840): the Job application bullet names the two-step seed, the apply walks, `payload.name` and the hidden value's slug rule; the definition bullet adds `ending` and `landing` to the seedable keys — verified against `apps/api/src/services/flow-engine.ts` and `packages/validation/src/schemas/flow.ts`.
264
+ Prior: 2026-10-18 — a default full-screen magnet (#8840): the Job application bullet names the two-step seed, the apply walks, `payload.name` and the hidden value's slug rule; the definition bullet adds `ending` and `landing` to the seedable keys — verified against `packages-paid/api-signal-collectors/src/services/flow-engine.ts` and `packages/validation/src/schemas/flow.ts`.
229
265
 
230
- Prior: 2026-10-18 — the Offerings checkout authorization (#8931): the `/v1/checkout/sessions` bullet names the two paths, the `404`/`502`/`503`/`429` rules, the stamped and recorded session, A9's env-path refusal and A13's session read; BYO Stripe names the env key's Products: Read need — verified against `apps/api/src/routes/checkout/{sessions,session}.ts`, `apps/api/src/services/offerings/checkout-authorization.ts` and `packages/stripe-client/src/checkout.ts`.
266
+ Prior: 2026-10-18 — the Offerings checkout authorization (#8931): the `/v1/checkout/sessions` bullet names the two paths, the `404`/`502`/`503`/`429` rules, the stamped and recorded session, A9's env-path refusal and A13's session read; BYO Stripe names the env key's Products: Read need — verified against `apps/api/src/routes/checkout/{sessions,session}.ts`, `packages/api/src/services/offerings/checkout-authorization.ts` and `packages/stripe-client/src/checkout.ts`.
231
267
 
232
- Prior: 2026-10-18 — the agent pricing tools read the Offerings shown set (#8933): the public content reads bullet names the twins' shown-set branch, `readShownOfferings`, the shared mapping, the shared route options and the content fallback — verified against `apps/api/src/routes/content/{pricing,offerings,shown-offerings}.ts`, `apps/api/src/server.ts` and `packages/content-adapters/src/shown-offerings.ts`. Also 2026-10-18 — the workspace Offerings routes (#8926): added the `/v1/workspaces/:workspaceId/offerings` bullet (the availability answers, the enabled catalog with the shown set and Recommended, the PUT's check order, the write budget, the selection rules, the store writer, the audit row and the forced refresh) — verified against `apps/api/src/routes/workspaces/offerings.ts`, `apps/api/src/services/offerings/store.ts`, `apps/api/src/server.ts` and `architecture.yaml`.
268
+ Prior: 2026-10-18 — the agent pricing tools read the Offerings shown set (#8933): the public content reads bullet names the twins' shown-set branch, `readShownOfferings`, the shared mapping, the shared route options and the content fallback — verified against `apps/api/src/routes/content/{pricing,offerings,shown-offerings}.ts`, `packages/api/src/server.ts` and `packages/content-adapters/src/shown-offerings.ts`. Also 2026-10-18 — the workspace Offerings routes (#8926): added the `/v1/workspaces/:workspaceId/offerings` bullet (the availability answers, the enabled catalog with the shown set and Recommended, the PUT's check order, the write budget, the selection rules, the store writer, the audit row and the forced refresh) — verified against `packages-paid/api-multi-tenant/src/routes/workspaces/offerings.ts`, `packages/api/src/services/offerings/store.ts`, `packages/api/src/server.ts` and `architecture.yaml`.
233
269
 
234
- Prior: 2026-10-18 — the per-Organization Stripe webhook (#8932): added the `POST /v1/webhooks/stripe/:connectionToken` bullet (the token, the key-less verify, the record attribution, the Price check, the one lead on the full key held for a live session, the legacy `offerings_path` step-aside, the dedup split, the log and path redaction) and named the Offerings webhook as the marker's second writer in the member reads bullet — verified against `apps/api/src/routes/webhooks/{stripe-offerings,stripe,index}.ts`, `apps/api/src/services/lead-routing.ts`, `apps/api/src/lib/{webhook-idempotency,error-serializer}.ts` and `packages/stripe-client/src/webhook.ts`.
270
+ Prior: 2026-10-18 — the per-Organization Stripe webhook (#8932): added the `POST /v1/webhooks/stripe/:connectionToken` bullet (the token, the key-less verify, the record attribution, the Price check, the one lead on the full key held for a live session, the legacy `offerings_path` step-aside, the dedup split, the log and path redaction) and named the Offerings webhook as the marker's second writer in the member reads bullet — verified against `apps/api/src/routes/webhooks/{stripe-offerings,stripe,index}.ts`, `packages/api/src/services/lead-routing.ts`, `apps/api/src/lib/{webhook-idempotency,error-serializer}.ts` and `packages/stripe-client/src/webhook.ts`.
235
271
 
236
- Prior: 2026-10-18 — the may-sell product list (#8922): added the `/v1/billing/stripe-products` bullet (the two routes, their permissions and check order, the entry shape, the refusals, the store writer, the catalog source and its bounds, the audit row) — verified against `apps/api/src/routes/billing/stripe-products.ts`, `apps/api/src/services/offerings/{index,store,source}.ts` and `architecture.yaml`.
272
+ Prior: 2026-10-18 — the may-sell product list (#8922): added the `/v1/billing/stripe-products` bullet (the two routes, their permissions and check order, the entry shape, the refusals, the store writer, the catalog source and its bounds, the audit row) — verified against `packages-paid/api-multi-tenant/src/routes/billing/stripe-products.ts`, `apps/api/src/services/offerings/{index,store,source}.ts` and `architecture.yaml`.
237
273
 
238
- Prior: 2026-10-18 — the Organization Stripe connection (#8907): added the `/v1/billing/stripe-connection` bullet (the six routes, their permissions and check order, the cipher and its one file, the probes, the write budget, the audit and email, the rekey bin) and the `/v1/billing` read's `commerce` field — verified against `apps/api/src/routes/billing/{stripe-connection,read}.ts`, `apps/api/src/services/offerings/connection.ts`, `apps/api/src/lib/stripe.ts` and `apps/api/src/middleware/rate-limit.ts`.
274
+ Prior: 2026-10-18 — the Organization Stripe connection (#8907): added the `/v1/billing/stripe-connection` bullet (the six routes, their permissions and check order, the cipher and its one file, the probes, the write budget, the audit and email, the rekey bin) and the `/v1/billing` read's `commerce` field — verified against `apps/api/src/routes/billing/{stripe-connection,read}.ts`, `packages/api/src/services/offerings/connection.ts`, `packages/api/src/lib/stripe.ts` and `packages/api/src/middleware/rate-limit.ts`.
239
275
 
240
- Prior: 2026-10-18 — the plan offerings re-sourced from the shown set (#8934): the member-area bullet's `GET …/plan-offerings` sentence names both sources, the claim gate and the failed reads — verified against `apps/api/src/services/plan-offerings.ts` and `apps/api/src/routes/workspaces/plan.ts`.
276
+ Prior: 2026-10-18 — the plan offerings re-sourced from the shown set (#8934): the member-area bullet's `GET …/plan-offerings` sentence names both sources, the claim gate and the failed reads — verified against `packages/api/src/services/plan-offerings.ts` and `packages-paid/api-workspace/src/routes/workspaces/plan.ts`.
241
277
 
242
- Prior: 2026-10-18 — the anonymous `/pricing` read (#8924): the public content reads bullet names `shown-offerings`, its three answers, the `commerce` claim, the bounded refresh and its missing MCP twin — verified against `apps/api/src/routes/content/{index,shown-offerings}.ts`, `apps/api/src/server.ts` and `apps/api/src/services/offerings/index.ts`.
278
+ Prior: 2026-10-18 — the anonymous `/pricing` read (#8924): the public content reads bullet names `shown-offerings`, its three answers, the `commerce` claim, the bounded refresh and its missing MCP twin — verified against `apps/api/src/routes/content/{index,shown-offerings}.ts`, `packages/api/src/server.ts` and `packages/api/src/services/offerings/index.ts`.
243
279
 
244
- Prior: 2026-10-18 — the meetings read (#8843 PR A): added the `/v1/schedule/meetings` bullet (the Live set, the row order, the bookable rule, `privateSlugs` and its unlisted-not-secret meaning, the re-parse, the `list_meetings` twin) — verified against `apps/api/src/routes/schedule/index.ts`, `apps/api/src/lib/{public-meetings-config,tenant-db}.ts` and `packages/site-config/src/schema/meetings-schema.ts`.
280
+ Prior: 2026-10-18 — the meetings read (#8843 PR A): added the `/v1/schedule/meetings` bullet (the Live set, the row order, the bookable rule, `privateSlugs` and its unlisted-not-secret meaning, the re-parse, the `list_meetings` twin) — verified against `packages-paid/api-scheduling/src/routes/schedule/index.ts`, `apps/api/src/lib/{public-meetings-config,tenant-db}.ts` and `packages/site-config/src/schema/meetings-schema.ts`.
245
281
 
246
282
  Prior: 2026-10-18 — an HTTP twin for `get_pricing` (#7178): the public content reads bullet names `pricing`, and `GET /v1/content/pricing` as the tool's twin through `getPricingTiers` — verified against `apps/api/src/routes/content/{index,pricing}.ts` and `packages/content-adapters/src/pricing.ts`.
247
283
 
@@ -249,100 +285,100 @@ Prior: 2026-10-18 — an HTTP twin for `get_offerings` (#7177): the public conte
249
285
 
250
286
  Prior: 2026-10-16 — the checkout plan is held until a verified first link (#8793): the member-area bullet's checkout plan rule names the hold, the release in `resolveMemberPersonId` on a fresh `stamped` link, the agreement rule and the guarded write — verified against `apps/api/src/services/{lead-routing,person}.ts`.
251
287
 
252
- Prior: 2026-10-16 — a magnet rename sets unclaimed leads aside (#8734): the magnets bullet names the holder read first, the `unclaimed:` re-stamp in place of the lead refusal, the race's `409 conflict` and the audit summary's `unclaimed` — verified against `apps/api/src/lib/tenant-db.ts` and `apps/api/src/routes/magnets/index.ts`.
288
+ Prior: 2026-10-16 — a magnet rename sets unclaimed leads aside (#8734): the magnets bullet names the holder read first, the `unclaimed:` re-stamp in place of the lead refusal, the race's `409 conflict` and the audit summary's `unclaimed` — verified against `packages/api/src/lib/tenant-db.ts` and `packages-paid/api-signal-collectors/src/routes/magnets/index.ts`.
253
289
 
254
- Prior: 2026-10-16 — the sign-up evidence read is budgeted per site key (#8805): the consent bullet names the per-key bucket, its default and override, and the server-wide ceiling's exemption — verified against `apps/api/src/middleware/rate-limit.ts` and `apps/api/src/server.ts`.
290
+ Prior: 2026-10-16 — the sign-up evidence read is budgeted per site key (#8805): the consent bullet names the per-key bucket, its default and override, and the server-wide ceiling's exemption — verified against `packages/api/src/middleware/rate-limit.ts` and `packages/api/src/server.ts`.
255
291
 
256
292
  Prior: 2026-10-16 — a spoke purchase's checkout grant (#8778): the consent bullet names the spokeCheckout write point's grant from `payload.termsAccepted` — verified against `apps/api/src/services/{lead-routing,consent}.ts`.
257
293
 
258
- Prior: 2026-10-16 — the checkout grant (#8765 PR B): the consent bullet names `checkout` as the eighth, unverified source and the webhook's grant (`recordCheckoutConsent`, `checkoutBuyerPerson`, the stamp and outcome rules); BYO Stripe names the terms box and its Dashboard precondition — verified against `apps/api/src/routes/webhooks/stripe.ts`, `apps/api/src/services/{consent,checkout-buyer}.ts`, `apps/api/src/lib/consent-store.ts` and `database/migrations/20261019120000_consent_source_checkout/migration.sql`.
294
+ Prior: 2026-10-16 — the checkout grant (#8765 PR B): the consent bullet names `checkout` as the eighth, unverified source and the webhook's grant (`recordCheckoutConsent`, `checkoutBuyerPerson`, the stamp and outcome rules); BYO Stripe names the terms box and its Dashboard precondition — verified against `packages/api/src/routes/webhooks/stripe.ts`, `apps/api/src/services/{consent,checkout-buyer}.ts`, `packages/api/src/lib/consent-store.ts` and `database/migrations/20261019120000_consent_source_checkout/migration.sql`.
259
295
 
260
- Prior: 2026-10-15 — sign-up refuses without the agreements (#8765): the consent bullet names the evidence route, the account-agreements pair, the second ledger writer (the web sign-up hook), the account-level `user`-subject reading of a sign-up agreement and the member pair's change; `HUB_SESSION_ROUTES` is 26 entries — verified against `apps/api/src/routes/consent/account.ts`, `apps/api/src/routes/member/consents.ts`, `apps/api/src/middleware/auth.ts` and `apps/api/src/server.ts`.
296
+ Prior: 2026-10-15 — sign-up refuses without the agreements (#8765): the consent bullet names the evidence route, the account-agreements pair, the second ledger writer (the web sign-up hook), the account-level `user`-subject reading of a sign-up agreement and the member pair's change; `HUB_SESSION_ROUTES` is 26 entries — verified against `packages/api/src/routes/consent/account.ts`, `packages-paid/api-workspace/src/routes/member/consents.ts`, `packages/api/src/middleware/auth.ts` and `packages/api/src/server.ts`.
261
297
 
262
- Prior: 2026-10-15 — a per-magnet completion setting (#8757): the leads bullet names the `next` key and the invite email; the magnets bullet names the completion pair, its PATCH rules and refusals — verified against `apps/api/src/routes/{leads,magnets}/index.ts`, `apps/api/src/services/{magnet-completion,plan-offerings}.ts` and `apps/api/src/lib/flow-completion-store.ts`.
298
+ Prior: 2026-10-15 — a per-magnet completion setting (#8757): the leads bullet names the `next` key and the invite email; the magnets bullet names the completion pair, its PATCH rules and refusals — verified against `apps/api/src/routes/{leads,magnets}/index.ts`, `apps/api/src/services/{magnet-completion,plan-offerings}.ts` and `packages/api/src/lib/flow-completion-store.ts`.
263
299
 
264
- Prior: 2026-10-14 — the Contact form magnet (#8661): the magnets bullet names the Contact form seed, its callers and the `contact` slug's place in `slug_fixed`/`slug_reserved`; the definition bullet names the `contact` refusal and the `upsertFlowDefinition` guard — verified against `apps/api/src/lib/tenant-db.ts`, `apps/api/src/routes/{workspaces,magnets}/index.ts`, `apps/api/src/services/{flow-engine,project-listings}.ts` and `apps/api/src/bin/seed-presets.ts`.
300
+ Prior: 2026-10-14 — the Contact form magnet (#8661): the magnets bullet names the Contact form seed, its callers and the `contact` slug's place in `slug_fixed`/`slug_reserved`; the definition bullet names the `contact` refusal and the `upsertFlowDefinition` guard — verified against `packages/api/src/lib/tenant-db.ts`, `apps/api/src/routes/{workspaces,magnets}/index.ts`, `apps/api/src/services/{flow-engine,project-listings}.ts` and `packages-paid/api-signal-collectors/src/bin/seed-presets.ts`.
265
301
 
266
- Prior: 2026-10-14 — magnets editable after creation (#8641): the magnets bullet names the slug rename, its four `409` refusals and its audit row in place of the old immutable-slug refusal — verified against `apps/api/src/routes/magnets/index.ts` and `apps/api/src/lib/tenant-db.ts`.
302
+ Prior: 2026-10-14 — magnets editable after creation (#8641): the magnets bullet names the slug rename, its four `409` refusals and its audit row in place of the old immutable-slug refusal — verified against `packages-paid/api-signal-collectors/src/routes/magnets/index.ts` and `packages/api/src/lib/tenant-db.ts`.
267
303
 
268
- Prior: 2026-10-14 — the standard client onboarding diagnostic (#8515): the magnets bullet names the standard onboarding seed and its two callers; the flow-definition seed refuses the `onboarding` slug; the member-area bullet names Start onboarding and the outbound action; the member reads and writes bullets name the onboarding pair and the message `action`; `HUB_SESSION_ROUTES` is 24 entries — verified against `apps/api/src/lib/{tenant-db,member-store}.ts`, `apps/api/src/routes/{member/onboarding,workspaces/messages-out,workspaces/index}.ts`, `apps/api/src/services/flow-engine.ts`, `apps/api/src/bin/seed-presets.ts` and `apps/api/src/middleware/auth.ts`.
304
+ Prior: 2026-10-14 — the standard client onboarding diagnostic (#8515): the magnets bullet names the standard onboarding seed and its two callers; the flow-definition seed refuses the `onboarding` slug; the member-area bullet names Start onboarding and the outbound action; the member reads and writes bullets name the onboarding pair and the message `action`; `HUB_SESSION_ROUTES` is 24 entries — verified against `apps/api/src/lib/{tenant-db,member-store}.ts`, `apps/api/src/routes/{member/onboarding,workspaces/messages-out,workspaces/index}.ts`, `packages-paid/api-signal-collectors/src/services/flow-engine.ts`, `packages-paid/api-signal-collectors/src/bin/seed-presets.ts` and `packages/api/src/middleware/auth.ts`.
269
305
 
270
- Prior: 2026-10-14 — proposals as an artifact kind (#8516): the member-area bullet names the piece `kind`, the proposals pair, its two MCP twins and the money gate (C4); the member-writes bullet names a proposal's artifact keys, the client review audit row and the pay route with its same-site and sandbox rule; the BYO Stripe bullet names the webhook's proposal stamp — verified against `apps/api/src/routes/workspaces/pieces.ts`, `apps/api/src/routes/member/{artifacts,artifact-review}.ts`, `apps/api/src/services/proposal-payments.ts`, `apps/api/src/lib/{tenant-db,member-store,proposal-paid-store}.ts` and `apps/api/src/routes/webhooks/stripe.ts`.
306
+ Prior: 2026-10-14 — proposals as an artifact kind (#8516): the member-area bullet names the piece `kind`, the proposals pair, its two MCP twins and the money gate (C4); the member-writes bullet names a proposal's artifact keys, the client review audit row and the pay route with its same-site and sandbox rule; the BYO Stripe bullet names the webhook's proposal stamp — verified against `packages-paid/api-workspace/src/routes/workspaces/pieces.ts`, `apps/api/src/routes/member/{artifacts,artifact-review}.ts`, `packages-paid/api-workspace/src/services/proposal-payments.ts`, `apps/api/src/lib/{tenant-db,member-store,proposal-paid-store}.ts` and `packages/api/src/routes/webhooks/stripe.ts`.
271
307
 
272
- Prior: 2026-10-13 — the Person record redesign (#8514): the People bullet names members on the three client rows, the agent-withheld rule and its fail-closed marker, and the private notes pair with its two MCP twins; the member-area bullet's floors name members — verified against `packages/validation/src/schemas/permissions.ts`, `apps/api/src/services/default-tenant.ts`, `apps/api/src/middleware/{tenant,default-tenant}.ts`, `apps/api/src/routes/workspaces/{people,plan}.ts` and `apps/api/src/lib/tenant-db.ts`. Before that, 2026-10-13 — the contact details on the client record (#8435): the People bullet names the contact columns, the capture fill rule, the operator PATCH and the subscriber/callback links; the member-writes bullet names the two contact routes; `HUB_SESSION_ROUTES` is 20 entries. Before that, 2026-10-11 — the spoke member admission (#8220): the hub-sign-in bullet names `HUB_SESSION_ROUTES`' 18 entries by group, the per-(person, spoke) budget, the `spokeId` log binding, the per-IP residual and the spoke's cross-site-request obligation; the #8169 and #8170 bullets say their routes are admitted, and the #8169 bullet states the one-owner-per-organization precondition — verified against `apps/api/src/middleware/auth.ts`, `apps/api/src/lib/member-store.ts`, `apps/api/src/routes/member/billing.ts` and `apps/api/src/__tests__/member-isolation.test.ts`.
308
+ Prior: 2026-10-13 — the Person record redesign (#8514): the People bullet names members on the three client rows, the agent-withheld rule and its fail-closed marker, and the private notes pair with its two MCP twins; the member-area bullet's floors name members — verified against `packages/validation/src/schemas/permissions.ts`, `packages/api/src/services/default-tenant.ts`, `apps/api/src/middleware/{tenant,default-tenant}.ts`, `apps/api/src/routes/workspaces/{people,plan}.ts` and `packages/api/src/lib/tenant-db.ts`. Before that, 2026-10-13 — the contact details on the client record (#8435): the People bullet names the contact columns, the capture fill rule, the operator PATCH and the subscriber/callback links; the member-writes bullet names the two contact routes; `HUB_SESSION_ROUTES` is 20 entries. Before that, 2026-10-11 — the spoke member admission (#8220): the hub-sign-in bullet names `HUB_SESSION_ROUTES`' 18 entries by group, the per-(person, spoke) budget, the `spokeId` log binding, the per-IP residual and the spoke's cross-site-request obligation; the #8169 and #8170 bullets say their routes are admitted, and the #8169 bullet states the one-owner-per-organization precondition — verified against `packages/api/src/middleware/auth.ts`, `packages/api/src/lib/member-store.ts`, `packages-paid/api-workspace/src/routes/member/billing.ts` and `packages-paid/api-multi-tenant/src/__tests__/member-isolation.test.ts`.
273
309
 
274
- Prior: 2026-10-12 — the hub's restricted live Stripe key (#8450): the BYO Stripe bullet on the boot admission, the admitted-key guard on the portal session and the checkout start and read, the three routes that stay sandbox-only, the live-only checkout bucket and the webhook's livemode drop — verified against `apps/api/src/lib/stripe.ts`, `apps/api/src/server.ts`, `apps/api/src/routes/{subscription/portal,checkout/sessions,checkout/session,webhooks/stripe}.ts` and `architecture.yaml`.
310
+ Prior: 2026-10-12 — the hub's restricted live Stripe key (#8450): the BYO Stripe bullet on the boot admission, the admitted-key guard on the portal session and the checkout start and read, the three routes that stay sandbox-only, the live-only checkout bucket and the webhook's livemode drop — verified against `packages/api/src/lib/stripe.ts`, `packages/api/src/server.ts`, `apps/api/src/routes/{subscription/portal,checkout/sessions,checkout/session,webhooks/stripe}.ts` and `architecture.yaml`.
275
311
 
276
- Prior: 2026-10-11 — the spoke member admission (#8220): the hub-sign-in bullet names `HUB_SESSION_ROUTES`' 18 entries by group, the per-(person, spoke) budget, the `spokeId` log binding, the per-IP residual and the spoke's cross-site-request obligation; the #8169 and #8170 bullets say their routes are admitted, and the #8169 bullet states the one-owner-per-organization precondition — verified against `apps/api/src/middleware/auth.ts`, `apps/api/src/lib/member-store.ts`, `apps/api/src/routes/member/billing.ts` and `apps/api/src/__tests__/member-isolation.test.ts`.
312
+ Prior: 2026-10-11 — the spoke member admission (#8220): the hub-sign-in bullet names `HUB_SESSION_ROUTES`' 18 entries by group, the per-(person, spoke) budget, the `spokeId` log binding, the per-IP residual and the spoke's cross-site-request obligation; the #8169 and #8170 bullets say their routes are admitted, and the #8169 bullet states the one-owner-per-organization precondition — verified against `packages/api/src/middleware/auth.ts`, `packages/api/src/lib/member-store.ts`, `packages-paid/api-workspace/src/routes/member/billing.ts` and `packages-paid/api-multi-tenant/src/__tests__/member-isolation.test.ts`.
277
313
 
278
- Prior: 2026-10-11 — the provider routes (#8171): added the bullet for the Requests read and Deliver a version, and extended the People (`request` and `version` timeline kinds), meetings (`customerSlugs`), magnets (`customersEnabled`), deliverables and asset-store (a version's file) bullets and added a `/v1/subscription/me` note (`?include=billing`) — verified against `apps/api/src/routes/workspaces/{requests,pieces,meetings}.ts`, `apps/api/src/routes/magnets/index.ts`, `apps/api/src/routes/subscription/me.ts`, `apps/api/src/services/subscription.ts` and `apps/api/src/lib/{tenant-db,asset-store,deliverable-asset-store,stripe}.ts`.
314
+ Prior: 2026-10-11 — the provider routes (#8171): added the bullet for the Requests read and Deliver a version, and extended the People (`request` and `version` timeline kinds), meetings (`customerSlugs`), magnets (`customersEnabled`), deliverables and asset-store (a version's file) bullets and added a `/v1/subscription/me` note (`?include=billing`) — verified against `apps/api/src/routes/workspaces/{requests,pieces,meetings}.ts`, `packages-paid/api-signal-collectors/src/routes/magnets/index.ts`, `packages-paid/api-commerce/src/routes/subscription/me.ts`, `packages-paid/api-commerce/src/services/subscription.ts` and `apps/api/src/lib/{tenant-db,asset-store,deliverable-asset-store,stripe}.ts`.
279
315
 
280
- Prior: 2026-10-11 — the Phase 1 member writes (#8170): added the bullet for the six `/v1/member/*` writes (the inbound Interaction rows, the hourly cap under the Person lock, the review under the Person then piece lock, the booking bound, the deletion destination, the log rule) — verified against `apps/api/src/routes/member/*.ts`, `apps/api/src/lib/member-store.ts`, `apps/api/src/services/member-area.ts` and `apps/api/src/server.ts`.
316
+ Prior: 2026-10-11 — the Phase 1 member writes (#8170): added the bullet for the six `/v1/member/*` writes (the inbound Interaction rows, the hourly cap under the Person lock, the review under the Person then piece lock, the booking bound, the deletion destination, the log rule) — verified against `apps/api/src/routes/member/*.ts`, `packages/api/src/lib/member-store.ts`, `packages/api/src/services/member-area.ts` and `packages/api/src/server.ts`.
281
317
 
282
- Prior: 2026-10-11 — the root index's `modules` report (#7546, after #7501): added the `/` (GET) bullet at the head of Endpoint conventions — the field, its `servedModules()` / `OPTIONAL_MODULES` source, `docs` absent from the vocabulary, `content-write` reported only alongside `workspace` or `multi-tenant` — verified against `apps/api/src/config/modules.ts`, `apps/api/src/server.ts` and `apps/api/src/__tests__/root-modules.test.ts`.
318
+ Prior: 2026-10-11 — the root index's `modules` report (#7546, after #7501): added the `/` (GET) bullet at the head of Endpoint conventions — the field, its `servedModules()` / `OPTIONAL_MODULES` source, `docs` absent from the vocabulary, `content-write` reported only alongside `workspace` or `multi-tenant` — verified against `packages/api/src/config/modules.ts`, `packages/api/src/server.ts` and `packages-paid/api-multi-tenant/src/__tests__/root-modules.test.ts`.
283
319
 
284
- Prior: 2026-10-10 — the Phase 1 member reads (#8169): added the bullet for the eight `/v1/member/*` GETs (the organization grain of purchases and billing, the hub-credential-only billing stance, the scheduling-gated pair) — verified against `apps/api/src/routes/member/*.ts`, `apps/api/src/lib/member-store.ts` and `apps/api/src/server.ts`.
320
+ Prior: 2026-10-10 — the Phase 1 member reads (#8169): added the bullet for the eight `/v1/member/*` GETs (the organization grain of purchases and billing, the hub-credential-only billing stance, the scheduling-gated pair) — verified against `apps/api/src/routes/member/*.ts`, `packages/api/src/lib/member-store.ts` and `packages/api/src/server.ts`.
285
321
 
286
322
  Prior: 2026-10-09 — an agent key may be granted `member-pieces-manage` (#7878): the member-area bullet's four writes read it through `allows`, and the agent-seat bullet lists seven grantable permissions — verified against `apps/api/src/routes/workspaces/{pieces,plan,messages-out}.ts` and `packages/validation/src/schemas/permissions.ts`.
287
323
 
288
- Prior: 2026-10-09 — a spoke's schedule calls bind the event owner to its workspace (#7944): the `/v1/schedule/*` bullet gains the spoke gate and the accepted shared MCP schedule budget — verified against `apps/api/src/routes/schedule/index.ts`, `apps/api/src/services/scheduling/{types,cal-provider}.ts` and `apps/api/src/middleware/rate-limit.ts`.
324
+ Prior: 2026-10-09 — a spoke's schedule calls bind the event owner to its workspace (#7944): the `/v1/schedule/*` bullet gains the spoke gate and the accepted shared MCP schedule budget — verified against `packages-paid/api-scheduling/src/routes/schedule/index.ts`, `apps/api/src/services/scheduling/{types,cal-provider}.ts` and `packages/api/src/middleware/rate-limit.ts`.
289
325
 
290
- Prior: 2026-10-09 — the member area contract (#7877): added the operator-route bullet (pieces, piece stages, plan, outbound messages; `people-write`; the bounds, refusals and audit; the checkout plan rule; the no-twin reason) and the member-read bullet (`/v1/member/*`, the caller seam, the refusals, the never-served fields, the file headers), the timeline's `message` kind on the People bullet, and the private-asset rule on the asset-store bullet — verified against `apps/api/src/routes/{workspaces/{pieces,plan,messages-out},member/{pieces,messages}}.ts`, `apps/api/src/services/{member-area,person,lead-routing}.ts`, `apps/api/src/lib/{member-store,tenant-db,asset-store}.ts` and `apps/api/src/middleware/auth.ts`.
326
+ Prior: 2026-10-09 — the member area contract (#7877): added the operator-route bullet (pieces, piece stages, plan, outbound messages; `people-write`; the bounds, refusals and audit; the checkout plan rule; the no-twin reason) and the member-read bullet (`/v1/member/*`, the caller seam, the refusals, the never-served fields, the file headers), the timeline's `message` kind on the People bullet, and the private-asset rule on the asset-store bullet — verified against `apps/api/src/routes/{workspaces/{pieces,plan,messages-out},member/{pieces,messages}}.ts`, `apps/api/src/services/{member-area,person,lead-routing}.ts`, `apps/api/src/lib/{member-store,tenant-db,asset-store}.ts` and `packages/api/src/middleware/auth.ts`.
291
327
 
292
- Prior: 2026-10-09 — Person, the per-workspace client record (#852): added the `/v1/workspaces/:workspaceId/people` bullet (the two reads, `people-read`, the grant, the bounds, the no-twin reason, the never-served columns) — verified against `apps/api/src/routes/workspaces/people.ts`, `apps/api/src/lib/tenant-db.ts` and `apps/api/src/services/person.ts`.
328
+ Prior: 2026-10-09 — Person, the per-workspace client record (#852): added the `/v1/workspaces/:workspaceId/people` bullet (the two reads, `people-read`, the grant, the bounds, the no-twin reason, the never-served columns) — verified against `packages-paid/api-workspace/src/routes/workspaces/people.ts`, `packages/api/src/lib/tenant-db.ts` and `packages/api/src/services/person.ts`.
293
329
 
294
- Prior: 2026-10-09 — the hub signs in its spokes (#7894): the `/v1/hub-sign-in/*` bullet in Endpoint conventions — verified against `apps/api/src/routes/auth/hub-sign-in.ts`, `apps/api/src/services/hub-sign-in.ts`, `apps/api/src/lib/hub-sign-in-store.ts`, `apps/api/src/middleware/auth.ts` and `apps/api/src/bin/spokes.ts`.
330
+ Prior: 2026-10-09 — the hub signs in its spokes (#7894): the `/v1/hub-sign-in/*` bullet in Endpoint conventions — verified against `packages-paid/api-hub/src/routes/auth/hub-sign-in.ts`, `packages-paid/api-hub/src/services/hub-sign-in.ts`, `packages-paid/api-hub/src/lib/hub-sign-in-store.ts`, `packages/api/src/middleware/auth.ts` and `packages-paid/api-hub/src/bin/spokes.ts`.
295
331
 
296
- Prior: 2026-10-08 — agent key grants (#7864): the agent-seat bullet names `PUT /v1/workspaces/:workspaceId/agents/:agentId/grants`, the five grantable permissions and the person clamp — verified against `apps/api/src/routes/workspaces/agents.ts`, `apps/api/src/services/default-tenant.ts` and `packages/validation/src/schemas/permissions.ts`.
332
+ Prior: 2026-10-08 — agent key grants (#7864): the agent-seat bullet names `PUT /v1/workspaces/:workspaceId/agents/:agentId/grants`, the five grantable permissions and the person clamp — verified against `packages-paid/api-workspace/src/routes/workspaces/agents.ts`, `packages/api/src/services/default-tenant.ts` and `packages/validation/src/schemas/permissions.ts`.
297
333
 
298
- Prior: 2026-10-05 — doc-sync: added the agent seat routes (#3591, #7678) to Endpoint conventions — verified against `apps/api/src/routes/workspaces/agents.ts`.
334
+ Prior: 2026-10-05 — doc-sync: added the agent seat routes (#3591, #7678) to Endpoint conventions — verified against `packages-paid/api-workspace/src/routes/workspaces/agents.ts`.
299
335
 
300
336
  Prior: 2026-10-04 — doc-sync: the agent seat, step E (#7684) — the messages and submissions bullets now name each route's signed-in MCP twin on `/mcp/workspace` (#3591 D13) in place of the retired no-twin exception — verified against `apps/api/src/routes/workspaces/{messages,submissions}.ts`, `apps/mcp/src/workspace-server.ts` and `tests/parity/api-parity/kinds/site.ts`.
301
337
 
302
- Prior: 2026-10-04 — doc-sync: roles by job (#7636 PR-A, #7653) — every role floor on the routes above now names its row of `@working-theory/validation`'s permission table (`can(tenant.role, "<permission>")`, no route-local role constant): a member reads and acts on the inbox family, appointments, the waitlist dashboard and listing decisions, reads live-only docs, magnets and meeting types and the asset list, and is refused draft content, workspace settings (the config PUT gained its first check) and agent sessions; an admin manages the calendar connection (audited, other owners emailed) and is refused the billing portal — verified against `apps/api/src/routes/{workspaces,magnets,assets,billing,waitlist-experiments,project-listings}/*.ts`, `apps/api/src/routes/schedule/appointments.ts` and `apps/api/src/services/{leads-inbox,inbox-messages,inbox-submissions,waitlist-dashboard,waitlist-operator-reads,tenant-context}.ts`.
338
+ Prior: 2026-10-04 — doc-sync: roles by job (#7636 PR-A, #7653) — every role floor on the routes above now names its row of `@working-theory/validation`'s permission table (`can(tenant.role, "<permission>")`, no route-local role constant): a member reads and acts on the inbox family, appointments, the waitlist dashboard and listing decisions, reads live-only docs, magnets and meeting types and the asset list, and is refused draft content, workspace settings (the config PUT gained its first check) and agent sessions; an admin manages the calendar connection (audited, other owners emailed) and is refused the billing portal — verified against `apps/api/src/routes/{workspaces,magnets,assets,billing,waitlist-experiments,project-listings}/*.ts`, `packages-paid/api-scheduling/src/routes/schedule/appointments.ts` and `apps/api/src/services/{leads-inbox,inbox-messages,inbox-submissions,waitlist-dashboard,waitlist-operator-reads,tenant-context}.ts`.
303
339
 
304
- Prior: 2026-10-03 — doc-sync: the Inbox submissions and subscribers reads (#7535) — added the `/v1/workspaces/:workspaceId/submissions` and `/v1/workspaces/:workspaceId/subscribers` bullets — verified against `apps/api/src/routes/workspaces/{submissions,subscribers}.ts`, `apps/api/src/services/inbox-submissions.ts`, `apps/api/src/lib/tenant-db.ts` and `apps/api/src/server.ts`.
340
+ Prior: 2026-10-03 — doc-sync: the Inbox submissions and subscribers reads (#7535) — added the `/v1/workspaces/:workspaceId/submissions` and `/v1/workspaces/:workspaceId/subscribers` bullets — verified against `apps/api/src/routes/workspaces/{submissions,subscribers}.ts`, `packages-paid/api-workspace/src/services/inbox-submissions.ts`, `packages/api/src/lib/tenant-db.ts` and `packages/api/src/server.ts`.
305
341
 
306
- Prior: 2026-10-03 — doc-sync: a scheduling connection per workspace (#7533) — added the `/v1/workspaces/:workspaceId/meetings/connection` and `POST /v1/webhooks/calcom/:connectionToken` bullets and the workspace-aware resolution note on `/v1/schedule/*` — verified against `apps/api/src/routes/workspaces/meetings.ts`, `apps/api/src/routes/webhooks/{calcom,index}.ts`, `apps/api/src/routes/schedule/index.ts` and `apps/api/src/services/scheduling/{connections,resolve}.ts`.
342
+ Prior: 2026-10-03 — doc-sync: a scheduling connection per workspace (#7533) — added the `/v1/workspaces/:workspaceId/meetings/connection` and `POST /v1/webhooks/calcom/:connectionToken` bullets and the workspace-aware resolution note on `/v1/schedule/*` — verified against `packages-paid/api-scheduling/src/routes/workspaces/meetings.ts`, `apps/api/src/routes/webhooks/{calcom,index}.ts`, `packages-paid/api-scheduling/src/routes/schedule/index.ts` and `apps/api/src/services/scheduling/{connections,resolve}.ts`.
307
343
 
308
- Prior: 2026-10-03 — doc-sync: the multi-tenant null-binding fallback and the platform-operator scope both require the caller to BE the verified `AILK_OPERATOR_EMAIL` pin (#7523 D1, D3) — corrected the Inbox message role-floor note, the `/v1/waitlist-experiments/*` row and the `AILK_OPERATOR_EMAIL` paragraph — verified against `apps/api/src/services/tenant-context.ts`, `apps/api/src/services/default-tenant.ts` and `apps/api/src/services/waitlist-operator-reads.ts`.
344
+ Prior: 2026-10-03 — doc-sync: the multi-tenant null-binding fallback and the platform-operator scope both require the caller to BE the verified `AILK_OPERATOR_EMAIL` pin (#7523 D1, D3) — corrected the Inbox message role-floor note, the `/v1/waitlist-experiments/*` row and the `AILK_OPERATOR_EMAIL` paragraph — verified against `packages/api/src/services/tenant-context.ts`, `packages/api/src/services/default-tenant.ts` and `packages-paid/api-signal-collectors/src/services/waitlist-operator-reads.ts`.
309
345
 
310
- Prior: 2026-10-03 — doc-sync: the general GET gains `emailFrom`, and `GET /v1/flows/:slug` serves the config minus `completionEmail` (#7519 D5, D6) — verified against `apps/api/src/routes/workspaces/general.ts`, `apps/api/src/services/flow-engine.ts` and `apps/api/src/services/deliverable-fulfillment.ts`.
346
+ Prior: 2026-10-03 — doc-sync: the general GET gains `emailFrom`, and `GET /v1/flows/:slug` serves the config minus `completionEmail` (#7519 D5, D6) — verified against `packages-paid/api-workspace/src/routes/workspaces/general.ts`, `packages-paid/api-signal-collectors/src/services/flow-engine.ts` and `packages/api/src/services/deliverable-fulfillment.ts`.
311
347
 
312
- Prior: 2026-09-21 — doc-sync: re-pointed magnet deliverables at the workspace-scoped `Asset` store (#6310) — extended the `/v1/deliverables/:grantId` row with the new `asset_` grant class (the named boundary it reads through, its serve-time tenancy re-check, its hardened attachment+CSP+no-store serve for every content type, and its uniform-404 fold), recording the rejected redirect-to-`/v1/assets/:assetId` mechanism and why — verified against `apps/api/src/routes/deliverables/index.ts`, `apps/api/src/lib/deliverable-asset-store.ts` and `apps/api/src/services/deliverable-fulfillment.ts`.
348
+ Prior: 2026-09-21 — doc-sync: re-pointed magnet deliverables at the workspace-scoped `Asset` store (#6310) — extended the `/v1/deliverables/:grantId` row with the new `asset_` grant class (the named boundary it reads through, its serve-time tenancy re-check, its hardened attachment+CSP+no-store serve for every content type, and its uniform-404 fold), recording the rejected redirect-to-`/v1/assets/:assetId` mechanism and why — verified against `packages-paid/api-signal-collectors/src/routes/deliverables/index.ts`, `packages/api/src/lib/deliverable-asset-store.ts` and `packages/api/src/services/deliverable-fulfillment.ts`.
313
349
 
314
- Prior: 2026-09-19 — doc-sync: added the three per-magnet captured-lead list routes (#6246 S6h) — `GET /v1/workspaces/:workspaceId/magnets/:id/{signups,subscribers,messages}` — to the magnets bullet, recording the `Lead.source = 'flow:' + slug` join, the new `tenantDb(tenant).leads.listBySource` accessor, the new `(workspaceId, source)` index, the wrong-kind/absent-magnet shared 404, the four/five-key wire shapes, the bounded `?limit=`, and the no-MCP-twin decision — verified against `apps/api/src/routes/magnets/index.ts`, `apps/api/src/lib/tenant-db.ts` and `database/inbox/schema.prisma`.
350
+ Prior: 2026-09-19 — doc-sync: added the three per-magnet captured-lead list routes (#6246 S6h) — `GET /v1/workspaces/:workspaceId/magnets/:id/{signups,subscribers,messages}` — to the magnets bullet, recording the `Lead.source = 'flow:' + slug` join, the new `tenantDb(tenant).leads.listBySource` accessor, the new `(workspaceId, source)` index, the wrong-kind/absent-magnet shared 404, the four/five-key wire shapes, the bounded `?limit=`, and the no-MCP-twin decision — verified against `packages-paid/api-signal-collectors/src/routes/magnets/index.ts`, `packages/api/src/lib/tenant-db.ts` and `database/inbox/schema.prisma`.
315
351
 
316
- Prior: 2026-09-19 — doc-sync: added the Inbox message API (#6216 S7e) — `GET /v1/workspaces/:workspaceId/messages` and `GET`/`PATCH /v1/workspaces/:workspaceId/messages/:leadId` — to Endpoint conventions, recording the six-key served row, the message-source allowlist confinement, the shared 404 for four causes, the one-way idempotent PATCH, the `owner|admin` role floor and its TOFU precondition, and the no-MCP-twin decision — verified against `apps/api/src/routes/workspaces/messages.ts`, `apps/api/src/services/inbox-messages.ts`, `apps/api/src/lib/tenant-db.ts` and `apps/api/src/server.ts`.
352
+ Prior: 2026-09-19 — doc-sync: added the Inbox message API (#6216 S7e) — `GET /v1/workspaces/:workspaceId/messages` and `GET`/`PATCH /v1/workspaces/:workspaceId/messages/:leadId` — to Endpoint conventions, recording the six-key served row, the message-source allowlist confinement, the shared 404 for four causes, the one-way idempotent PATCH, the `owner|admin` role floor and its TOFU precondition, and the no-MCP-twin decision — verified against `packages-paid/api-workspace/src/routes/workspaces/messages.ts`, `packages-paid/api-workspace/src/services/inbox-messages.ts`, `packages/api/src/lib/tenant-db.ts` and `packages/api/src/server.ts`.
317
353
 
318
- Prior: 2026-09-19 — doc-sync: added the tenant-scoped magnet Admin authoring surface (#6157 S5c) — `GET`/`POST /v1/workspaces/:workspaceId/magnets` and `GET`/`PATCH /v1/workspaces/:workspaceId/magnets/:id` — to Endpoint conventions, recording its `tenantDb`-only data path, its `owner|admin` role floor, the `multi-tenant || signal-collectors` gate, the config-URL origin gate and the no-MCP-twin parity decision — verified against `apps/api/src/routes/magnets/index.ts`, `apps/api/src/lib/tenant-db.ts` and `apps/api/src/server.ts`.
354
+ Prior: 2026-09-19 — doc-sync: added the tenant-scoped magnet Admin authoring surface (#6157 S5c) — `GET`/`POST /v1/workspaces/:workspaceId/magnets` and `GET`/`PATCH /v1/workspaces/:workspaceId/magnets/:id` — to Endpoint conventions, recording its `tenantDb`-only data path, its `owner|admin` role floor, the `multi-tenant || signal-collectors` gate, the config-URL origin gate and the no-MCP-twin parity decision — verified against `packages-paid/api-signal-collectors/src/routes/magnets/index.ts`, `packages/api/src/lib/tenant-db.ts` and `packages/api/src/server.ts`.
319
355
 
320
- Prior: 2026-09-19 — doc-sync: the enabled-meeting-slugs gate (#6183 S9b) — added the `/v1/workspaces/:workspaceId/meetings` bullet (its own scope, gated on `scheduling` alone, the D24 `listEventTypes` join) and the `/v1/schedule/meeting-types` bullet (the public read, its own boundary file) — verified against `apps/api/src/routes/workspaces/meetings.ts`, `apps/api/src/routes/schedule/meeting-types.ts`, `apps/api/src/lib/public-meetings-config.ts` and `apps/api/src/server.ts`.
356
+ Prior: 2026-09-19 — doc-sync: the enabled-meeting-slugs gate (#6183 S9b) — added the `/v1/workspaces/:workspaceId/meetings` bullet (its own scope, gated on `scheduling` alone, the D24 `listEventTypes` join) and the `/v1/schedule/meeting-types` bullet (the public read, its own boundary file) — verified against `packages-paid/api-scheduling/src/routes/workspaces/meetings.ts`, `packages-paid/api-scheduling/src/routes/schedule/meeting-types.ts`, `packages-paid/api-scheduling/src/lib/public-meetings-config.ts` and `packages/api/src/server.ts`.
321
357
 
322
- Prior: 2026-09-18 — doc-sync: the Docs list gains tags and a closed kind, and the asset store admits documents and gains list, metadata update and delete (#6093) — completed the `/v1/workspaces/:workspaceId/content` bullet with the page/asset union (`tags`, derived `kind`, `?tag=` filter), and added the `/v1/workspaces/:workspaceId/assets` + `/v1/assets/:assetId` family bullet (list/PATCH/DELETE, the widened upload allowlist admitting `image/svg+xml` and `application/pdf`, and the public read's raster-inline/non-raster-attachment+CSP serve split) — verified against `apps/api/src/routes/workspaces/content.ts`, `apps/api/src/routes/assets/{index,upload,read,manage}.ts` and `apps/api/src/lib/tenant-db.ts`.
358
+ Prior: 2026-09-18 — doc-sync: the Docs list gains tags and a closed kind, and the asset store admits documents and gains list, metadata update and delete (#6093) — completed the `/v1/workspaces/:workspaceId/content` bullet with the page/asset union (`tags`, derived `kind`, `?tag=` filter), and added the `/v1/workspaces/:workspaceId/assets` + `/v1/assets/:assetId` family bullet (list/PATCH/DELETE, the widened upload allowlist admitting `image/svg+xml` and `application/pdf`, and the public read's raster-inline/non-raster-attachment+CSP serve split) — verified against `packages-paid/api-content-write/src/routes/workspaces/content.ts`, `apps/api/src/routes/assets/{index,upload,read,manage}.ts` and `packages/api/src/lib/tenant-db.ts`.
323
359
 
324
- Prior: 2026-09-16 — doc-sync: added `GET /v1/leads` (#5678), the owner-scoped leads inbox read, to Endpoint conventions, recording its unconditional registration, its five-field row, its `resolveOwnerWorkspaces` scoping and its no-MCP-twin parity decision — verified against `apps/api/src/routes/leads/list.ts`, `apps/api/src/services/leads-inbox.ts`, `apps/api/src/services/owner-workspaces.ts` and `apps/api/src/server.ts`.
360
+ Prior: 2026-09-16 — doc-sync: added `GET /v1/leads` (#5678), the owner-scoped leads inbox read, to Endpoint conventions, recording its unconditional registration, its five-field row, its `resolveOwnerWorkspaces` scoping and its no-MCP-twin parity decision — verified against `packages/api/src/routes/leads/list.ts`, `packages/api/src/services/leads-inbox.ts`, `packages/api/src/services/owner-workspaces.ts` and `packages/api/src/server.ts`.
325
361
 
326
- Prior: 2026-09-14 — doc-sync: signal-collectors module extraction (D-48, #5366) — added the `module: signal-collectors` note to the flows/waitlist-signups-me/project-listings-me endpoints, added the previously-undocumented `/v1/deliverables/:grantId`, `/v1/project-listings/{start,submit,:id,public}` and `/v1/slack/actions` rows — verified against `apps/api/src/server.ts` and each route's own header.
362
+ Prior: 2026-09-14 — doc-sync: signal-collectors module extraction (D-48, #5366) — added the `module: signal-collectors` note to the flows/waitlist-signups-me/project-listings-me endpoints, added the previously-undocumented `/v1/deliverables/:grantId`, `/v1/project-listings/{start,submit,:id,public}` and `/v1/slack/actions` rows — verified against `packages/api/src/server.ts` and each route's own header.
327
363
 
328
- Prior: 2026-09-12 — doc-sync: added the `## Deploy path` section and the matching `## What NOT to do` line (#5334), recording that bundling `apps/api` is not a supported deploy path and naming the three gates — verified against `apps/api/vercel.json`, `apps/api/src/lib/slice-load.ts` and `scripts/check-bundled-build-diagnoses.ts`.
364
+ Prior: 2026-09-12 — doc-sync: added the `## Deploy path` section and the matching `## What NOT to do` line (#5334), recording that bundling `apps/api` is not a supported deploy path and naming the three gates — verified against `apps/api/vercel.json`, `packages/api/src/lib/slice-load.ts` and `scripts/check-bundled-build-diagnoses.ts`.
329
365
 
330
- Prior: 2026-09-12 — doc-sync: added `GET /v1/project-listings/me` (#5295), the auth-scoped founder read joined on the owner link, to Endpoint conventions — verified against `apps/api/src/routes/project-listings/me.ts` and `apps/api/src/server.ts`.
366
+ Prior: 2026-09-12 — doc-sync: added `GET /v1/project-listings/me` (#5295), the auth-scoped founder read joined on the owner link, to Endpoint conventions — verified against `packages-paid/api-signal-collectors/src/routes/project-listings/me.ts` and `packages/api/src/server.ts`.
331
367
 
332
- Prior: 2026-09-07 — doc-sync: added `listing-csv:sweep` (#5001), the weekly founder-CSV sweep, to `## Scheduled sweeps (external cron)` — verified against `apps/api/src/bin/listing-csv-sweep.ts` and `apps/api/package.json`.
368
+ Prior: 2026-09-07 — doc-sync: added `listing-csv:sweep` (#5001), the weekly founder-CSV sweep, to `## Scheduled sweeps (external cron)` — verified against `packages-paid/api-signal-collectors/src/bin/listing-csv-sweep.ts` and `apps/api/package.json`.
333
369
 
334
370
  Prior: 2026-09-04 — doc-sync: added the `## Scheduled sweeps (external cron)` section documenting `abandon:sweep` and `followup:sweep` (#4732 AC-6) — verified against `apps/api/package.json` and `apps/api/src/bin/`.
335
371
 
336
- Prior: 2026-09-04 — doc-sync: added `GET /v1/waitlist-signups/me` (#4731), the auth-scoped respondent read, to Endpoint conventions — verified against `apps/api/src/routes/waitlist-signups/index.ts` and `apps/api/src/server.ts`.
372
+ Prior: 2026-09-04 — doc-sync: added `GET /v1/waitlist-signups/me` (#4731), the auth-scoped respondent read, to Endpoint conventions — verified against `packages-paid/api-signal-collectors/src/routes/waitlist-signups/index.ts` and `packages/api/src/server.ts`.
337
373
 
338
- Prior: 2026-09-02 — doc-sync: noted the `AILK_OPERATOR_EMAIL` fail-closed precondition on the platform-operator scope in the `/v1/waitlist-experiments/*` row (PR #4687 security review, High finding) — verified against `apps/api/src/services/waitlist-dashboard.ts`.
374
+ Prior: 2026-09-02 — doc-sync: noted the `AILK_OPERATOR_EMAIL` fail-closed precondition on the platform-operator scope in the `/v1/waitlist-experiments/*` row (PR #4687 security review, High finding) — verified against `packages-paid/api-signal-collectors/src/services/waitlist-dashboard.ts`.
339
375
 
340
- Prior: 2026-09-02 — doc-sync: added `/v1/waitlist-experiments/*` routes (#4665 D7, #4671) to Endpoint conventions — verified against `apps/api/src/routes/waitlist-experiments/index.ts`.
376
+ Prior: 2026-09-02 — doc-sync: added `/v1/waitlist-experiments/*` routes (#4665 D7, #4671) to Endpoint conventions — verified against `packages-paid/api-signal-collectors/src/routes/waitlist-experiments/index.ts`.
341
377
 
342
- Prior: 2026-08-31 — doc-sync: added `GET /v1/schedule/event-meta` (#4633), the meta-only scheduling read, to the `/v1/schedule/*` row — verified against `apps/api/src/routes/schedule/index.ts`.
378
+ Prior: 2026-08-31 — doc-sync: added `GET /v1/schedule/event-meta` (#4633), the meta-only scheduling read, to the `/v1/schedule/*` row — verified against `packages-paid/api-scheduling/src/routes/schedule/index.ts`.
343
379
 
344
- Prior: 2026-08-26 — doc-sync: added `/v1/schedule/*` routes (#4416) to Endpoint conventions — verified against `apps/api/src/routes/schedule/index.ts`.
380
+ Prior: 2026-08-26 — doc-sync: added `/v1/schedule/*` routes (#4416) to Endpoint conventions — verified against `packages-paid/api-scheduling/src/routes/schedule/index.ts`.
345
381
 
346
- Prior: 2026-08-10 — doc-sync: added `/v1/flows/*` routes (#3956) to Endpoint conventions — verified against `apps/api/src/routes/flows/index.ts`.
382
+ Prior: 2026-08-10 — doc-sync: added `/v1/flows/*` routes (#3956) to Endpoint conventions — verified against `packages-paid/api-signal-collectors/src/routes/flows/index.ts`.
347
383
 
348
- Prior: 2026-07-15 — doc-sync: added `/v1/billing/*` routes (#3701) to Endpoint conventions; added the `## Services (billing-ledger)` section documenting `usage-metering.ts` / `usage-aggregation.ts` / `usage-credits.ts` — verified against `apps/api/src/routes/billing/index.ts` and `apps/api/src/services/`.
384
+ Prior: 2026-07-15 — doc-sync: added `/v1/billing/*` routes (#3701) to Endpoint conventions; added the `## Services (billing-ledger)` section documenting `usage-metering.ts` / `usage-aggregation.ts` / `usage-credits.ts` — verified against `packages-paid/api-multi-tenant/src/routes/billing/index.ts` and `apps/api/src/services/`.