create-ailk 0.24.0 → 0.25.1

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 (599) hide show
  1. package/README.md +42 -20
  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 +1 -1
  14. package/dist/lib/fetch-template-snapshot.js +7 -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 +34 -12
  19. package/dist/module-architecture.d.ts +20 -3
  20. package/dist/module-architecture.js +117 -550
  21. package/dist/paid-sections.d.ts +25 -1
  22. package/dist/paid-sections.js +88 -3
  23. package/dist/surfaces.js +10 -2
  24. package/dist/template-record.d.ts +14 -4
  25. package/dist/template-record.js +23 -5
  26. package/dist/topology.d.ts +16 -3
  27. package/dist/topology.js +31 -19
  28. package/dist/update.d.ts +10 -1
  29. package/dist/update.js +540 -16
  30. package/package.json +3 -3
  31. package/release-scope.json +1 -1
  32. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  33. package/templates/.claude/hooks/template-guard.sh +137 -0
  34. package/templates/.claude/rules/extending.md +106 -0
  35. package/templates/.claude/rules/loop-contract.md +2 -2
  36. package/templates/.claude/settings.json +11 -0
  37. package/templates/.env.example +5 -5
  38. package/templates/.github/workflows/ci.yml +7 -4
  39. package/templates/CLAUDE.md +11 -1
  40. package/templates/CONVENTIONS.md +8 -10
  41. package/templates/README.md +14 -13
  42. package/templates/apps/api/.env.example +2 -2
  43. package/templates/apps/api/CLAUDE.md +102 -76
  44. package/templates/apps/api/jest.config.cjs +15 -5
  45. package/templates/apps/api/package.json +16 -20
  46. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  47. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  48. package/templates/apps/api/src/index.ts +1 -1
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -3
  50. package/templates/apps/api/src/modules/commerce.ts +10 -0
  51. package/templates/apps/api/src/modules/content-write.ts +10 -0
  52. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  53. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  54. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  55. package/templates/apps/api/src/modules/workspace.ts +10 -0
  56. package/templates/apps/api/src/server.ts +117 -1882
  57. package/templates/apps/api/vercel.json +2 -2
  58. package/templates/apps/mcp/CLAUDE.md +67 -20
  59. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  60. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  61. package/templates/apps/mcp/jest.config.cjs +6 -1
  62. package/templates/apps/mcp/package.json +5 -13
  63. package/templates/apps/mcp/src/app.ts +8 -7
  64. package/templates/apps/mcp/src/index.ts +6 -34
  65. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  66. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  67. package/templates/apps/mcp/src/site.ts +12 -0
  68. package/templates/apps/web/.env.example +1 -1
  69. package/templates/apps/web/CLAUDE.md +10 -3
  70. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  71. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  72. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  73. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +7 -3
  74. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  75. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  76. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  77. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  78. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  79. package/templates/apps/web/auth.ts +2 -2
  80. package/templates/apps/web/jest.config.cjs +1 -0
  81. package/templates/apps/web/lib/auth-email.ts +1 -1
  82. package/templates/apps/web/lib/relay.ts +2 -2
  83. package/templates/apps/web/lib/release-scope.ts +47 -0
  84. package/templates/apps/web/lib/server-api.ts +1 -1
  85. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  86. package/templates/customizations.yaml +37 -0
  87. package/templates/database/prisma.config.ts +10 -14
  88. package/templates/database/seed-test.ts +2 -2
  89. package/templates/database/seed.ts +5 -5
  90. package/templates/package.json +12 -6
  91. package/templates/pnpm-workspace.yaml +0 -1
  92. package/templates/project.yaml +16 -8
  93. package/templates/tools/jest/reporters.cjs +65 -0
  94. package/templates/tools/lint-architecture.ts +553 -7
  95. package/templates/tsconfig.json +1 -2
  96. package/versions.json +48 -35
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  98. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  99. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  100. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  101. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  102. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  103. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  104. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  105. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  106. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  107. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  108. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  109. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  110. package/templates/apps/api/src/bin/spokes.ts +0 -198
  111. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  112. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  113. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  114. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  115. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  116. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  117. package/templates/apps/api/src/config/index.ts +0 -596
  118. package/templates/apps/api/src/config/modules.ts +0 -83
  119. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  120. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  121. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  122. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  123. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  124. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  125. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  126. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  127. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  128. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  129. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  130. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  131. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  132. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  133. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  134. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  135. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  136. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  137. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  138. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -308
  139. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  140. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  141. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  142. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  143. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  144. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  145. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  146. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  147. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  148. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  149. package/templates/apps/api/src/lib/errors.ts +0 -95
  150. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  151. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  152. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  153. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  154. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  155. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  156. package/templates/apps/api/src/lib/locales.ts +0 -201
  157. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  158. package/templates/apps/api/src/lib/prisma.ts +0 -21
  159. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  160. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  161. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  162. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  163. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  164. package/templates/apps/api/src/lib/site.ts +0 -257
  165. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  166. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  167. package/templates/apps/api/src/lib/stripe.ts +0 -321
  168. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  169. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  170. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  171. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  172. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  173. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  174. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  175. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  176. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  177. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  178. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  179. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  180. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  181. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  182. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  183. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  184. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  185. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  186. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  187. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  188. package/templates/apps/api/src/middleware/auth.ts +0 -864
  189. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  190. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  191. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -391
  192. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  193. package/templates/apps/api/src/openapi/index.ts +0 -18
  194. package/templates/apps/api/src/openapi/spec.ts +0 -2148
  195. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  196. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  197. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  198. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  199. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  200. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  201. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  202. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  203. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  204. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  205. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  206. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  207. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  208. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -332
  209. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  210. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1426
  211. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  212. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  213. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  214. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  215. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  216. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  217. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  218. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  219. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  220. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  221. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  222. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  223. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  224. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  225. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  226. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  227. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  228. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  229. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  230. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  231. package/templates/apps/api/src/routes/content/get-page.ts +0 -183
  232. package/templates/apps/api/src/routes/content/index.ts +0 -49
  233. package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
  234. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  235. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  236. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  237. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  238. package/templates/apps/api/src/routes/content/search.ts +0 -100
  239. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  240. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  241. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  242. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  243. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  244. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  245. package/templates/apps/api/src/routes/leads/index.ts +0 -639
  246. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  247. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  248. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  249. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  250. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -491
  251. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
  252. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  253. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  254. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  255. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  256. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  257. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  258. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  259. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  260. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  261. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  262. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  263. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  264. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  265. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  266. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  267. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  268. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  269. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  270. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  271. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  272. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  273. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  274. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -449
  275. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  276. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  277. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  278. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  279. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  280. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  281. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  282. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  283. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -809
  284. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  285. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  286. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  287. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2028
  288. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
  289. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  290. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  291. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  292. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  293. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  294. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  295. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  296. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -877
  297. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  298. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  299. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  300. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  301. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  302. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  303. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  304. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  305. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  306. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1204
  307. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
  308. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
  309. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
  310. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  311. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  312. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  313. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  314. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  315. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  316. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  317. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  318. package/templates/apps/api/src/services/abandon-email.ts +0 -336
  319. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  320. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  321. package/templates/apps/api/src/services/audit-log.ts +0 -961
  322. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  323. package/templates/apps/api/src/services/consent.ts +0 -834
  324. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  325. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -750
  326. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  327. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  328. package/templates/apps/api/src/services/entitlements.ts +0 -338
  329. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  330. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  331. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  332. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  333. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  334. package/templates/apps/api/src/services/listing-config.ts +0 -178
  335. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  336. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -348
  337. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  338. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -975
  339. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  340. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -400
  341. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
  342. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  343. package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
  344. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
  345. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  346. package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
  347. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  348. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  349. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  350. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  351. package/templates/apps/api/src/services/person.ts +0 -1211
  352. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  353. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  354. package/templates/apps/api/src/services/project-listings.ts +0 -2085
  355. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  356. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  357. package/templates/apps/api/src/services/revalidation.ts +0 -235
  358. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  359. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  360. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  361. package/templates/apps/api/src/services/verified-email.ts +0 -71
  362. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  363. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  364. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  365. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  366. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  367. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  368. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  369. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  370. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  371. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  372. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  373. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  374. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  375. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  376. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  377. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  378. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1232
  379. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  380. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  381. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  382. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -70
  383. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
  384. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  385. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  386. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -180
  387. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  388. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
  389. package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
  390. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  391. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  392. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +0 -189
  393. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
  394. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
  395. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
  396. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  397. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  398. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -222
  399. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
  400. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  401. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  402. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  403. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  404. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  405. package/templates/apps/mcp/src/abuse/index.ts +0 -349
  406. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  407. package/templates/apps/mcp/src/boot.ts +0 -167
  408. package/templates/apps/mcp/src/config/modules.ts +0 -43
  409. package/templates/apps/mcp/src/http-transport.ts +0 -564
  410. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  411. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  412. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -116
  413. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
  414. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  415. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  416. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  417. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  418. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  419. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  420. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  421. package/templates/apps/mcp/src/lib/site-config.ts +0 -53
  422. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  423. package/templates/apps/mcp/src/request-context.ts +0 -52
  424. package/templates/apps/mcp/src/server.ts +0 -343
  425. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  426. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  427. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -66
  428. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  429. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  430. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  431. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  432. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  433. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  434. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  435. package/templates/apps/mcp/src/tools/index.ts +0 -138
  436. package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
  437. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  438. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  439. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  440. package/templates/apps/mcp/src/tools/read_content.ts +0 -81
  441. package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
  442. package/templates/apps/mcp/src/tools/search_content.ts +0 -63
  443. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
  444. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  445. package/templates/database/CHANGELOG.md +0 -981
  446. package/templates/database/LICENSE +0 -202
  447. package/templates/database/NOTICE +0 -2
  448. package/templates/database/README.md +0 -243
  449. package/templates/database/__tests__/agents.db.test.ts +0 -320
  450. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  451. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  452. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  453. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  454. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  455. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  456. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  457. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  458. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  459. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  460. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  461. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  462. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  463. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  464. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  465. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  466. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  467. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  468. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  469. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  470. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  471. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  472. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  473. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  474. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  475. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  476. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  477. package/templates/database/__tests__/person.db.test.ts +0 -586
  478. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  479. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  480. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  481. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  482. package/templates/database/__tests__/seed.test.ts +0 -272
  483. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  484. package/templates/database/__tests__/tenancy.test.ts +0 -141
  485. package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
  486. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  487. package/templates/database/_gitignore +0 -8
  488. package/templates/database/auth/.gitkeep +0 -0
  489. package/templates/database/auth/schema.prisma +0 -641
  490. package/templates/database/config/.gitkeep +0 -0
  491. package/templates/database/config/schema.prisma +0 -441
  492. package/templates/database/content/.gitkeep +0 -0
  493. package/templates/database/content/schema.prisma +0 -339
  494. package/templates/database/eslint.config.mjs +0 -69
  495. package/templates/database/flows/listing-diagnostic.ts +0 -182
  496. package/templates/database/flows/presets.ts +0 -315
  497. package/templates/database/inbox/.gitkeep +0 -0
  498. package/templates/database/inbox/README.md +0 -3
  499. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  500. package/templates/database/inbox/schema.prisma +0 -1447
  501. package/templates/database/jest.config.cjs +0 -36
  502. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  503. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  504. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  505. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  506. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  507. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  508. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  509. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  510. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  511. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  512. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  513. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  514. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  515. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  516. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  517. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  518. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  519. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  520. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  521. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  522. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  523. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  524. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  525. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  526. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  527. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  528. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  529. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  530. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  531. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  532. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  533. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  534. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  535. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  536. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  537. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  538. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  539. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  540. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  541. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  542. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  543. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  544. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  545. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  546. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  547. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  548. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  549. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  550. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  551. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  552. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  553. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  554. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  555. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  556. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  557. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  558. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  559. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  560. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  561. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  562. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  563. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  564. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  565. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  566. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  567. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  568. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  569. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  570. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  571. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  572. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  573. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  574. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  575. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  576. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  577. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  578. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  579. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  580. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  581. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  582. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  583. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  584. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  585. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  586. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +0 -28
  587. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
  588. package/templates/database/migrations/migration_lock.toml +0 -3
  589. package/templates/database/ops/.gitkeep +0 -0
  590. package/templates/database/ops/schema.prisma +0 -43
  591. package/templates/database/package.json +0 -89
  592. package/templates/database/schema.prisma +0 -71
  593. package/templates/database/scripts/db-generate-locked.sh +0 -80
  594. package/templates/database/scripts/person-backfill.sql +0 -132
  595. package/templates/database/tenancy.ts +0 -183
  596. package/templates/database/tsconfig.build.json +0 -16
  597. package/templates/database/tsconfig.flows.json +0 -15
  598. package/templates/database/tsconfig.json +0 -16
  599. package/templates/database/tsconfig.tenancy.json +0 -14
@@ -1,864 +0,0 @@
1
- /**
2
- * Dual-mode authentication preHandler for apps/api (#587).
3
- *
4
- * Decorates `request.userId` on authenticated requests, or returns 401.
5
- * Two modes, evaluated in order:
6
- *
7
- * 1. Cookie path — Better Auth session cookie present and valid.
8
- * Looked up directly against the `Session` table (the row Better Auth's
9
- * database adapter creates on sign-in). Cookie wins when both modes are
10
- * present (locked by brief decision OQ-D / spec §3 "Both present").
11
- * Non-GET requests with a cookie are required to carry an Origin header
12
- * that is exactly one of the configured trusted origins (#7395) — the
13
- * lightweight CSRF mechanism agreed in OQ-D. Bearer-authenticated callers
14
- * skip the origin check (non-browser flow).
15
- *
16
- * 2. Bearer path — Authorization: Bearer <token>. The token is sha256-hashed
17
- * and matched against `ApiToken.token` (stored as hash). Revoked / expired
18
- * tokens fail closed; lastUsedAt is bumped on a successful hit.
19
- *
20
- * 3. Agent path (#3591) — a bearer starting `ailk_agt_` is an agent's key
21
- * (`lib/agent-store.ts`). Its digest must match an enabled `Agent` whose
22
- * key-holder still holds a membership; anything else is the same 401.
23
- * The request acts as the agent's person (`request.userId`) and carries
24
- * `request.agent` — never `request.sessionTenant`, which stays the
25
- * cookie-only signal. An agent reaches ONLY routes a tenant guard tagged
26
- * (`config.tenantGuarded`, set by both guards' `onRoute` hook) and is 403
27
- * everywhere else; inside them the guard clamps its role. Each agent is
28
- * limited to 60 requests a minute, at most 10 of them writes (any
29
- * method but GET and HEAD).
30
- *
31
- * 4. Hub-credential path (#7894) — a bearer starting `ailk_hst_` is a
32
- * credential a spoke holds for a person the hub signed in on its behalf
33
- * (`lib/hub-sign-in-store.ts`). It is admitted only beside the SAME
34
- * spoke's key (`x-ailk-spoke-key`), only while its grant is unrevoked and
35
- * unexpired, its client enabled and its person's email verified, and only
36
- * on `HUB_SESSION_ROUTES` — an exact list of METHOD + registered URL
37
- * pairs — the credential's own `me`, the member routes and the
38
- * account-agreements pair, 26 in all (#8220, #8515, #8765); any other
39
- * route is 404 (never 403, so the account area's `read()` loads
40
- * "nothing for this caller"). It acts as its person
41
- * (`request.userId`) and carries `request.hubSession` — never
42
- * `request.sessionTenant` — and its request logger names the spoke
43
- * (`spokeId`). Each person is limited, at each spoke, to 60 requests and
44
- * 10 writes a minute and 30 writes an hour — a fixed hour window from
45
- * the bucket's first admitted write (#8220 D2, SEC-8169-4): the bucket is
46
- * keyed on the VERIFIED spoke and person, never a header, and charged
47
- * only after every check above passes, so a refused credential or an
48
- * off-list route never gets one. The write limits refuse only writes,
49
- * and a request the budget refuses is not counted.
50
- *
51
- * Every authenticated request carries `request.actor` — the person, or the
52
- * agent and the person it acts for — and its request/reply loggers are
53
- * rebound to name it, so Fastify's "request completed" line says who acted.
54
- * No log line carries a credential.
55
- *
56
- * Decision matrix (per spec §3):
57
- * - Cookie valid → cookie path wins. Bearer ignored.
58
- * - Cookie absent, bearer valid → bearer path; CSRF skipped.
59
- * - Both modes invalid → 401 unauthorized.
60
- * - Cookie expired + bearer valid → 401 (no silent fallback).
61
- * - Cookie absent, bearer malformed → 401 (no silent fallback).
62
- * - DATABASE_URL unset → 503 database_not_configured (#6482).
63
- *
64
- * Public routes (health, content, leads, webhooks, openapi) are NOT registered
65
- * with this preHandler — server.ts narrows scope via a sub-plugin.
66
- */
67
- import { createHash } from "node:crypto";
68
-
69
- import { getClock } from "@working-theory/common";
70
- import type { Id } from "@working-theory/ids";
71
- import type {
72
- FastifyPluginCallback,
73
- FastifyReply,
74
- FastifyRequest,
75
- } from "fastify";
76
-
77
- import {
78
- AGENT_KEY_PREFIX,
79
- findAgentByKeyHash,
80
- hashAgentKey,
81
- touchAgentLastActive,
82
- type AgentGrant,
83
- } from "../lib/agent-store.js";
84
- import {
85
- findAuthorizationByCredentialHash,
86
- HUB_SESSION_PREFIX,
87
- } from "../lib/hub-sign-in-store.js";
88
- import { prisma } from "../lib/prisma.js";
89
- import { findSpokeByKeyHash, hashSpokeKey } from "../lib/spoke-store.js";
90
-
91
- /**
92
- * Better Auth's default cookie names. Both are tried in order so a deployment
93
- * running over HTTP locally and HTTPS in production reads the same code path.
94
- */
95
- const COOKIE_NAMES = [
96
- "better-auth.session_token",
97
- "__Secure-better-auth.session_token",
98
- ] as const;
99
-
100
- const UNAUTHORIZED = { error: "unauthorized" } as const;
101
- const DATABASE_NOT_CONFIGURED = { error: "database_not_configured" } as const;
102
- const NOT_FOUND = {
103
- ok: false,
104
- error: { code: "not_found", message: "Not found" },
105
- } as const;
106
- const SPOKE_KEY_HEADER = "x-ailk-spoke-key";
107
- const FORBIDDEN = {
108
- ok: false,
109
- error: { code: "forbidden", message: "Forbidden" },
110
- } as const;
111
- /**
112
- * How stale `Agent.lastActiveAt` may be before an admitted request writes it
113
- * again: the column is "active within the last minute", not a request log,
114
- * so a busy agent costs one UPDATE a minute rather than one per request.
115
- */
116
- const AGENT_ACTIVITY_WRITE_INTERVAL_MS = 60_000;
117
-
118
- const RATE_LIMITED = {
119
- ok: false,
120
- error: { code: "rate_limited", message: "Too many requests" },
121
- } as const;
122
-
123
- declare module "fastify" {
124
- interface FastifyRequest {
125
- userId?: Id<"user">;
126
- sessionTenant?: SessionTenant;
127
- /** Set ONLY on the agent path (#3591 D4). */
128
- agent?: AgentGrant;
129
- /** Who made the request — set on every authenticated request (#3591 D9). */
130
- actor?: RequestActor;
131
- /**
132
- * Set ONLY on the hub-credential path (#7894 D6): the spoke the credential
133
- * and the request's spoke key both name, and that spoke's workspace.
134
- */
135
- hubSession?: HubSession;
136
- }
137
- interface FastifyContextConfig {
138
- /**
139
- * Set by both tenant guards' `onRoute` hook on every route registered
140
- * after them in their scope (#3591 D6). An agent is refused on any route
141
- * without it.
142
- */
143
- tenantGuarded?: boolean;
144
- }
145
- }
146
-
147
- /** The spoke a hub-issued credential was admitted for (#7894 D6). */
148
- export type HubSession = {
149
- spokeId: Id<"spoke">;
150
- workspaceId: Id<"workspace">;
151
- };
152
-
153
- /**
154
- * The only routes a hub-issued credential reaches (#7894 D6, #8220 D1): an
155
- * EXACT list of method + registered URL, never a prefix and never computed,
156
- * so a route added later joins only by a reviewed edit of this list
157
- * (SEC-7894-3). Six groups, in this order: the credential's own `me` and
158
- * #7877's three member reads; #8169's eight member reads; #8170's six member
159
- * writes (roadmap §7.2 row order); #8438's member consents read and write;
160
- * #8765's account-agreements read and write; #8515's onboarding read and
161
- * write — 26 in all.
162
- * `GET /v1/consent/platform` is not admitted — it is the Admin owner's own
163
- * consent to Working Theory, on the hub's cookie session. Nor is the
164
- * anonymous `GET /v1/consent/sign-up` (#8765 D2): it takes no credential. The member profile write IS admitted — it
165
- * is the person's own account, which roadmap D9 saves through it. Not
166
- * admitted, deliberately: `GET /v1/waitlist-signups/me`,
167
- * `GET /v1/flow-checkouts/me`, `GET /v1/subscription/me` and
168
- * `POST /v1/subscription/portal-session` (they answer for the HUB's site and
169
- * the person's platform billing, SEC-7894-1, -2), the token routes and the
170
- * hub's own-account `/v1/user/*` routes, `PATCH /v1/user/profile` among them.
171
- */
172
- export const HUB_SESSION_ROUTES: readonly string[] = Object.freeze([
173
- "GET /v1/hub-sign-in/me",
174
- "GET /v1/member/pieces",
175
- "GET /v1/member/messages",
176
- "GET /v1/member/messages/:id/file",
177
- "GET /v1/member/purchases",
178
- "GET /v1/member/appointments",
179
- "GET /v1/member/artifacts",
180
- "GET /v1/member/artifacts/:pieceId/versions/:version/file",
181
- "GET /v1/member/meeting-types",
182
- "GET /v1/member/availability",
183
- "GET /v1/member/request-form",
184
- "GET /v1/member/billing",
185
- "POST /v1/member/artifacts/:pieceId/review",
186
- "POST /v1/member/bookings",
187
- "POST /v1/member/requests",
188
- "POST /v1/member/messages",
189
- "PATCH /v1/member/profile",
190
- "GET /v1/member/profile/contact",
191
- "PATCH /v1/member/profile/contact",
192
- "POST /v1/member/deletion-request",
193
- // #8438 D6 — the member's own consents. `GET /v1/consent/platform` is NOT
194
- // admitted: those are the Admin owner's, on the hub's own cookie session.
195
- "GET /v1/member/consents",
196
- "POST /v1/member/consents",
197
- // #8765 D5 — the caller's account-level sign-up agreements, so a spoke's
198
- // account layout reads and accepts the one grant every site shares.
199
- "GET /v1/consent/account",
200
- "POST /v1/consent/account",
201
- // #8515 D13 — the client's onboarding read and write.
202
- "GET /v1/member/onboarding",
203
- "POST /v1/member/onboarding",
204
- ]);
205
-
206
- /**
207
- * Who made a request (#3591 D9). A cookie session and a personal `ApiToken`
208
- * act as their person; an agent acts on behalf of its responsible person.
209
- */
210
- export type RequestActor =
211
- | { type: "user"; userId: Id<"user"> }
212
- | { type: "agent"; agentId: Id<"agent">; onBehalfOfUserId: Id<"user"> };
213
-
214
- /**
215
- * Active tenant context carried by a cookie session (multi-tenant module).
216
- * Set ONLY on the cookie path — a personal `ApiToken` carries no workspace
217
- * binding, and an agent carries its own (`request.agent`, #3591), so neither
218
- * ever sets this. Code that needs a cookie session (the billing portal, the
219
- * workspace switch) can rely on its presence. `sessionRowId` is the Session
220
- * PK, used by the workspace-switch service to update the active tenant
221
- * server-side.
222
- */
223
- export type SessionTenant = {
224
- sessionRowId: Id<"session">;
225
- activeOrganizationId: Id<"organization"> | null;
226
- activeWorkspaceId: Id<"workspace"> | null;
227
- };
228
-
229
- /**
230
- * Hash an opaque bearer-token string the same way `tokens.ts` does at create
231
- * time. Centralised here so the lookup contract stays identical.
232
- */
233
- function hashBearer(token: string): string {
234
- return createHash("sha256").update(token).digest("hex");
235
- }
236
-
237
- /**
238
- * Parse the request's Cookie header for the first Better Auth session-token
239
- * variant present. Returns the cookie value (decoded) or undefined.
240
- *
241
- * Cookie parsing intentionally avoids decoding `+` as space — session tokens
242
- * are URL-safe and don't contain `+`. Use decodeURIComponent only on the value
243
- * to handle the rare case Better Auth percent-encodes the token.
244
- */
245
- function readSessionCookie(request: FastifyRequest): string | undefined {
246
- const raw = request.headers.cookie;
247
- if (!raw) return undefined;
248
- for (const part of raw.split(";")) {
249
- const eq = part.indexOf("=");
250
- if (eq < 0) continue;
251
- const name = part.slice(0, eq).trim();
252
- if (!COOKIE_NAMES.includes(name as (typeof COOKIE_NAMES)[number])) continue;
253
- const value = part.slice(eq + 1).trim();
254
- if (!value) continue;
255
- try {
256
- // Better Auth signs the cookie as <token>.<signature>; the dual-mode
257
- // preHandler only needs the token portion to look up the Session row.
258
- // Strip the signature if present.
259
- const decoded = decodeURIComponent(value);
260
- const dot = decoded.indexOf(".");
261
- return dot > 0 ? decoded.slice(0, dot) : decoded;
262
- } catch {
263
- return undefined;
264
- }
265
- }
266
- return undefined;
267
- }
268
-
269
- function readBearer(request: FastifyRequest): string | undefined {
270
- const auth = request.headers.authorization;
271
- if (!auth) return undefined;
272
- const match = /^Bearer\s+(\S+)$/i.exec(auth);
273
- return match ? match[1] : undefined;
274
- }
275
-
276
- /**
277
- * CSRF check for cookie-authenticated mutating requests (#7395).
278
- * Returns true if the request is safe (GET/HEAD/OPTIONS) or the Origin header
279
- * is exactly one of the trusted origins. False otherwise — including for any
280
- * write when the list is empty, in every environment.
281
- */
282
- function isOriginAllowed(
283
- request: FastifyRequest,
284
- trustedOrigins: readonly string[],
285
- ): boolean {
286
- const method = request.method.toUpperCase();
287
- if (method === "GET" || method === "HEAD" || method === "OPTIONS")
288
- return true;
289
- const origin = request.headers.origin;
290
- if (!origin) return false;
291
- return trustedOrigins.includes(origin);
292
- }
293
-
294
- type SessionRow = {
295
- id: string;
296
- userId: string;
297
- expiresAt: Date;
298
- activeOrganizationId: string | null;
299
- activeWorkspaceId: string | null;
300
- };
301
-
302
- async function resolveCookieSession(
303
- cookieToken: string,
304
- ): Promise<SessionRow | undefined> {
305
- const row = await prisma.session.findUnique({
306
- where: { token: cookieToken },
307
- select: {
308
- id: true,
309
- userId: true,
310
- expiresAt: true,
311
- activeOrganizationId: true,
312
- activeWorkspaceId: true,
313
- },
314
- });
315
- if (!row) return undefined;
316
- return row;
317
- }
318
-
319
- type BearerRow = {
320
- userId: string;
321
- expiresAt: Date | null;
322
- revokedAt: Date | null;
323
- id: string;
324
- };
325
-
326
- async function resolveBearerToken(
327
- bearer: string,
328
- ): Promise<BearerRow | undefined> {
329
- const hash = hashBearer(bearer);
330
- const row = await prisma.apiToken.findUnique({
331
- where: { token: hash },
332
- select: { id: true, userId: true, expiresAt: true, revokedAt: true },
333
- });
334
- return row ?? undefined;
335
- }
336
-
337
- /**
338
- * Decorate `request.actor` and rebind the request and reply loggers to name
339
- * it (#3591 D9). Fastify writes its "request completed" line through
340
- * `reply.log`, so that line carries the actor too.
341
- */
342
- function bindActor(
343
- request: FastifyRequest,
344
- reply: FastifyReply,
345
- actor: RequestActor,
346
- ): void {
347
- request.actor = actor;
348
- request.log = request.log.child(
349
- actor.type === "agent"
350
- ? {
351
- actorType: "agent",
352
- agentId: actor.agentId,
353
- onBehalfOfUserId: actor.onBehalfOfUserId,
354
- }
355
- : { actorType: "user" },
356
- );
357
- reply.log = request.log;
358
- }
359
-
360
- // ── The per-caller limits (#3591 D4 step 5, SEC-3591-04/-14; #8220 D2) ──────
361
- //
362
- // In the handler, not `onRequest`: the shared limiter runs before any
363
- // identity exists, and a bucket must only ever be keyed on a VERIFIED id, so
364
- // an invented key or credential never gets one. The shape of
365
- // `routes/subscription/portal.ts`'s per-user budget. An agent and a person
366
- // never share a map.
367
-
368
- const AGENT_WINDOW_MS = 60_000;
369
- const AGENT_REQUESTS_PER_WINDOW = 60;
370
- const AGENT_WRITES_PER_WINDOW = 10;
371
- /** Cap on distinct buckets — the oldest is evicted at capacity (DEF-011's shape). */
372
- const MAX_AGENT_BUCKETS = 5_000;
373
-
374
- const HUB_SESSION_WINDOW_MS = 60_000;
375
- const HUB_SESSION_REQUESTS_PER_WINDOW = 60;
376
- const HUB_SESSION_WRITES_PER_WINDOW = 10;
377
- const HUB_SESSION_WRITE_HOUR_MS = 3_600_000;
378
- const HUB_SESSION_WRITES_PER_HOUR = 30;
379
- const MAX_HUB_SESSION_BUCKETS = 5_000;
380
-
381
- type CallerBucket = {
382
- windowStartMs: number;
383
- requests: number;
384
- writes: number;
385
- /** The hourly write window — set by the bucket's first admitted write. */
386
- hourStartMs: number | null;
387
- hourWrites: number;
388
- };
389
-
390
- type CallerLimits = {
391
- windowMs: number;
392
- maxBuckets: number;
393
- requests: number;
394
- writes: number;
395
- /** An hourly write ceiling — the hub-credential path's alone. */
396
- writesPerHour?: { limit: number; windowMs: number };
397
- /** When true, the write limits refuse only writes; a read meets only `requests`. */
398
- refuseOnlyWrites: boolean;
399
- /** When true, a refused request still counts toward every limit. */
400
- countRefused: boolean;
401
- };
402
-
403
- const AGENT_LIMITS: CallerLimits = {
404
- windowMs: AGENT_WINDOW_MS,
405
- maxBuckets: MAX_AGENT_BUCKETS,
406
- requests: AGENT_REQUESTS_PER_WINDOW,
407
- writes: AGENT_WRITES_PER_WINDOW,
408
- refuseOnlyWrites: false,
409
- countRefused: true,
410
- };
411
-
412
- const HUB_SESSION_LIMITS: CallerLimits = {
413
- windowMs: HUB_SESSION_WINDOW_MS,
414
- maxBuckets: MAX_HUB_SESSION_BUCKETS,
415
- requests: HUB_SESSION_REQUESTS_PER_WINDOW,
416
- writes: HUB_SESSION_WRITES_PER_WINDOW,
417
- writesPerHour: {
418
- limit: HUB_SESSION_WRITES_PER_HOUR,
419
- windowMs: HUB_SESSION_WRITE_HOUR_MS,
420
- },
421
- refuseOnlyWrites: true,
422
- countRefused: false,
423
- };
424
-
425
- const agentBuckets = new Map<string, CallerBucket>();
426
- const hubSessionBuckets = new Map<string, CallerBucket>();
427
-
428
- /** Test-only reset. Not intended for production use. */
429
- export function __resetAgentRateBuckets(): void {
430
- agentBuckets.clear();
431
- }
432
-
433
- /** Test-only reset. Not intended for production use. */
434
- export function __resetHubSessionRateBuckets(): void {
435
- hubSessionBuckets.clear();
436
- }
437
-
438
- function isWrite(method: string): boolean {
439
- const m = method.toUpperCase();
440
- return m !== "GET" && m !== "HEAD";
441
- }
442
-
443
- /** A bucket no window still holds — the eviction pass may drop it. */
444
- function isStale(
445
- bucket: CallerBucket,
446
- now: number,
447
- limits: CallerLimits,
448
- ): boolean {
449
- if (now - bucket.windowStartMs < limits.windowMs) return false;
450
- return (
451
- bucket.hourStartMs === null ||
452
- limits.writesPerHour === undefined ||
453
- now - bucket.hourStartMs >= limits.writesPerHour.windowMs
454
- );
455
- }
456
-
457
- /**
458
- * Charge one request to `key`'s bucket. `undefined` when admitted; the
459
- * seconds to wait — to the end of the latest window that refused, at least
460
- * 1 — when over budget.
461
- */
462
- function chargeCallerBucket(
463
- buckets: Map<string, CallerBucket>,
464
- key: string,
465
- method: string,
466
- now: number,
467
- limits: CallerLimits,
468
- ): number | undefined {
469
- let bucket = buckets.get(key);
470
- if (!bucket || now - bucket.windowStartMs >= limits.windowMs) {
471
- if (!bucket && buckets.size >= limits.maxBuckets) {
472
- for (const [k, b] of buckets) {
473
- if (isStale(b, now, limits)) buckets.delete(k);
474
- }
475
- if (buckets.size >= limits.maxBuckets) {
476
- const oldest = buckets.keys().next().value;
477
- if (oldest !== undefined) buckets.delete(oldest);
478
- }
479
- }
480
- // Re-insert, so Map order stays oldest-window-first for the eviction
481
- // above. A new minute keeps the hour's start and count.
482
- buckets.delete(key);
483
- bucket = {
484
- windowStartMs: now,
485
- requests: 0,
486
- writes: 0,
487
- hourStartMs: bucket?.hourStartMs ?? null,
488
- hourWrites: bucket?.hourWrites ?? 0,
489
- };
490
- buckets.set(key, bucket);
491
- }
492
- const hour = limits.writesPerHour;
493
- if (
494
- hour !== undefined &&
495
- bucket.hourStartMs !== null &&
496
- now - bucket.hourStartMs >= hour.windowMs
497
- ) {
498
- bucket.hourStartMs = null;
499
- bucket.hourWrites = 0;
500
- }
501
-
502
- const write = isWrite(method);
503
- const requests = bucket.requests + 1;
504
- const writes = bucket.writes + (write ? 1 : 0);
505
- const hourWrites = bucket.hourWrites + (write ? 1 : 0);
506
- const minuteEnd = bucket.windowStartMs + limits.windowMs;
507
- const refusedUntil: number[] = [];
508
- if (requests > limits.requests) refusedUntil.push(minuteEnd);
509
- if (write || !limits.refuseOnlyWrites) {
510
- if (writes > limits.writes) refusedUntil.push(minuteEnd);
511
- if (write && hour !== undefined && hourWrites > hour.limit) {
512
- refusedUntil.push((bucket.hourStartMs ?? now) + hour.windowMs);
513
- }
514
- }
515
-
516
- const refused = refusedUntil.length > 0;
517
- if (!refused || limits.countRefused) {
518
- bucket.requests = requests;
519
- bucket.writes = writes;
520
- if (write && hour !== undefined) {
521
- bucket.hourWrites = hourWrites;
522
- bucket.hourStartMs ??= now;
523
- }
524
- }
525
- if (!refused) return undefined;
526
- return Math.max(1, Math.ceil((Math.max(...refusedUntil) - now) / 1_000));
527
- }
528
-
529
- /**
530
- * The agent path (#3591 D4, D6). The key is never logged and never echoed:
531
- * every refusal is a fixed body.
532
- */
533
- async function authenticateAgent(
534
- request: FastifyRequest,
535
- reply: FastifyReply,
536
- key: string,
537
- now: number,
538
- ): Promise<void> {
539
- const row = await findAgentByKeyHash(hashAgentKey(key)).catch(() => null);
540
- // Unknown, switched off, or its key-holder has left the organization (the
541
- // FK set the column null) — one body for each, so none is an oracle.
542
- if (!row || !row.enabled || !row.keyIssuedToMembershipId) {
543
- reply.code(401).send(UNAUTHORIZED);
544
- return;
545
- }
546
-
547
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5): the agents
548
- // table's format CHECKs guarantee these columns hold well-formed ids.
549
- const agentId = row.id as Id<"agent">;
550
- const personId = row.responsible.userId as Id<"user">;
551
- request.userId = personId;
552
- request.agent = {
553
- agentId,
554
- workspaceId: row.workspaceId as Id<"workspace">,
555
- organizationId: row.organizationId as Id<"organization">,
556
- responsibleMembershipId: row.responsible.id as Id<"membership">,
557
- permissionLevel: row.permissionLevel,
558
- // #7864 D3 — the one place a missing value becomes none; the tenant
559
- // guards keep only the grants its person's role also holds.
560
- grants: row.grants ?? [],
561
- };
562
- bindActor(request, reply, {
563
- type: "agent",
564
- agentId,
565
- onBehalfOfUserId: personId,
566
- });
567
-
568
- const retryAfter = chargeCallerBucket(
569
- agentBuckets,
570
- agentId,
571
- request.method,
572
- now,
573
- AGENT_LIMITS,
574
- );
575
- if (retryAfter !== undefined) {
576
- reply.code(429).header("Retry-After", String(retryAfter)).send(RATE_LIMITED);
577
- return;
578
- }
579
-
580
- // D6 — an agent reaches only routes a tenant guard runs on. Every other
581
- // route in the auth scope (tokens, user data, `/me` reads, the leads union,
582
- // the workspace switcher), and any route added outside a tenant scope
583
- // later, is closed to it by default.
584
- if (request.routeOptions.config?.tenantGuarded !== true) {
585
- reply.code(403).send(FORBIDDEN);
586
- return;
587
- }
588
-
589
- // Best-effort, as the token path bumps lastUsedAt: a failure never 401s.
590
- // Only an admitted request writes, and only when the stamp is a minute old.
591
- if (
592
- !row.lastActiveAt ||
593
- now - row.lastActiveAt.getTime() >= AGENT_ACTIVITY_WRITE_INTERVAL_MS
594
- ) {
595
- touchAgentLastActive(agentId, new Date(now)).catch(() => undefined);
596
- }
597
- }
598
-
599
- /**
600
- * The hub-credential path (#7894 D6). The credential is never logged and
601
- * never echoed: every refusal is a fixed body. (1)-(3) failing is the one 401;
602
- * (4), the route not being on `HUB_SESSION_ROUTES`, is 404 with no handler run;
603
- * (5), the person over budget at this spoke (#8220 D2), is 429 with no
604
- * handler run.
605
- */
606
- async function authenticateHubSession(
607
- request: FastifyRequest,
608
- reply: FastifyReply,
609
- credential: string,
610
- now: number,
611
- ): Promise<void> {
612
- // (1) The spoke key resolves to an unrevoked spoke.
613
- const rawKey = request.headers[SPOKE_KEY_HEADER];
614
- const spokeKey = typeof rawKey === "string" ? rawKey.trim() : "";
615
- const spoke = spokeKey
616
- ? await findSpokeByKeyHash(hashSpokeKey(spokeKey)).catch(() => null)
617
- : null;
618
- if (!spoke || spoke.revokedAt) {
619
- reply.code(401).send(UNAUTHORIZED);
620
- return;
621
- }
622
-
623
- // (2) The credential's grant is live, and its client IS this spoke's.
624
- // (3) Its person's email is verified (read in the same query).
625
- const grant = await findAuthorizationByCredentialHash(
626
- hashBearer(credential),
627
- ).catch(() => null);
628
- if (
629
- !grant ||
630
- grant.revokedAt ||
631
- grant.expiresAt.getTime() <= now ||
632
- !grant.client.enabled ||
633
- grant.client.clientId !== spoke.id ||
634
- grant.user.emailVerified !== true
635
- ) {
636
- reply.code(401).send(UNAUTHORIZED);
637
- return;
638
- }
639
-
640
- // (4) Method and registered URL are on the list.
641
- const route = `${request.method.toUpperCase()} ${request.routeOptions.url ?? ""}`;
642
- if (!HUB_SESSION_ROUTES.includes(route)) {
643
- reply.code(404).send(NOT_FOUND);
644
- return;
645
- }
646
-
647
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5), as above.
648
- request.userId = grant.userId as Id<"user">;
649
- request.hubSession = {
650
- spokeId: spoke.id as Id<"spoke">,
651
- workspaceId: spoke.workspaceId as Id<"workspace">,
652
- };
653
- bindActor(request, reply, { type: "user", userId: request.userId });
654
- // #8220 D10 — every line the request logger writes after admission names
655
- // the spoke that acted: its id, never its key or the credential. (Lines a
656
- // helper writes through the module-level `logger` carry no request binding.)
657
- request.log = request.log.child({ spokeId: spoke.id });
658
- reply.log = request.log;
659
-
660
- // (5) The verified (spoke, person) is within its budget.
661
- const retryAfter = chargeCallerBucket(
662
- hubSessionBuckets,
663
- `${spoke.id}:${grant.userId}`,
664
- request.method,
665
- now,
666
- HUB_SESSION_LIMITS,
667
- );
668
- if (retryAfter !== undefined) {
669
- reply.code(429).header("Retry-After", String(retryAfter)).send(RATE_LIMITED);
670
- return;
671
- }
672
- }
673
-
674
- export type AuthPreHandlerOptions = {
675
- /**
676
- * The browser origins a cookie-authenticated write may come from — exact
677
- * matches only. server.ts passes `config.TRUSTED_ORIGINS` (#7395).
678
- */
679
- trustedOrigins: readonly string[];
680
- };
681
-
682
- /**
683
- * The result of `resolveOptionalCookieUser` — a signed-in user, or the reason
684
- * there is none (#6866 D2).
685
- */
686
- export type OptionalCookieUser =
687
- | { kind: "user"; userId: Id<"user"> }
688
- | {
689
- kind: "none";
690
- reason:
691
- | "no_cookie"
692
- | "no_database"
693
- | "unknown_session"
694
- | "expired"
695
- | "cross_origin"
696
- | "lookup_failed";
697
- };
698
-
699
- /**
700
- * Non-rejecting cookie resolver for routes that stay open to anonymous callers
701
- * but act on a signed-in user when there is one (#6866 D2 — the checkout
702
- * route's buyer stamp).
703
- *
704
- * Applies the cookie path's own rules — the same private cookie reader,
705
- * Session lookup, expiry comparison and trusted-origin check as
706
- * `makeAuthPreHandler` — so the two paths cannot drift. Reads the COOKIE only:
707
- * a bearer `ApiToken` never identifies a user here. Never writes a reply and
708
- * never throws; every failure is a `none` with its reason. The cookie is
709
- * checked first, so an anonymous call on a database-less boot is `no_cookie`.
710
- */
711
- export async function resolveOptionalCookieUser(
712
- request: FastifyRequest,
713
- opts: AuthPreHandlerOptions,
714
- ): Promise<OptionalCookieUser> {
715
- const cookieToken = readSessionCookie(request);
716
- if (!cookieToken) return { kind: "none", reason: "no_cookie" };
717
- if (!process.env.DATABASE_URL) return { kind: "none", reason: "no_database" };
718
-
719
- let session: SessionRow | undefined;
720
- try {
721
- session = await resolveCookieSession(cookieToken);
722
- } catch {
723
- return { kind: "none", reason: "lookup_failed" };
724
- }
725
- if (!session) return { kind: "none", reason: "unknown_session" };
726
- if (session.expiresAt.getTime() <= getClock().now()) {
727
- return { kind: "none", reason: "expired" };
728
- }
729
- if (!isOriginAllowed(request, opts.trustedOrigins)) {
730
- return { kind: "none", reason: "cross_origin" };
731
- }
732
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5), as above.
733
- return { kind: "user", userId: session.userId as Id<"user"> };
734
- }
735
-
736
- /**
737
- * The person a member read answers for (#7877 D5): the hub's own cookie
738
- * session or a hub-issued credential (#7894 D6) — never a personal ApiToken,
739
- * never an agent. `null` → the route answers 401 with `UNAUTHORIZED`.
740
- *
741
- * This is the ONE place the member reads (`services/member-area.ts`) learn
742
- * who is calling: a later credential kind joins them by an edit of this
743
- * function and nothing in `routes/member/`. An agent never gets here — the
744
- * member routes are not tenant-guarded, so this preHandler answers it 403.
745
- */
746
- export function memberCaller(request: FastifyRequest): Id<"user"> | null {
747
- if (request.userId === undefined) return null;
748
- if (request.sessionTenant !== undefined || request.hubSession !== undefined) {
749
- return request.userId;
750
- }
751
- return null;
752
- }
753
-
754
- /**
755
- * Fastify preHandler implementing the dual-mode contract.
756
- *
757
- * Exported standalone so route-test suites can register it directly without
758
- * spinning up the full server module.
759
- */
760
- export function makeAuthPreHandler(opts: AuthPreHandlerOptions) {
761
- const { trustedOrigins } = opts;
762
- return async function authPreHandler(
763
- request: FastifyRequest,
764
- reply: FastifyReply,
765
- ): Promise<void> {
766
- // A database-less dev boot (#6482). First, and read per request: after
767
- // the lookups below, their `.catch` would already have answered 401.
768
- if (!process.env.DATABASE_URL) {
769
- reply.code(503).send(DATABASE_NOT_CONFIGURED);
770
- return;
771
- }
772
-
773
- const now = getClock().now();
774
- const cookieToken = readSessionCookie(request);
775
-
776
- if (cookieToken) {
777
- const session = await resolveCookieSession(cookieToken).catch(
778
- () => undefined,
779
- );
780
- if (session && session.expiresAt.getTime() > now) {
781
- if (!isOriginAllowed(request, trustedOrigins)) {
782
- reply.code(401).send(UNAUTHORIZED);
783
- return;
784
- }
785
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5): the DB
786
- // CHECK constraints guarantee these columns hold well-formed
787
- // prefixed ids, so the compile-time brand is asserted once here.
788
- request.userId = session.userId as Id<"user">;
789
- request.sessionTenant = {
790
- sessionRowId: session.id as Id<"session">,
791
- activeOrganizationId: session.activeOrganizationId as Id<"organization"> | null,
792
- activeWorkspaceId: session.activeWorkspaceId as Id<"workspace"> | null,
793
- };
794
- bindActor(request, reply, { type: "user", userId: request.userId });
795
- return;
796
- }
797
- // Cookie present but invalid / expired → 401 (no silent bearer fallback).
798
- reply.code(401).send(UNAUTHORIZED);
799
- return;
800
- }
801
-
802
- const bearer = readBearer(request);
803
- if (bearer?.startsWith(HUB_SESSION_PREFIX)) {
804
- await authenticateHubSession(request, reply, bearer, now);
805
- return;
806
- }
807
- if (bearer?.startsWith(AGENT_KEY_PREFIX)) {
808
- await authenticateAgent(request, reply, bearer, now);
809
- return;
810
- }
811
- if (bearer) {
812
- const row = await resolveBearerToken(bearer).catch(() => undefined);
813
- if (!row) {
814
- reply.code(401).send(UNAUTHORIZED);
815
- return;
816
- }
817
- if (row.revokedAt) {
818
- reply.code(401).send(UNAUTHORIZED);
819
- return;
820
- }
821
- if (row.expiresAt && row.expiresAt.getTime() <= now) {
822
- reply.code(401).send(UNAUTHORIZED);
823
- return;
824
- }
825
- request.userId = row.userId as Id<"user">;
826
- // A personal token acts as its person, as before (#3591 D9).
827
- bindActor(request, reply, { type: "user", userId: request.userId });
828
- // Best-effort: bump lastUsedAt. Failure here MUST NOT 401 — the caller
829
- // is already authenticated. Swallow the error and continue.
830
- prisma.apiToken
831
- .update({ where: { id: row.id }, data: { lastUsedAt: new Date(now) } })
832
- .catch(() => undefined);
833
- return;
834
- }
835
-
836
- // Neither present.
837
- reply.code(401).send(UNAUTHORIZED);
838
- };
839
- }
840
-
841
- /**
842
- * Plugin form — exposes the preHandler via Fastify's `addHook`. The
843
- * encapsulation override (Symbol.for("skip-override")) lets the hook
844
- * propagate to sibling routes registered in the SAME parent scope after the
845
- * plugin — matching the rate-limit.ts pattern. Without it, the hook would
846
- * only fire for routes registered INSIDE the plugin, which is not the calling
847
- * convention (server.ts registers the plugin first, then sibling routes).
848
- */
849
- export const authPreHandlerPlugin: FastifyPluginCallback<
850
- AuthPreHandlerOptions
851
- > = (fastify, opts, done) => {
852
- const handler = makeAuthPreHandler(opts);
853
- fastify.decorateRequest("userId", undefined);
854
- fastify.decorateRequest("sessionTenant", undefined);
855
- fastify.decorateRequest("agent", undefined);
856
- fastify.decorateRequest("actor", undefined);
857
- fastify.decorateRequest("hubSession", undefined);
858
- fastify.addHook("preHandler", handler);
859
- done();
860
- };
861
-
862
- (authPreHandlerPlugin as FastifyPluginCallback & { [key: symbol]: boolean })[
863
- Symbol.for("skip-override")
864
- ] = true;