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
@@ -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;