create-ailk 0.23.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 (604) hide show
  1. package/README.md +68 -33
  2. package/component-catalog.json +3 -3
  3. package/component-catalog.md +3 -3
  4. package/dist/add-module.js +10 -3
  5. package/dist/add-route.d.ts +12 -1
  6. package/dist/add-route.js +38 -4
  7. package/dist/cli.js +12 -7
  8. package/dist/derive-architecture.d.ts +3 -4
  9. package/dist/derive-architecture.js +75 -22
  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/lib/scaffold-render-sweep.d.ts +33 -0
  20. package/dist/lib/scaffold-render-sweep.js +45 -0
  21. package/dist/module-architecture.d.ts +20 -3
  22. package/dist/module-architecture.js +122 -538
  23. package/dist/paid-sections.d.ts +25 -1
  24. package/dist/paid-sections.js +88 -3
  25. package/dist/surfaces.js +10 -2
  26. package/dist/sync-routes.d.ts +30 -4
  27. package/dist/sync-routes.js +271 -18
  28. package/dist/template-record.d.ts +14 -4
  29. package/dist/template-record.js +23 -5
  30. package/dist/topology.d.ts +16 -3
  31. package/dist/topology.js +31 -19
  32. package/dist/update.d.ts +10 -1
  33. package/dist/update.js +540 -16
  34. package/package.json +3 -3
  35. package/release-scope.json +1 -1
  36. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  37. package/templates/.claude/hooks/template-guard.sh +137 -0
  38. package/templates/.claude/rules/extending.md +106 -0
  39. package/templates/.claude/rules/loop-contract.md +2 -2
  40. package/templates/.claude/settings.json +11 -0
  41. package/templates/.env.example +9 -5
  42. package/templates/.github/workflows/ci.yml +7 -4
  43. package/templates/CLAUDE.md +15 -3
  44. package/templates/CONVENTIONS.md +8 -10
  45. package/templates/README.md +14 -13
  46. package/templates/apps/api/.env.example +9 -2
  47. package/templates/apps/api/CLAUDE.md +122 -82
  48. package/templates/apps/api/jest.config.cjs +15 -5
  49. package/templates/apps/api/package.json +16 -20
  50. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  51. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  52. package/templates/apps/api/src/index.ts +1 -1
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -4
  54. package/templates/apps/api/src/modules/commerce.ts +10 -0
  55. package/templates/apps/api/src/modules/content-write.ts +10 -0
  56. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  57. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  58. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  59. package/templates/apps/api/src/modules/workspace.ts +10 -0
  60. package/templates/apps/api/src/server.ts +117 -1857
  61. package/templates/apps/api/vercel.json +2 -2
  62. package/templates/apps/mcp/CLAUDE.md +77 -24
  63. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  64. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  65. package/templates/apps/mcp/jest.config.cjs +6 -1
  66. package/templates/apps/mcp/package.json +5 -12
  67. package/templates/apps/mcp/src/app.ts +8 -7
  68. package/templates/apps/mcp/src/index.ts +6 -34
  69. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  70. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  71. package/templates/apps/mcp/src/site.ts +12 -0
  72. package/templates/apps/web/.env.example +1 -1
  73. package/templates/apps/web/CLAUDE.md +21 -5
  74. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  75. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  76. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
  77. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  78. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  79. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  80. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +28 -7
  81. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  82. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  83. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  84. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  85. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  86. package/templates/apps/web/auth.ts +2 -2
  87. package/templates/apps/web/jest.config.cjs +1 -0
  88. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
  89. package/templates/apps/web/lib/auth-email.ts +1 -1
  90. package/templates/apps/web/lib/magnet-routes.ts +3 -1
  91. package/templates/apps/web/lib/newsletter.actions.ts +9 -6
  92. package/templates/apps/web/lib/relay.ts +2 -2
  93. package/templates/apps/web/lib/release-scope.ts +47 -0
  94. package/templates/apps/web/lib/server-api.ts +1 -1
  95. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  96. package/templates/customizations.yaml +37 -0
  97. package/templates/database/prisma.config.ts +10 -14
  98. package/templates/database/seed-test.ts +2 -2
  99. package/templates/database/seed.ts +32 -5
  100. package/templates/package.json +12 -6
  101. package/templates/pnpm-workspace.yaml +0 -1
  102. package/templates/project.yaml +16 -8
  103. package/templates/tools/jest/reporters.cjs +65 -0
  104. package/templates/tools/lint-architecture.ts +757 -7
  105. package/templates/tsconfig.json +1 -2
  106. package/versions.json +48 -35
  107. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  108. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  109. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  110. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  111. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  112. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  113. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  114. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  115. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  116. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  117. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  118. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  119. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  120. package/templates/apps/api/src/bin/spokes.ts +0 -198
  121. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  122. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  123. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  124. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  125. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  126. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  127. package/templates/apps/api/src/config/index.ts +0 -596
  128. package/templates/apps/api/src/config/modules.ts +0 -83
  129. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  130. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  131. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  132. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  133. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  134. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  135. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  136. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  137. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  138. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  139. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  140. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  141. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  142. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  143. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  144. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  145. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  146. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  147. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  148. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -292
  149. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  150. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  151. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  152. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  153. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  154. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  155. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  156. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  157. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  158. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  159. package/templates/apps/api/src/lib/errors.ts +0 -95
  160. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  161. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  162. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  163. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  164. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  165. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  166. package/templates/apps/api/src/lib/locales.ts +0 -201
  167. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  168. package/templates/apps/api/src/lib/prisma.ts +0 -21
  169. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  170. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  171. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  172. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  173. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  174. package/templates/apps/api/src/lib/site.ts +0 -254
  175. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  176. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  177. package/templates/apps/api/src/lib/stripe.ts +0 -318
  178. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  179. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  180. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  181. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  182. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  183. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  184. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  185. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  186. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  187. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  188. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  189. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  190. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  191. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  192. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  193. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  194. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  195. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  196. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  197. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  198. package/templates/apps/api/src/middleware/auth.ts +0 -864
  199. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  200. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  201. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -264
  202. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  203. package/templates/apps/api/src/openapi/index.ts +0 -18
  204. package/templates/apps/api/src/openapi/spec.ts +0 -1815
  205. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  206. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  207. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  208. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  209. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  210. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  211. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  212. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  213. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  214. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  215. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  216. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  217. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  218. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -325
  219. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  220. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1300
  221. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  222. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  223. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  224. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  225. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  226. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  227. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  228. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  229. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  230. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  231. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  232. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  233. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  234. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  235. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  236. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  237. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  238. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  239. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  240. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  241. package/templates/apps/api/src/routes/content/get-page.ts +0 -268
  242. package/templates/apps/api/src/routes/content/index.ts +0 -49
  243. package/templates/apps/api/src/routes/content/list-pages.ts +0 -95
  244. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  245. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  246. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  247. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  248. package/templates/apps/api/src/routes/content/search.ts +0 -129
  249. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  250. package/templates/apps/api/src/routes/content/slug.ts +0 -9
  251. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  252. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  253. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  254. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  255. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  256. package/templates/apps/api/src/routes/leads/index.ts +0 -573
  257. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  258. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  259. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  260. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  261. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -480
  262. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -465
  263. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  264. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  265. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  266. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  267. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  268. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  269. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  270. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  271. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  272. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  273. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  274. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  275. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  276. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  277. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  278. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  279. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  280. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  281. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  282. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  283. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  284. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  285. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -448
  286. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  287. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  288. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  289. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  290. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  291. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  292. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  293. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  294. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -808
  295. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  296. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  297. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  298. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2026
  299. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -528
  300. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  301. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  302. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  303. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  304. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  305. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  306. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  307. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -867
  308. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  309. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  310. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  311. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  312. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  313. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  314. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  315. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  316. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  317. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1176
  318. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -444
  319. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -154
  320. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  321. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  322. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  323. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  324. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  325. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  326. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  327. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  328. package/templates/apps/api/src/services/abandon-email.ts +0 -313
  329. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  330. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  331. package/templates/apps/api/src/services/audit-log.ts +0 -961
  332. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  333. package/templates/apps/api/src/services/consent.ts +0 -834
  334. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  335. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -755
  336. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  337. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  338. package/templates/apps/api/src/services/entitlements.ts +0 -338
  339. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  340. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  341. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  342. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  343. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  344. package/templates/apps/api/src/services/listing-config.ts +0 -178
  345. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  346. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -295
  347. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  348. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -868
  349. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  350. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -396
  351. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -681
  352. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  353. package/templates/apps/api/src/services/offerings/catalog.ts +0 -308
  354. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -385
  355. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  356. package/templates/apps/api/src/services/offerings/connection.ts +0 -1003
  357. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  358. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  359. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  360. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  361. package/templates/apps/api/src/services/person.ts +0 -1188
  362. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  363. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  364. package/templates/apps/api/src/services/project-listings.ts +0 -2073
  365. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  366. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  367. package/templates/apps/api/src/services/revalidation.ts +0 -235
  368. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  369. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  370. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  371. package/templates/apps/api/src/services/verified-email.ts +0 -71
  372. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  373. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  374. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  375. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  376. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  377. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  378. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  379. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  380. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  381. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  382. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  383. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  384. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  385. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  386. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  387. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  388. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1151
  389. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  390. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  391. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  392. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -65
  393. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -187
  394. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  395. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  396. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -165
  397. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  398. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -228
  399. package/templates/apps/mcp/__tests__/locale.test.ts +0 -135
  400. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  401. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  402. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -474
  403. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -225
  404. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -256
  405. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  406. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  407. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -307
  408. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -223
  409. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  410. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  411. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  412. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  413. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  414. package/templates/apps/mcp/src/abuse/index.ts +0 -342
  415. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  416. package/templates/apps/mcp/src/boot.ts +0 -167
  417. package/templates/apps/mcp/src/config/modules.ts +0 -43
  418. package/templates/apps/mcp/src/http-transport.ts +0 -526
  419. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  420. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  421. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -142
  422. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -139
  423. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  424. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  425. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  426. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  427. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  428. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  429. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  430. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  431. package/templates/apps/mcp/src/request-context.ts +0 -52
  432. package/templates/apps/mcp/src/server.ts +0 -343
  433. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  434. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  435. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -64
  436. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  437. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  438. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  439. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  440. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  441. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  442. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  443. package/templates/apps/mcp/src/tools/index.ts +0 -137
  444. package/templates/apps/mcp/src/tools/list_pages.ts +0 -74
  445. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  446. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  447. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  448. package/templates/apps/mcp/src/tools/read_content.ts +0 -75
  449. package/templates/apps/mcp/src/tools/request_callback.ts +0 -59
  450. package/templates/apps/mcp/src/tools/search_content.ts +0 -67
  451. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -57
  452. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  453. package/templates/database/CHANGELOG.md +0 -961
  454. package/templates/database/LICENSE +0 -202
  455. package/templates/database/NOTICE +0 -2
  456. package/templates/database/README.md +0 -243
  457. package/templates/database/__tests__/agents.db.test.ts +0 -320
  458. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  459. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  460. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  461. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  462. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  463. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  464. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  465. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  466. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  467. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  468. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  469. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  470. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  471. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  472. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  473. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  474. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  475. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  476. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  477. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  478. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  479. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  480. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  481. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  482. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  483. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  484. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  485. package/templates/database/__tests__/person.db.test.ts +0 -586
  486. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  487. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  488. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  489. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  490. package/templates/database/__tests__/seed.test.ts +0 -240
  491. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  492. package/templates/database/__tests__/tenancy.test.ts +0 -141
  493. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  494. package/templates/database/_gitignore +0 -8
  495. package/templates/database/auth/.gitkeep +0 -0
  496. package/templates/database/auth/schema.prisma +0 -641
  497. package/templates/database/config/.gitkeep +0 -0
  498. package/templates/database/config/schema.prisma +0 -441
  499. package/templates/database/content/.gitkeep +0 -0
  500. package/templates/database/content/schema.prisma +0 -339
  501. package/templates/database/eslint.config.mjs +0 -69
  502. package/templates/database/flows/listing-diagnostic.ts +0 -182
  503. package/templates/database/flows/presets.ts +0 -315
  504. package/templates/database/inbox/.gitkeep +0 -0
  505. package/templates/database/inbox/README.md +0 -3
  506. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  507. package/templates/database/inbox/schema.prisma +0 -1447
  508. package/templates/database/jest.config.cjs +0 -36
  509. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  510. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  511. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  512. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  513. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  514. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  515. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  516. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  517. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  518. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  519. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  520. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  521. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  522. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  523. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  524. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  525. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  526. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  527. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  528. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  529. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  530. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  531. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  532. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  533. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  534. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  535. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  536. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  537. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  538. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  539. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  540. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  541. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  542. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  543. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  544. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  545. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  546. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  547. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  548. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  549. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  550. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  551. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  552. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  553. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  554. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  555. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  556. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  557. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  558. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  559. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  560. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  561. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  562. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  563. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  564. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  565. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  566. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  567. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  568. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  569. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  570. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  571. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  572. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  573. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  574. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  575. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  576. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  577. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  578. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  579. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  580. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  581. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  582. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  583. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  584. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  585. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  586. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  587. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  588. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  589. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  590. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  591. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  592. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  593. package/templates/database/migrations/migration_lock.toml +0 -3
  594. package/templates/database/ops/.gitkeep +0 -0
  595. package/templates/database/ops/schema.prisma +0 -43
  596. package/templates/database/package.json +0 -89
  597. package/templates/database/schema.prisma +0 -71
  598. package/templates/database/scripts/db-generate-locked.sh +0 -80
  599. package/templates/database/scripts/person-backfill.sql +0 -132
  600. package/templates/database/tenancy.ts +0 -183
  601. package/templates/database/tsconfig.build.json +0 -16
  602. package/templates/database/tsconfig.flows.json +0 -15
  603. package/templates/database/tsconfig.json +0 -16
  604. package/templates/database/tsconfig.tenancy.json +0 -14
@@ -1,573 +0,0 @@
1
- /**
2
- * Leads route group.
3
- *
4
- * Routes:
5
- * POST /v1/leads/submit — lead submission (contact form / inquiry)
6
- * POST /v1/leads/subscribe — newsletter subscription
7
- * POST /v1/leads/callback — callback request (phone-back)
8
- * GET /v1/leads/capture — whether this deployment accepts a contact
9
- * message / a newsletter signup (#6730 D4)
10
- *
11
- * API↔MCP PARITY — `GET /v1/leads/capture` has NO MCP twin, a recorded
12
- * exception on the same footing as the anonymous form-lifecycle routes. It
13
- * exists so a web page can choose between the form and the contact email
14
- * before rendering; an agent never needs that preflight, because the three
15
- * write tools (`capture_lead`, `subscribe_newsletter`, `request_callback`)
16
- * already return a typed `api_error` carrying the 503 when capture is not
17
- * accepted — the same answer, at the moment it matters.
18
- *
19
- * TENANT RESOLUTION (#6203, spec §10.4 operator ruling — reverses this
20
- * file's former byte-unchanged posture). `siteId` used to be taken VERBATIM
21
- * from the request body (`body["siteId"] as string`) — an anonymous, no-
22
- * ownership-check value (`packages/validation/src/schemas/leads.ts`'s
23
- * `safeSiteIdString` permits a `wksp_<uuidv7>`-shaped literal). Every
24
- * capture kind now resolves its tenant SERVER-SIDE, via
25
- * `resolveSiteWorkspaceId()` (`lib/site.ts`) — the SAME alias -> workspace
26
- * resolution `currentSiteId()`/`AILK_SITE_ID` already drive throughout this
27
- * app, unconditional on the content adapter — and a client-posted `siteId`
28
- * is IGNORED entirely: it can no longer place a row anywhere. An unresolved
29
- * result (no DATABASE_URL, or the alias names no workspace this database
30
- * holds) is a 503 before any write is attempted — never a guess. See
31
- * `lead-routing.ts`'s own header for the persistence-side half.
32
- *
33
- * A REGISTERED SPOKE (#7532 D4) resolves FIRST: a hub serving several spokes
34
- * reads the spoke's key from `x-ailk-spoke-key` through
35
- * `resolveCallerSite()` (`services/public-site-resolver.ts`) and writes the
36
- * lead into that spoke's own workspace. A refused key — malformed, unknown or
37
- * revoked, or no key at all on a hub (a deployment whose spoke registry holds
38
- * a row, or one whose `AILK_HUB_REQUIRE_SPOKE_KEY` is `true`; #7633) — is the
39
- * same 503 `tenant_unresolved` before any write. A header-less request on a
40
- * deployment with no registered spoke, or under the emergency opt-out
41
- * (`AILK_HUB_REQUIRE_SPOKE_KEY=false`), takes the alias resolution above,
42
- * unchanged. So does a keyless request whose hub-status read fails before the
43
- * process has any answer: lead capture is never lost (#7633, conductor
44
- * amendment to NFR3). The body's `siteId` stays ignored on every branch.
45
- *
46
- * IDEMPOTENCY KEY (#7869, roadmap D5). `POST /v1/leads/submit` accepts an
47
- * optional `Idempotency-Key` header: 16 to 255 visible ASCII characters, and
48
- * it must be UNGUESSABLE (a UUID, a Stripe Checkout Session id) — a key is
49
- * first-write-wins inside its workspace, so a short or predictable one could
50
- * be claimed by someone else first (SEC-7869-1). It is checked after the
51
- * tenant gate and before `routeLead`; a malformed key is `400 invalid_input`
52
- * with no read or write. A repeat of a stored key in the same workspace with
53
- * the same `email`, `source` and `payload` answers the first lead's 201 and
54
- * writes nothing; with anything different it is `422
55
- * idempotency_key_reused`. The key dedupes only where a `Lead` row exists —
56
- * `inbox` delivery — so a first write that fell back to email or a local
57
- * file leaves nothing to find, and its retry writes again. `subscribe` and
58
- * `callback` never read the header. The key is never logged or echoed.
59
- *
60
- * CONTACT TERMS ACCEPTANCE (#8730). A `submit` whose `source` is a
61
- * consent-bearing source (`isConsentLeadSource` in @working-theory/validation:
62
- * the contact sources `contact-form`, `conversational-intake`, `flow:contact`,
63
- * and since #9130 the Free apply page's `flow:job-application`) must carry a
64
- * top-level `consent: true` — the boolean, nothing else — or it is
65
- * `400 consent_required` before the tenant read or any write. An accepted one
66
- * gets `payload.consent = { statement, acceptedAt }` written HERE, replacing
67
- * any `payload.consent` the client sent: `statement` is the platform's
68
- * contact terms statement (`CONTACT_FORM_TERMS_STATEMENT`) and `acceptedAt`
69
- * this request's receive time. The `consent` flag itself is never stored. A
70
- * lead with any other source, `subscribe` and `callback` are unchanged. A
71
- * direct caller can still send `consent: true`; the gate makes acceptance an
72
- * explicit, recorded assertion, not proof that a person read the terms.
73
- *
74
- * API↔MCP PARITY for `consent` (#8730): no guard entry, for the reason the
75
- * Idempotency-Key paragraph below gives — this route parses no validation
76
- * export, so it binds as `api-args-unbound`. `capture_lead` stamps `mcp`, a
77
- * source this check never reads, so the tool is unchanged.
78
- *
79
- * A MAGNET'S COMPLETION (#8757 D6). After a `flow:<slug>` submit is written
80
- * to the database, `services/magnet-completion.ts` reads that LIVE magnet's
81
- * completion setting. On `invite` the 201 gains `next: { completion:
82
- * "invite" }` and — on a first write, never a replay — the submitter is sent
83
- * one confirmation email whose link starts sign-in with their address
84
- * prefilled (capped per recipient and per magnet, counted in the database);
85
- * on `checkout`, re-checked against the `commerce` claim and the published
86
- * offerings, it gains `next: { completion: "checkout", checkoutOffering }`.
87
- * `lead`, a draft or missing magnet, or a caller with nowhere to sign in
88
- * leaves the 201 as it was. A failed read or send never changes the 201,
89
- * and the completion read has a time budget (`MAGNET_COMPLETION_BUDGET_MS`).
90
- * `next` is reachable only through a `flow:` source: `capture_lead` posts
91
- * `source: "mcp"`, so the tool's arguments get the same answer as before and
92
- * no parity guard row changes (NFR5).
93
- *
94
- * A CHECKOUT'S ACCOUNT LINK (#8757 D14). A checkout fulfilment's submit
95
- * (`source: "web"`, `payload.page: "checkout"`) carries the caller's account
96
- * origin into `routeLead` as `accountOrigin`, resolved exactly as the invite
97
- * link's (`accountTargetFor`): the spoke's enabled sign-in client's callback
98
- * origin, or `FRONTEND_URL`'s on a keyless build with an account area. With
99
- * none, or a failed read, it is left out and the buyer's purchase email goes
100
- * without the link. No other submit makes this read, and the 201 is unchanged.
101
- *
102
- * API↔MCP PARITY for the header (#7869 D9, roadmap §12 Q5): NO guard entry.
103
- * The guard binds a route's arguments to the validation export its handler
104
- * `.safeParse`s on `request.params|query|body`; the header check below is a
105
- * regex test, not such a parse, so this route still binds as
106
- * `api-args-unbound` and the `capture_lead` row is unchanged. The 201 body's
107
- * field set is unchanged too, and `capture_lead` sends no header. If the
108
- * guard ever models headers, this becomes an API-side `arg-only-in`.
109
- */
110
-
111
- import { track } from "@working-theory/analytics/server";
112
- import {
113
- CONTACT_FORM_TERMS_STATEMENT,
114
- isConsentLeadSource,
115
- type LeadSubmitNext,
116
- } from "@working-theory/validation";
117
- import type { FastifyPluginAsync } from "fastify";
118
-
119
- import { acceptsKind, resolveLeadDelivery } from "../../lib/lead-delivery.js";
120
- import { currentSiteId, resolveSiteWorkspaceId } from "../../lib/site.js";
121
- import {
122
- CONSENT_VISITOR_HEADER,
123
- consentVisitorFromHeader,
124
- } from "../../services/consent.js";
125
- import {
126
- type LeadKind,
127
- type RouteLeadInput,
128
- routeLead,
129
- type SourceMeta,
130
- } from "../../services/lead-routing.js";
131
- import {
132
- accountTargetFor,
133
- magnetCompletionWithinBudget,
134
- sendInviteConfirmation,
135
- } from "../../services/magnet-completion.js";
136
- import { resolveCallerSite } from "../../services/public-site-resolver.js";
137
- import { sendRequestConfirmLink } from "../../services/request-confirm.js";
138
-
139
- /** #7869 D1 — 16 to 255 visible ASCII characters; no spaces, no list. */
140
- const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{16,255}$/;
141
-
142
- /**
143
- * Read a named string field from the body, falling back to the same-named
144
- * field inside `body.payload` when the top-level value is absent — the
145
- * step-flow envelope (`{ siteId, source, payload: <record> }`) carries its
146
- * identity fields inside `payload` rather than at the top level. A top-level
147
- * value always wins.
148
- */
149
- function liftString(
150
- body: Record<string, unknown>,
151
- payload: Record<string, unknown>,
152
- key: string,
153
- ): string {
154
- const topLevel = body[key];
155
- if (typeof topLevel === "string") return topLevel;
156
- const fromPayload = payload[key];
157
- return (typeof fromPayload === "string" ? fromPayload : topLevel) as string;
158
- }
159
-
160
- /** #8730, #9130 — the refusal a consent-bearing lead without terms acceptance gets. */
161
- const CONSENT_REQUIRED_BODY = {
162
- error: "consent_required",
163
- message: "Accept the terms to send this form.",
164
- } as const;
165
-
166
- /**
167
- * #8730 D3, #9130 — the accepted terms, recorded by the server on a
168
- * consent-bearing lead's payload, replacing any `consent` the client wrote
169
- * there. Only a plain object payload is stamped: anything else is left for
170
- * `routeLead`'s schema to refuse, never spread into an object that would then
171
- * pass it.
172
- */
173
- function withConsentRecord(payload: unknown, acceptedAt: string): unknown {
174
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
175
- return payload;
176
- }
177
- return {
178
- ...(payload as Record<string, unknown>),
179
- consent: { statement: CONTACT_FORM_TERMS_STATEMENT, acceptedAt },
180
- };
181
- }
182
-
183
- /**
184
- * Build a typed RouteLeadInput from the raw request body for a given kind.
185
- * Explicit destructure per kind prevents unknown fields (e.g. attacker-supplied
186
- * `id`) from reaching the service — the service's .strict() Zod schemas provide
187
- * a second layer, but we also keep the handler clean at the boundary. Lifting
188
- * `email`/`name`/`phone` out of `payload` (see liftString above) reads by
189
- * explicit field name only — it does not spread `payload`'s other keys, so
190
- * the same discipline holds. `payload` itself is passed through unchanged.
191
- *
192
- * `siteId` is the RESOLVED `workspaceId` (#6203) — never `body["siteId"]`.
193
- * A body-supplied `siteId` is read nowhere in this function; a client that
194
- * posts one has it silently ignored, never honoured.
195
- */
196
- function buildInput(
197
- kind: LeadKind,
198
- body: Record<string, unknown>,
199
- workspaceId: string,
200
- receivedAt: string,
201
- ): RouteLeadInput {
202
- const payload = (body["payload"] ?? {}) as Record<string, unknown>;
203
-
204
- if (kind === "submit") {
205
- return {
206
- kind,
207
- siteId: workspaceId,
208
- source: body["source"] as string,
209
- email: liftString(body, payload, "email"),
210
- payload: isConsentLeadSource(body["source"])
211
- ? (withConsentRecord(payload, receivedAt) as Record<string, unknown>)
212
- : payload,
213
- };
214
- }
215
- if (kind === "subscribe") {
216
- return {
217
- kind,
218
- siteId: workspaceId,
219
- email: liftString(body, payload, "email"),
220
- source: body["source"] as string,
221
- };
222
- }
223
- return {
224
- kind,
225
- siteId: workspaceId,
226
- name: liftString(body, payload, "name"),
227
- phone: liftString(body, payload, "phone"),
228
- ...(body["preferredTime"] !== undefined && {
229
- preferredTime: body["preferredTime"] as string,
230
- }),
231
- payload,
232
- };
233
- }
234
-
235
- /** A spoke checkout fulfilment's submit (#8757 D14): `source: "web"`, `payload.page: "checkout"`. */
236
- function isCheckoutSubmit(body: Record<string, unknown>): boolean {
237
- const payload = body["payload"];
238
- return (
239
- body["source"] === "web" &&
240
- typeof payload === "object" &&
241
- payload !== null &&
242
- (payload as Record<string, unknown>)["page"] === "checkout"
243
- );
244
- }
245
-
246
- function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
247
- return async (fastify) => {
248
- fastify.post(`/v1/leads/${kind}`, async (request, reply) => {
249
- const body = request.body as Record<string, unknown>;
250
-
251
- // #8730 D1, #9130 — a contact lead or a job application carries the
252
- // visitor's terms acceptance, or it is refused before the tenant read
253
- // and any write.
254
- if (
255
- kind === "submit" &&
256
- isConsentLeadSource(body?.["source"]) &&
257
- body?.["consent"] !== true
258
- ) {
259
- return reply.status(400).send(CONSENT_REQUIRED_BODY);
260
- }
261
- // request.ip became attacker-influenceable when trustProxy: true went
262
- // instance-wide (server.ts, #4730/#4778) — leads/index.ts was never
263
- // itself touched by that PR. Traced every consumer of this value
264
- // (#5112, DEF-004): lead-routing.ts's structured log field and its
265
- // opt-in (LEAD_CAPTURE_INCLUDE_IP) persisted metadata.ip both RECORD
266
- // the value; neither branches routing, persistence, or dedup on it.
267
- // The best-effort/record-only posture is judged sufficient — the same
268
- // trade-off server.ts's own trustProxy comment already accepts. The
269
- // private @working-theory/lead-routing package (not present in this
270
- // repo, not inspectable) is the one consumer this trace could not
271
- // complete; see docs/specs/5112-leads-ip-trace-spec.md §8.
272
- const sourceMeta: SourceMeta = {
273
- ip: request.ip,
274
- userAgent: request.headers["user-agent"],
275
- receivedAt: new Date().toISOString(),
276
- };
277
-
278
- // #7532 D4 — a registered spoke resolves first, to its own workspace; a
279
- // refused key never falls through to the alias below. A keyless lead
280
- // whose hub-status read fails on a cold start proceeds as on a non-hub,
281
- // so routeLead's outage fallback captures it (#7633 amendment).
282
- const caller = await resolveCallerSite(request, {
283
- keylessOnRegistryFailure: "anonymous",
284
- });
285
- if (caller.kind === "refused") {
286
- return reply.status(503).send({
287
- error: "tenant_unresolved",
288
- message: "This site is not configured to accept submissions.",
289
- });
290
- }
291
-
292
- // #6203 — resolve the tenant SERVER-SIDE before any write is
293
- // attempted. Two branches, deliberately different:
294
- //
295
- // - No DATABASE_URL: the zero-config OSS file-fallback deployment.
296
- // There is no `workspaces` table to resolve against (and
297
- // `assertSiteAliasResolves()` already skips its own boot check for
298
- // exactly this reason), so the "resolved" value here is the site
299
- // ALIAS itself (`currentSiteId()`) — the same value the file
300
- // fallback would have carried before this unit. Never a rejection:
301
- // that would break lead capture for every zero-config scaffold.
302
- // - DATABASE_URL set: resolve via `resolveSiteWorkspaceId()`, the
303
- // SAME alias -> workspace resolution `currentSiteId()` already
304
- // drives everywhere else in this app. `null` here is a genuine
305
- // misconfiguration `build()` already refuses to boot past
306
- // (`assertSiteAliasResolves()`) — reaching it at request time is
307
- // the residual case that startup check cannot catch (a workspace
308
- // deleted after boot, a transient read), and it fails closed the
309
- // same way: reject, never guess (spec AC-4).
310
- let workspaceId: string;
311
- if (caller.kind === "spoke") {
312
- workspaceId = caller.workspaceId;
313
- sourceMeta.viaSpoke = true;
314
- } else if (!process.env.DATABASE_URL) {
315
- workspaceId = currentSiteId();
316
- } else {
317
- const resolved = await resolveSiteWorkspaceId();
318
- if (resolved === null) {
319
- request.log.error(
320
- { kind },
321
- "lead capture: DATABASE_URL is set but the site alias resolves to no workspace",
322
- );
323
- return reply.status(503).send({
324
- error: "tenant_unresolved",
325
- message: "This site is not configured to accept submissions.",
326
- });
327
- }
328
- workspaceId = resolved;
329
- }
330
-
331
- // #7869 D1 — after the tenant gate, before any read or write. Node
332
- // lower-cases header names, and joins a duplicated header with ", ",
333
- // which the pattern refuses.
334
- let idempotencyKey: string | undefined;
335
- if (kind === "submit") {
336
- const rawKey = request.headers["idempotency-key"];
337
- if (rawKey !== undefined) {
338
- if (
339
- typeof rawKey !== "string" ||
340
- !IDEMPOTENCY_KEY_PATTERN.test(rawKey)
341
- ) {
342
- return reply.status(400).send({
343
- error: "invalid_input",
344
- message:
345
- "Idempotency-Key must be 16 to 255 visible ASCII characters.",
346
- });
347
- }
348
- idempotencyKey = rawKey;
349
- }
350
- }
351
-
352
- // #8185 D3 — a spoke's same-origin relay marks every forwarded call.
353
- // The mark can only remove the spoke-checkout marker, never grant it.
354
- sourceMeta.relayed = request.headers["x-ailk-relayed"] !== undefined;
355
-
356
- // #8438 D11 — the cookie banner's visitor id, forwarded by the site's
357
- // own server from its first-party cookie. Only a v4 UUID counts; the
358
- // header is never echoed or logged. It links the visitor to the
359
- // lead's Person after the write, and changes nothing about the answer.
360
- const consentVisitorId = consentVisitorFromHeader(
361
- request.headers[CONSENT_VISITOR_HEADER],
362
- );
363
- if (consentVisitorId !== undefined) {
364
- sourceMeta.consentVisitorId = consentVisitorId;
365
- }
366
-
367
- // #8757 D14 — where a buyer's account lives, for the purchase email.
368
- let accountOrigin: string | undefined;
369
- if (kind === "submit" && isCheckoutSubmit(body)) {
370
- const target = await accountTargetFor(caller).catch(() => null);
371
- accountOrigin = target?.origin;
372
- }
373
-
374
- const result = await routeLead(
375
- buildInput(kind, body, workspaceId, sourceMeta.receivedAt),
376
- sourceMeta,
377
- {
378
- ...(idempotencyKey !== undefined && { idempotencyKey }),
379
- ...(accountOrigin !== undefined && { accountOrigin }),
380
- },
381
- );
382
-
383
- if (!result.ok) {
384
- if (result.error.kind === "idempotency_conflict") {
385
- // #7869 D6 — the operator's signal that a keyed lead was refused.
386
- // Never the key, the email or the payload.
387
- request.log.warn(
388
- { kind, siteId: workspaceId, outcome: "idempotency_conflict" },
389
- "lead idempotency_conflict",
390
- );
391
- return reply.status(422).send({
392
- error: "idempotency_key_reused",
393
- message: "This Idempotency-Key was used for a different lead.",
394
- });
395
- }
396
- if (result.error.kind === "invalid_input") {
397
- return reply
398
- .status(400)
399
- .send({ error: "invalid_input", message: result.error.message });
400
- }
401
- if (result.error.kind === "persist_failed") {
402
- // Do not surface internal error detail — log it server-side only
403
- request.log.error(
404
- { kind, siteId: workspaceId, error: result.error.message },
405
- "lead persist_failed",
406
- );
407
- return reply.status(503).send({ error: "persist_failed" });
408
- }
409
- if (result.error.kind === "delivery_failed") {
410
- // Email was the delivery and the provider did not accept it —
411
- // never reported as sent (#6730 D5). Detail stays server-side.
412
- // `{ kind, siteId }` only (spec D5): provider error text can echo
413
- // an address.
414
- request.log.error({ kind, siteId: workspaceId }, "lead delivery_failed");
415
- return reply.status(503).send({
416
- error: "delivery_failed",
417
- message: "The message could not be delivered.",
418
- });
419
- }
420
- // config_missing — this deployment has nowhere to put this kind of
421
- // lead (#6730 D1: production with neither a database nor the email
422
- // vars, or a newsletter signup with no database).
423
- return reply.status(503).send({
424
- error: "lead_capture_not_configured",
425
- message: "Lead capture is not configured.",
426
- });
427
- }
428
-
429
- // PII-safe structured log: only routing metadata, no email/phone/name/payload
430
- const emailKind =
431
- "skipped" in result.email
432
- ? "skipped"
433
- : result.email.ok
434
- ? "ok"
435
- : "error";
436
- request.log.info(
437
- {
438
- siteId: workspaceId,
439
- kind,
440
- ip: sourceMeta.ip,
441
- persisted: result.persisted,
442
- emailKind,
443
- replayed: result.replayed === true,
444
- },
445
- "lead routed",
446
- );
447
-
448
- // Request-and-confirm (#5970): mint the tenant's one-time confirm
449
- // credential and email them the link. Fire-and-forget and `void`-
450
- // returning, so the 201 below stays independent of both Resend and that
451
- // write — the request is already persisted by this point, and a send
452
- // that fails must never turn a captured lead into an error.
453
- //
454
- // Callback only: `submit` and `subscribe` have no confirm step. The
455
- // service itself no-ops (with a logged reason) when the feature is not
456
- // configured, which is the default.
457
- // `result.persisted === "db"` for the same reason `lead-routing.ts`
458
- // consults it (#5970 security review round 3, M1): the credential is
459
- // minted by an update on the `callbacks` row, so a FILE-FALLBACK id
460
- // would match nothing and the operator would be left with no email at
461
- // all. On that path the generic notification is sent instead, which is
462
- // what keeps "exactly one" true in both directions.
463
- // A spoke's callback never takes this path (#7532 D4, SEC-7532-2): the
464
- // link is minted on the deployment's one Stripe account.
465
- if (
466
- kind === "callback" &&
467
- result.persisted === "db" &&
468
- caller.kind !== "spoke"
469
- ) {
470
- const callbackInput = buildInput(
471
- kind,
472
- body,
473
- workspaceId,
474
- sourceMeta.receivedAt,
475
- ) as Extract<RouteLeadInput, { kind: "callback" }>;
476
- sendRequestConfirmLink(result.id, {
477
- name: callbackInput.name,
478
- phone: callbackInput.phone,
479
- ...(callbackInput.preferredTime !== undefined && {
480
- preferredTime: callbackInput.preferredTime,
481
- }),
482
- });
483
- }
484
-
485
- // #8757 D6 — a written `flow:<slug>` submit answers its magnet's
486
- // completion. A read that fails, or outlasts its budget, leaves the 201
487
- // as it was: the lead is saved.
488
- let next: LeadSubmitNext | undefined;
489
- const source = body["source"];
490
- if (
491
- kind === "submit" &&
492
- result.persisted === "db" &&
493
- typeof source === "string" &&
494
- source.startsWith("flow:")
495
- ) {
496
- const slug = source.slice("flow:".length);
497
- try {
498
- const completion = await magnetCompletionWithinBudget(workspaceId, slug, () =>
499
- accountTargetFor(caller),
500
- );
501
- if (completion !== null) {
502
- next = completion.next;
503
- if (completion.next.completion === "invite" && !result.replayed) {
504
- const submitInput = buildInput(
505
- kind,
506
- body,
507
- workspaceId,
508
- sourceMeta.receivedAt,
509
- ) as Extract<RouteLeadInput, { kind: "submit" }>;
510
- const locale = submitInput.payload["locale"];
511
- void sendInviteConfirmation({
512
- workspaceId,
513
- slug,
514
- email: submitInput.email,
515
- locale: typeof locale === "string" ? locale : undefined,
516
- completion,
517
- now: new Date(),
518
- });
519
- }
520
- }
521
- } catch (error) {
522
- request.log.error(
523
- { event: "magnet-completion", error: error instanceof Error ? error.name : "unknown" },
524
- "lead magnet completion read failed",
525
- );
526
- }
527
- }
528
-
529
- // Fire-and-forget: track() never throws; no need to hold the response.
530
- // #7869 D6 — a replay records nothing new: the first write counted it.
531
- if (!result.replayed) {
532
- void track("lead-captured", {
533
- source: body["source"] as string | undefined,
534
- locale: (body["payload"] as Record<string, unknown> | undefined)?.[
535
- "locale"
536
- ] as string | undefined,
537
- });
538
- }
539
-
540
- return reply.status(201).send({
541
- id: result.id,
542
- persisted: result.persisted,
543
- email: result.email,
544
- ...(next === undefined ? {} : { next }),
545
- });
546
- });
547
- };
548
- }
549
-
550
- export const leadSubmitRoute = makeLeadsRoute("submit");
551
- export const leadSubscribeRoute = makeLeadsRoute("subscribe");
552
- export const leadCallbackRoute = makeLeadsRoute("callback");
553
-
554
- /**
555
- * `GET /v1/leads/capture` (#6730 D4) — two booleans and nothing else: never
556
- * an address or which env var is missing. (The pair implies as much of the
557
- * delivery mode as the rendered page already shows.) Registered at
558
- * the top level in `server.ts`, outside `leadsScope`'s strict per-IP limit
559
- * (a web server's single egress IP already spends that on form submissions),
560
- * so only the server-wide unauthenticated ceiling applies.
561
- */
562
- export const leadCaptureStatusRoute: FastifyPluginAsync = async (fastify) => {
563
- fastify.get("/v1/leads/capture", async (_request, reply) => {
564
- const delivery = resolveLeadDelivery();
565
- return reply.header("Cache-Control", "public, max-age=60").send({
566
- ok: true,
567
- data: {
568
- contact: acceptsKind(delivery, "submit"),
569
- newsletter: acceptsKind(delivery, "subscribe"),
570
- },
571
- });
572
- });
573
- };
@@ -1,104 +0,0 @@
1
- /**
2
- * The leads INBOX read (#5678).
3
- *
4
- * Routes:
5
- * GET /v1/leads?cursor=&limit= — the caller's own sites' leads, newest first
6
- *
7
- * Registered INSIDE `server.ts`'s `authScope` (auth only — NOT the inner
8
- * `tenantScope`, which the multi-tenant module owns), beside
9
- * `/v1/waitlist-signups/me` and `/v1/project-listings/me`. Lead capture is a
10
- * never-gate surface at every level, so reading a lead back must be too: this
11
- * route is registered unconditionally, not behind an `isModuleEnabled()` gate.
12
- * Registering everywhere is not the same as RETURNING a row everywhere —
13
- * `services/leads-inbox.ts` § "What it takes for this to return anything"
14
- * names the two deployment preconditions, both of which fail empty, never
15
- * wide.
16
- *
17
- * Identity is `request.userId` ONLY. There is no site, org or workspace
18
- * parameter — the query schema declares `cursor` and `limit` and nothing else
19
- * — so the scope cannot be widened from the wire. `services/leads-inbox.ts`
20
- * resolves it from the caller's own memberships through
21
- * `services/owner-workspaces.ts`'s `resolveOwnerWorkspaces` and filters every
22
- * read on `siteId ∈ workspaceIds`.
23
- *
24
- * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
25
- * `{ ok: false, error: { code, message } }`, real HTTP status codes:
26
- * 400 validation_failed · 401 unauthorized · 500 internal_error.
27
- *
28
- * No response-contract re-parse (the `waitlist-experiments/respond.ts`
29
- * `sendValidated` net): the five fields of a row are assembled one by one in
30
- * `leads-inbox.ts` from five explicitly selected columns, with no
31
- * pass-through of a stored value that could carry an unexpected shape — so
32
- * there is no drift for that net to catch.
33
- *
34
- * API↔MCP parity: identity-bound (the session IS the scope; the OSS MCP
35
- * surface is identity-blind) — no MCP twin (#3591 exception, the same shape as
36
- * `/v1/workspaces/*`, `/v1/billing/*` and `/v1/waitlist-experiments/*`).
37
- */
38
- import { zLeadId } from "@working-theory/ids";
39
- import type { FastifyPluginAsync, FastifyReply } from "fastify";
40
- import { z } from "zod";
41
-
42
- import { listOwnedLeads } from "../../services/leads-inbox.js";
43
-
44
- const DEFAULT_LIMIT = 50;
45
- const MAX_LIMIT = 200;
46
-
47
- const emptyToUndefined = (val: unknown) => (val === "" ? undefined : val);
48
-
49
- /** Boundary validation, mirroring `waitlist-experiments/signups.ts`'s query schema. */
50
- const listQuerySchema = z.object({
51
- cursor: z.preprocess(emptyToUndefined, zLeadId.optional()),
52
- limit: z.preprocess(
53
- emptyToUndefined,
54
- z.coerce.number().int().min(1).max(MAX_LIMIT).optional(),
55
- ),
56
- });
57
-
58
- function unauthorized(reply: FastifyReply) {
59
- return reply.status(401).send({
60
- ok: false,
61
- error: { code: "unauthorized", message: "Authentication required" },
62
- });
63
- }
64
-
65
- function invalidQuery(reply: FastifyReply, message: string) {
66
- return reply.status(400).send({
67
- ok: false,
68
- error: { code: "validation_failed", message },
69
- });
70
- }
71
-
72
- export const leadListRoute: FastifyPluginAsync = async (fastify) => {
73
- fastify.get("/v1/leads", async (request, reply) => {
74
- const userId = request.userId;
75
- if (!userId) return unauthorized(reply);
76
-
77
- const query = listQuerySchema.safeParse(request.query);
78
- if (!query.success) return invalidQuery(reply, "Invalid query parameters");
79
-
80
- const result = await listOwnedLeads({
81
- userId,
82
- cursor: query.data.cursor,
83
- limit: query.data.limit ?? DEFAULT_LIMIT,
84
- });
85
-
86
- if (!result.ok) {
87
- if (result.error.kind === "invalid_cursor") {
88
- return invalidQuery(reply, result.error.message);
89
- }
90
- // Do not surface internal error detail — log it server-side only, and
91
- // with no lead field in the log line (the leads PII boundary).
92
- request.log.error(
93
- { route: "leads/list", error: result.error.message },
94
- "leads list failed",
95
- );
96
- return reply.status(500).send({
97
- ok: false,
98
- error: { code: "internal_error", message: "Failed to read leads" },
99
- });
100
- }
101
-
102
- return reply.status(200).send({ ok: true, data: result.value });
103
- });
104
- };