create-ailk 0.2.0 → 0.3.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 (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. package/templates/apps/web/source.config.ts +0 -78
@@ -9,53 +9,50 @@
9
9
  * - Vercel serverless deployment (api/index.ts)
10
10
  * - Local development (src/index.ts)
11
11
  */
12
- import cors from '@fastify/cors';
13
- import rateLimit from '@fastify/rate-limit';
14
- import Fastify from 'fastify';
15
- import type { FastifyPluginAsync, FastifyPluginCallback } from 'fastify';
12
+ import cors from "@fastify/cors";
13
+ import rateLimit from "@fastify/rate-limit";
14
+ import Fastify from "fastify";
15
+ import type { FastifyPluginAsync, FastifyPluginCallback } from "fastify";
16
16
 
17
- import { config } from './config/index.js';
18
- import { isModuleEnabled } from './config/modules.js';
19
- import { registerErrorSerializer } from './lib/error-serializer.js';
20
- import { validateLeadCaptureDir } from './lib/lead-capture-store.js';
21
- import { assertSandboxKey } from './lib/stripe.js';
22
- import { authPreHandlerPlugin } from './middleware/auth.js';
17
+ import { config } from "./config/index.js";
18
+ import { isModuleEnabled } from "./config/modules.js";
19
+ import { registerErrorSerializer } from "./lib/error-serializer.js";
20
+ import { validateLeadCaptureDir } from "./lib/lead-capture-store.js";
21
+ import { loadSlice } from "./lib/slice-load.js";
22
+ import { authPreHandlerPlugin } from "./middleware/auth.js";
23
23
  import {
24
24
  rateLimitMiddleware,
25
25
  DEFAULT_PER_IP_PER_MINUTE_STRICT,
26
- } from './middleware/rate-limit.js';
27
- import { openapiRoutes } from './openapi/index.js';
28
- import { exampleAeoRoute, scoreAeoRoute } from './routes/aeo/index.js';
29
- import { tokenRoutes } from './routes/auth/tokens.js';
30
- import { userRoutes } from './routes/auth/user.js';
31
- import { checkoutRoutes } from './routes/checkout/index.js';
32
- import { getPageRoute } from './routes/content/get-page.js';
33
- import { listPagesRoute } from './routes/content/list-pages.js';
34
- import { listTypesRoute } from './routes/content/list-types.js';
26
+ } from "./middleware/rate-limit.js";
27
+ import { openapiRoutes } from "./openapi/index.js";
28
+ import {
29
+ deriveRootEndpoints,
30
+ REGISTERED_ROUTES_DECORATOR,
31
+ API_VERSION,
32
+ apiDescription,
33
+ apiDisplayName,
34
+ resolveApiIdentity,
35
+ } from "./openapi/spec.js";
36
+ import type { RegisteredRoute } from "./openapi/surface-drift.js";
37
+ import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
38
+ import { tokenRoutes } from "./routes/auth/tokens.js";
39
+ import { userRoutes } from "./routes/auth/user.js";
40
+ import { getPageRoute } from "./routes/content/get-page.js";
41
+ import { listPagesRoute } from "./routes/content/list-pages.js";
42
+ import { listTypesRoute } from "./routes/content/list-types.js";
35
43
  import {
36
44
  CONTENT_RATE_LIMIT_UNAUTH,
37
45
  CONTENT_RATE_LIMIT_WINDOW,
38
- } from './routes/content/rate-limit.js';
39
- import { deliverableDownloadRoute } from './routes/deliverables/index.js';
40
- import { flowCheckoutRoutes } from './routes/flow-checkouts/index.js';
41
- import { getFlowAggregateRoute, getFlowRoute, submitFlowStepRoute } from './routes/flows/index.js';
42
- import { checkoutRecommendationRoute, saveRecommendationRoute } from './routes/flows/recommender.js';
46
+ } from "./routes/content/rate-limit.js";
43
47
  import {
44
48
  leadCallbackRoute,
45
49
  leadSubmitRoute,
46
50
  leadSubscribeRoute,
47
- } from './routes/leads/index.js';
51
+ } from "./routes/leads/index.js";
48
52
  import {
49
- projectListingDraftPatchRoute,
50
- projectListingMeRoute,
51
- projectListingPublicRoute,
52
- projectListingStartRoute,
53
- projectListingSubmitRoute,
54
- } from './routes/project-listings/index.js';
55
- import { slackActionsRoute } from './routes/slack/index.js';
56
- import { registerTestAuthRoute, shouldMountTestAuth } from './routes/test/auth/session.js';
57
- import { waitlistSignupRoutes } from './routes/waitlist-signups/index.js';
58
- import { webhooksRoutes } from './routes/webhooks/index.js';
53
+ registerTestAuthRoute,
54
+ shouldMountTestAuth,
55
+ } from "./routes/test/auth/session.js";
59
56
 
60
57
  /**
61
58
  * Paid-module SLICE entry points, keyed by the export each branch needs
@@ -70,9 +67,15 @@ import { webhooksRoutes } from './routes/webhooks/index.js';
70
67
  * `./middleware/tenant.js` import was an eleventh — measured on a stripped
71
68
  * tree, not argued.
72
69
  *
73
- * Reading the specifier from this table is what makes it non-literal at the
74
- * call site, and that INDIRECTION — not the type of the value — is what defeats
75
- * resolution. S1's `isModuleEnabled()` gate is unchanged and still decides
70
+ * Keeping the specifier off the `import()` call site is what defeats that
71
+ * resolution, and the INDIRECTION — not the type of the value — is what does
72
+ * the work. WHICH FORM of indirection is not free, and #5304 is the correction:
73
+ * reading the specifier out of a TABLE, as this file did until then, hid it
74
+ * from the deploy-time file tracer as well as from `tsc`, so the files were
75
+ * compiled and never shipped. The form below — a module-level constant
76
+ * dereferenced into `import()` — is unresolvable to `tsc` and traceable to
77
+ * `@vercel/nft`, the only pair of properties that satisfies both the strip and
78
+ * the deployment. S1's `isModuleEnabled()` gate is unchanged and still decides
76
79
  * whether the branch runs; `AILK_ENABLED_MODULES` unset still means every
77
80
  * module on, so a default build registers exactly what it registered before.
78
81
  *
@@ -82,27 +85,200 @@ import { webhooksRoutes } from './routes/webhooks/index.js';
82
85
  * with no members, so the module silently fails to load on the tree that HAS
83
86
  * it. See the note above the imports in `packages/ui/src/sections/SectionRenderer.tsx`.
84
87
  *
85
- * `./routes/project-listings/index.js` is deliberately absent: that barrel is a
86
- * MIXED directory that ships (the free anonymous listing routes are registered
87
- * from it statically at the top of this file), so its dynamic import at the
88
- * tenant scope is a literal on purpose and resolves fine on both trees.
88
+ * BUNDLING `apps/api` IS NOT A SUPPORTED DEPLOY PATH, and that is a property of
89
+ * the analysers rather than a posture (#5334). esbuild bundles a dynamic
90
+ * `import()` only when its argument is a string literal at the call site
91
+ * exactly the predicate `tsc` resolves on, and exactly the one the strip
92
+ * forbids. So no expression shape can satisfy both, and in a bundled build
93
+ * these specifiers survive verbatim and resolve against the BUNDLE OUTPUT
94
+ * directory instead of their source module's: `"./list.js"` in
95
+ * `routes/project-listings/index.ts` becomes `dist/list.js` and is not there.
96
+ * esbuild does not warn. `build()` then throws during CONSTRUCTION, so every
97
+ * route 500s rather than one — measured in production, nine hours of total API
98
+ * outage on a consuming site. The supported path is what the scaffold ships:
99
+ * plain `tsc` output traced by `@vercel/node` (`apps/api/vercel.json`,
100
+ * `framework: null`). If you are syncing this file into a repo that bundles,
101
+ * that is the constraint you have just inherited; `apps/api/CLAUDE.md`
102
+ * § Deploy path says what to do instead. Every load site below routes through
103
+ * `loadSlice()` so a bundled build says so at boot rather than failing mute.
104
+ *
105
+ * `./routes/project-listings/index.js` carries two independent load paths on
106
+ * purpose: its five anonymous-surface exports are reached below through
107
+ * `PROJECT_LISTINGS_INDEX` / `SLICE_PLUGINS`, now that signal-collectors gates
108
+ * their registration (D4), while the tenant scope's own `import()` of the same
109
+ * specifier stays a literal — the file is a MIXED directory that always ships
110
+ * (multi-tenant's `TENANT_HANDLERS` table lives in the same file), so a literal
111
+ * there resolves fine on every tree regardless of module selection.
112
+ */
113
+ /**
114
+ * One module-level constant per slice module. The call sites below read the
115
+ * loader out of `SLICE_PLUGINS`; the specifier itself is never written at an
116
+ * `import()` call site, which is what keeps `tsc` from resolving it (#5182 D1).
117
+ *
118
+ * The constant is not a style choice — it is the mechanism (#5304 D3).
119
+ * `@vercel/nft` constant-propagates a module-level binding into `import()` and
120
+ * emits the file; it cannot evaluate a table lookup or a for-of destructure,
121
+ * which is what the previous `Record<string, string>` shape forced it to do.
122
+ * So the old shape was invisible to the DEPLOY tracer as well as to `tsc`, the
123
+ * files went untraced and unshipped, and `build()` threw ERR_MODULE_NOT_FOUND
124
+ * on the first slice it reached — measured in production, nine hours of total
125
+ * API outage on a consuming site. Both analysers are re-measured on every run
126
+ * by `scripts/__tests__/enumerate-slice-specifiers.test.ts`, and
127
+ * `scripts/check-slice-reachability.ts` fails when any specifier here stops
128
+ * being traced.
89
129
  */
90
- const SLICE_PLUGIN_SPECIFIERS = {
91
- workspaceContextRoutes: './routes/workspaces/index.js',
92
- workspaceSessionRoutes: './routes/workspaces/index.js',
93
- sessionRoutes: './routes/workspaces/sessions.js',
94
- workspaceConfigRoutes: './routes/workspaces/config.js',
95
- billingRoutes: './routes/billing/index.js',
96
- waitlistExperimentRoutes: './routes/waitlist-experiments/index.js',
97
- createPageRoute: './routes/content/create.js',
98
- updatePageRoute: './routes/content/update.js',
99
- deletePageRoute: './routes/content/delete.js',
100
- listAvailabilityRoute: './routes/schedule/index.js',
101
- getEventMetaRoute: './routes/schedule/index.js',
102
- createBookingRoute: './routes/schedule/index.js',
103
- } satisfies Record<string, string>;
130
+ const WORKSPACES_INDEX = "./routes/workspaces/index.js";
131
+ const WORKSPACES_SESSIONS = "./routes/workspaces/sessions.js";
132
+ const WORKSPACES_CONFIG = "./routes/workspaces/config.js";
133
+ const BILLING_INDEX = "./routes/billing/index.js";
134
+ const WAITLIST_EXPERIMENTS_INDEX = "./routes/waitlist-experiments/index.js";
135
+ const CONTENT_CREATE = "./routes/content/create.js";
136
+ const CONTENT_UPDATE = "./routes/content/update.js";
137
+ const CONTENT_DELETE = "./routes/content/delete.js";
138
+ const SCHEDULE_INDEX = "./routes/schedule/index.js";
139
+ const FLOWS_INDEX = "./routes/flows/index.js";
140
+ const DELIVERABLES_INDEX = "./routes/deliverables/index.js";
141
+ const PROJECT_LISTINGS_INDEX = "./routes/project-listings/index.js";
142
+ const SLACK_INDEX = "./routes/slack/index.js";
143
+ const WAITLIST_SIGNUPS_INDEX = "./routes/waitlist-signups/index.js";
144
+ const CHECKOUT_INDEX = "./routes/checkout/index.js";
145
+ const WEBHOOKS_INDEX = "./routes/webhooks/index.js";
146
+ const FLOW_CHECKOUTS_INDEX = "./routes/flow-checkouts/index.js";
147
+ const RECOMMENDER_INDEX = "./routes/flows/recommender.js";
104
148
 
105
- type SlicePluginName = keyof typeof SLICE_PLUGIN_SPECIFIERS;
149
+ /**
150
+ * Paid-module SLICE entry points, keyed by the export each branch needs.
151
+ *
152
+ * `specifier` is carried alongside `load` for the drift message below, and both
153
+ * read the SAME constant, so the two can never disagree. `load` is what runs:
154
+ * an arrow closing over that constant, which is the only shape both analysers
155
+ * treat as intended — unresolvable to `tsc`, traceable to `nft`.
156
+ */
157
+ const SLICE_PLUGINS = {
158
+ workspaceContextRoutes: {
159
+ specifier: WORKSPACES_INDEX,
160
+ load: () => import(WORKSPACES_INDEX),
161
+ },
162
+ workspaceSessionRoutes: {
163
+ specifier: WORKSPACES_INDEX,
164
+ load: () => import(WORKSPACES_INDEX),
165
+ },
166
+ sessionRoutes: {
167
+ specifier: WORKSPACES_SESSIONS,
168
+ load: () => import(WORKSPACES_SESSIONS),
169
+ },
170
+ workspaceConfigRoutes: {
171
+ specifier: WORKSPACES_CONFIG,
172
+ load: () => import(WORKSPACES_CONFIG),
173
+ },
174
+ billingRoutes: {
175
+ specifier: BILLING_INDEX,
176
+ load: () => import(BILLING_INDEX),
177
+ },
178
+ waitlistExperimentRoutes: {
179
+ specifier: WAITLIST_EXPERIMENTS_INDEX,
180
+ load: () => import(WAITLIST_EXPERIMENTS_INDEX),
181
+ },
182
+ createPageRoute: {
183
+ specifier: CONTENT_CREATE,
184
+ load: () => import(CONTENT_CREATE),
185
+ },
186
+ updatePageRoute: {
187
+ specifier: CONTENT_UPDATE,
188
+ load: () => import(CONTENT_UPDATE),
189
+ },
190
+ deletePageRoute: {
191
+ specifier: CONTENT_DELETE,
192
+ load: () => import(CONTENT_DELETE),
193
+ },
194
+ listAvailabilityRoute: {
195
+ specifier: SCHEDULE_INDEX,
196
+ load: () => import(SCHEDULE_INDEX),
197
+ },
198
+ getEventMetaRoute: {
199
+ specifier: SCHEDULE_INDEX,
200
+ load: () => import(SCHEDULE_INDEX),
201
+ },
202
+ createBookingRoute: {
203
+ specifier: SCHEDULE_INDEX,
204
+ load: () => import(SCHEDULE_INDEX),
205
+ },
206
+ getFlowRoute: {
207
+ specifier: FLOWS_INDEX,
208
+ load: () => import(FLOWS_INDEX),
209
+ },
210
+ submitFlowStepRoute: {
211
+ specifier: FLOWS_INDEX,
212
+ load: () => import(FLOWS_INDEX),
213
+ },
214
+ getFlowAggregateRoute: {
215
+ specifier: FLOWS_INDEX,
216
+ load: () => import(FLOWS_INDEX),
217
+ },
218
+ deliverableDownloadRoute: {
219
+ specifier: DELIVERABLES_INDEX,
220
+ load: () => import(DELIVERABLES_INDEX),
221
+ },
222
+ projectListingStartRoute: {
223
+ specifier: PROJECT_LISTINGS_INDEX,
224
+ load: () => import(PROJECT_LISTINGS_INDEX),
225
+ },
226
+ projectListingDraftPatchRoute: {
227
+ specifier: PROJECT_LISTINGS_INDEX,
228
+ load: () => import(PROJECT_LISTINGS_INDEX),
229
+ },
230
+ projectListingSubmitRoute: {
231
+ specifier: PROJECT_LISTINGS_INDEX,
232
+ load: () => import(PROJECT_LISTINGS_INDEX),
233
+ },
234
+ // #5407 — the resume link's verification write. Anonymous surface,
235
+ // module-conditional (signal-collectors, D-48) like its four siblings.
236
+ projectListingVerifyEmailRoute: {
237
+ specifier: PROJECT_LISTINGS_INDEX,
238
+ load: () => import(PROJECT_LISTINGS_INDEX),
239
+ },
240
+ projectListingPublicRoute: {
241
+ specifier: PROJECT_LISTINGS_INDEX,
242
+ load: () => import(PROJECT_LISTINGS_INDEX),
243
+ },
244
+ projectListingMeRoute: {
245
+ specifier: PROJECT_LISTINGS_INDEX,
246
+ load: () => import(PROJECT_LISTINGS_INDEX),
247
+ },
248
+ slackActionsRoute: {
249
+ specifier: SLACK_INDEX,
250
+ load: () => import(SLACK_INDEX),
251
+ },
252
+ waitlistSignupRoutes: {
253
+ specifier: WAITLIST_SIGNUPS_INDEX,
254
+ load: () => import(WAITLIST_SIGNUPS_INDEX),
255
+ },
256
+ // Commerce (D-47, #5364) — checkout, the Stripe webhook receiver, flow
257
+ // checkouts and the recommender. Module-conditional (isModuleEnabled
258
+ // "commerce") like every other paid slice above.
259
+ checkoutRoutes: {
260
+ specifier: CHECKOUT_INDEX,
261
+ load: () => import(CHECKOUT_INDEX),
262
+ },
263
+ webhooksRoutes: {
264
+ specifier: WEBHOOKS_INDEX,
265
+ load: () => import(WEBHOOKS_INDEX),
266
+ },
267
+ flowCheckoutRoutes: {
268
+ specifier: FLOW_CHECKOUTS_INDEX,
269
+ load: () => import(FLOW_CHECKOUTS_INDEX),
270
+ },
271
+ saveRecommendationRoute: {
272
+ specifier: RECOMMENDER_INDEX,
273
+ load: () => import(RECOMMENDER_INDEX),
274
+ },
275
+ checkoutRecommendationRoute: {
276
+ specifier: RECOMMENDER_INDEX,
277
+ load: () => import(RECOMMENDER_INDEX),
278
+ },
279
+ } satisfies Record<string, { specifier: string; load: () => Promise<unknown> }>;
280
+
281
+ type SlicePluginName = keyof typeof SLICE_PLUGINS;
106
282
 
107
283
  /**
108
284
  * Load one slice route plugin. Every entry above is a `FastifyPluginAsync`
@@ -117,8 +293,8 @@ type SlicePluginName = keyof typeof SLICE_PLUGIN_SPECIFIERS;
117
293
  async function sliceRoutePlugin(
118
294
  name: SlicePluginName,
119
295
  ): Promise<FastifyPluginAsync> {
120
- const specifier: string = SLICE_PLUGIN_SPECIFIERS[name];
121
- const mod = (await import(specifier)) as Record<
296
+ const { specifier, load } = SLICE_PLUGINS[name];
297
+ const mod = (await loadSlice(specifier, load)) as Record<
122
298
  string,
123
299
  FastifyPluginAsync | undefined
124
300
  >;
@@ -135,17 +311,40 @@ async function sliceRoutePlugin(
135
311
  * (apps/api/src/middleware/tenant.ts:83).
136
312
  */
137
313
  async function tenantPreHandlerSlicePlugin(): Promise<FastifyPluginCallback> {
138
- const specifier: string = './middleware/tenant.js';
139
- const mod = (await import(specifier)) as {
314
+ const specifier: string = "./middleware/tenant.js";
315
+ const mod = (await loadSlice(specifier, () => import(specifier))) as {
140
316
  tenantPreHandlerPlugin?: FastifyPluginCallback;
141
317
  };
142
318
  const plugin = mod.tenantPreHandlerPlugin;
143
319
  if (!plugin) {
144
- throw new Error('slice export tenantPreHandlerPlugin missing from ' + specifier);
320
+ throw new Error(
321
+ "slice export tenantPreHandlerPlugin missing from " + specifier,
322
+ );
145
323
  }
146
324
  return plugin;
147
325
  }
148
326
 
327
+ /**
328
+ * The commerce boot-time sandbox guard — the one other slice entry that is
329
+ * not a route plugin, mirroring `tenantPreHandlerSlicePlugin()` above (D3).
330
+ * `lib/stripe.ts` is commerce-owned (D5) and its `assertSandboxKey` is the
331
+ * sole `apps/api` reach into the Stripe SDK wrapper package, so it can no
332
+ * longer be a static import (spec D1) once the module is stripped. The
333
+ * widened `const specifier: string` annotation is what defeats `tsc`
334
+ * resolution, exactly as it does above.
335
+ */
336
+ async function assertCommerceSandboxKey(key: string): Promise<void> {
337
+ const specifier: string = "./lib/stripe.js";
338
+ const mod = (await loadSlice(specifier, () => import(specifier))) as {
339
+ assertSandboxKey?: (key: string) => void;
340
+ };
341
+ const assertSandboxKeyFn = mod.assertSandboxKey;
342
+ if (!assertSandboxKeyFn) {
343
+ throw new Error("slice export assertSandboxKey missing from " + specifier);
344
+ }
345
+ assertSandboxKeyFn(key);
346
+ }
347
+
149
348
  /**
150
349
  * Build Fastify server instance (for serverless or standalone)
151
350
  * @returns Configured Fastify instance ready for requests
@@ -156,9 +355,11 @@ export async function build() {
156
355
 
157
356
  // Runtime sandbox-mode guard: reject live-mode Stripe keys before any
158
357
  // webhook or checkout call can exercise them. Idempotent — safe even if
159
- // assertSandboxKey is called again inside a route handler.
160
- if (config.STRIPE_SECRET_KEY) {
161
- assertSandboxKey(config.STRIPE_SECRET_KEY);
358
+ // assertSandboxKey is called again inside a route handler. Module-
359
+ // conditional (commerce, D-47/D3): a build without commerce has no
360
+ // commerce request path to guard, and lib/stripe.ts is stripped with it.
361
+ if (isModuleEnabled("commerce") && config.STRIPE_SECRET_KEY) {
362
+ await assertCommerceSandboxKey(config.STRIPE_SECRET_KEY);
162
363
  }
163
364
 
164
365
  // trustProxy: true (#4730) — the Vercel serverless handler
@@ -188,6 +389,32 @@ export async function build() {
188
389
  trustProxy: true,
189
390
  });
190
391
 
392
+ // Collect the route table this server registers (#5314).
393
+ //
394
+ // Installed here — after `Fastify()` and before the first `register()` — so
395
+ // no route can precede it: a root-instance `onRoute` hook fires for every
396
+ // route added afterwards in ANY encapsulated child scope, which is what lets
397
+ // one hook see the auth, tenant, webhooks, leads, flows, aeo and schedule
398
+ // scopes without touching any of them.
399
+ //
400
+ // HEAD is dropped: Fastify synthesises it from every GET, so publishing it
401
+ // would describe routes no handler declared.
402
+ //
403
+ // The table lives on THIS instance, not in module state, so two servers
404
+ // built concurrently in one process cannot corrupt each other's — see the
405
+ // note on `readRegisteredRoutes` in openapi/spec.ts for the concrete race.
406
+ // The decorator holds the array by reference and the hook pushes into it, so
407
+ // every reader sees the current contents without re-publishing.
408
+ const registeredRoutes: RegisteredRoute[] = [];
409
+ server.decorate(REGISTERED_ROUTES_DECORATOR, registeredRoutes);
410
+ server.addHook("onRoute", (route) => {
411
+ const methods = Array.isArray(route.method) ? route.method : [route.method];
412
+ for (const method of methods) {
413
+ if (method === "HEAD") continue;
414
+ registeredRoutes.push({ method, url: route.url });
415
+ }
416
+ });
417
+
191
418
  // Register error serializer — maps internal exceptions to generic strings
192
419
  // so connection strings, hostnames, stack traces, and SDK error details
193
420
  // never reach the client. Full detail still goes to pino at error level.
@@ -199,9 +426,32 @@ export async function build() {
199
426
  // `credentials: true` would let any website read credentialed responses
200
427
  // (security review M1). `false` disables cross-origin access entirely;
201
428
  // same-origin and non-browser callers are unaffected.
429
+ //
430
+ // `methods` is EXPLICIT because @fastify/cors defaults it to `GET,HEAD,POST`
431
+ // (its own `defaultOptions`), which is narrower than the surface this server
432
+ // registers (#5397). With only `origin` + `credentials` passed, the preflight
433
+ // for `PATCH /v1/project-listings/drafts/:id` answered
434
+ // `access-control-allow-methods: GET, HEAD, POST`, so a browser refused every
435
+ // draft save on a split-origin deploy — a site could create a listing draft
436
+ // and never save one. The list below is the union of the methods the route
437
+ // table actually serves (GET/POST/PATCH/PUT/DELETE — `/v1/auth/tokens/:id`,
438
+ // `/v1/content/:type/:slug`, `/v1/user/data/:key`, the workspace-config PUTs),
439
+ // plus HEAD (Fastify synthesises it from every GET) and OPTIONS (the plugin's
440
+ // own wildcard preflight route). `cors.test.ts` re-derives that union from the
441
+ // live route table on every run, so a route added in a NEW method fails there
442
+ // rather than silently becoming unreachable from a browser.
443
+ //
444
+ // This widens WHICH METHODS the allowed origin may use, never WHO is allowed:
445
+ // `origin` and `credentials` are untouched.
446
+ //
447
+ // `allowedHeaders` is deliberately left unset: null means @fastify/cors
448
+ // reflects the request's `Access-Control-Request-Headers`, so `authorization`
449
+ // and `content-type` pass without this file having to maintain a list that
450
+ // silently drops a header a future route needs.
202
451
  await server.register(cors, {
203
- origin: config.FRONTEND_URL || config.NODE_ENV !== 'production',
452
+ origin: config.FRONTEND_URL || config.NODE_ENV !== "production",
204
453
  credentials: true,
454
+ methods: ["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
205
455
  });
206
456
 
207
457
  // Register rate limiting — applied globally with the unauth ceiling. The
@@ -214,32 +464,35 @@ export async function build() {
214
464
  });
215
465
 
216
466
  // Health check
217
- server.get('/health', async () => ({
218
- status: 'healthy',
467
+ server.get("/health", async () => ({
468
+ status: "healthy",
219
469
  timestamp: new Date().toISOString(),
220
470
  }));
221
471
 
222
- // Root endpoint
223
- server.get('/', async () => {
472
+ // Root endpoint — introduces this deployment as the SITE it serves, not as
473
+ // the kit it was built with (#5307). `_site.mdx` is read once per process;
474
+ // a site without one degrades to a neutral name, never to the kit's.
475
+ server.get("/", async () => {
476
+ const identity = resolveApiIdentity();
224
477
  return {
225
- name: 'AI Launch Kit API',
226
- version: '1.0.0',
227
- description: 'Local auth + session backend + public content API',
228
- endpoints: {
229
- health: 'GET /health',
230
- openapi: 'GET /v1/openapi.json',
231
- // Auth (local)
232
- tokens: 'POST/GET/DELETE /v1/auth/tokens',
233
- profile: 'GET/PATCH /v1/user/profile',
234
- userData: 'GET/PUT/DELETE /v1/user/data/:key',
235
- // Public content (read-only)
236
- contentTypes: 'GET /v1/content/types',
237
- contentList: 'GET /v1/content/:type',
238
- contentPage: 'GET /v1/content/:type/:slug',
239
- checkoutSessions: 'POST /v1/checkout/sessions',
240
- billing: 'GET /v1/billing',
241
- billingPortalSession: 'POST /v1/billing/portal-session',
242
- },
478
+ name: apiDisplayName(identity),
479
+ version: API_VERSION,
480
+ description: apiDescription(identity),
481
+ // Derived from the routes this process actually registered (#5314),
482
+ // not a hand-maintained literal. The literal listed 11 endpoints against
483
+ // 68 registered routes, omitted `/v1/project-listings/public` and
484
+ // `/v1/billing/usage`, advertised `DELETE /v1/auth/tokens` at a path
485
+ // that 404s (the route is `/v1/auth/tokens/:id`), and named two billing
486
+ // routes on builds where the multi-tenant module registers neither.
487
+ // Same mechanism now covers commerce (D-47, #5364): checkout, the
488
+ // Stripe webhook, the flow-checkouts read and the two recommender
489
+ // routes are derived exactly like the billing routes above and
490
+ // disappear from this list on a build where `commerce` is off, rather
491
+ // than advertising a route that 404s.
492
+ //
493
+ // Read at REQUEST time, so `ready()` has fired and the set is complete
494
+ // even though this handler is registered before most routes.
495
+ endpoints: deriveRootEndpoints(registeredRoutes),
243
496
  };
244
497
  });
245
498
 
@@ -258,20 +511,30 @@ export async function build() {
258
511
  // apply to them; each enforces membership in the service layer.
259
512
  // Module-conditional (S1, #5088): without the multi-tenant module there
260
513
  // is no workspace concept to switch or inspect.
261
- if (isModuleEnabled('multi-tenant')) {
262
- await authScope.register(await sliceRoutePlugin('workspaceContextRoutes'));
514
+ if (isModuleEnabled("multi-tenant")) {
515
+ await authScope.register(
516
+ await sliceRoutePlugin("workspaceContextRoutes"),
517
+ );
263
518
  }
264
519
 
265
520
  // Respondent signup read (#4731) — auth only, NOT tenant-guarded: a
266
521
  // respondent has no workspace membership and the tenant guard is
267
522
  // default-deny. Identity is request.userId; the service re-checks
268
- // User.emailVerified before any Lead read.
269
- await authScope.register(waitlistSignupRoutes);
523
+ // User.emailVerified before any Lead read. Module-conditional
524
+ // (signal-collectors, D-48): the waitlist instrument's respondent half.
525
+ if (isModuleEnabled("signal-collectors")) {
526
+ await authScope.register(await sliceRoutePlugin("waitlistSignupRoutes"));
527
+ }
270
528
 
271
529
  // Respondent checkouts read (#4984) — the same posture as the signup
272
530
  // read above: auth only, NOT tenant-guarded. Identity is request.userId;
273
531
  // the only join is the lead-promotion link (never an email match).
274
- await authScope.register(flowCheckoutRoutes);
532
+ // Module-conditional (commerce, D-47): the `/account` Purchases panel
533
+ // already degrades to its empty state with no code change when this is
534
+ // absent (`SKIPPED_STATUSES` covers 404 — spec §4.2).
535
+ if (isModuleEnabled("commerce")) {
536
+ await authScope.register(await sliceRoutePlugin("flowCheckoutRoutes"));
537
+ }
275
538
 
276
539
  // The founder's OWN listings read (#5295) — the same posture again: auth
277
540
  // only, NOT tenant-guarded, because a founder is not a workspace member
@@ -284,7 +547,12 @@ export async function build() {
284
547
  // build without the module; the static `/me` path takes router precedence
285
548
  // over the tenant scope's parametric `/v1/project-listings/:id`, exactly as
286
549
  // the already-shipped static `/v1/project-listings/public` does.
287
- await authScope.register(projectListingMeRoute);
550
+ // Module-conditional (signal-collectors, D-48 / D4): the listing
551
+ // instrument's founder-read half; not severable from the four anonymous
552
+ // listing routes below (same static barrel, D4).
553
+ if (isModuleEnabled("signal-collectors")) {
554
+ await authScope.register(await sliceRoutePlugin("projectListingMeRoute"));
555
+ }
288
556
 
289
557
  // Tenant-guarded domain routes (multi-tenant module) — the tenant
290
558
  // preHandler (default-deny guard, see middleware/tenant.ts) runs after
@@ -293,24 +561,27 @@ export async function build() {
293
561
  // no tenant concept at all — the whole scope is module-conditional
294
562
  // (S1, #5088), so an OSS build that excludes multi-tenant registers
295
563
  // none of it.
296
- if (isModuleEnabled('multi-tenant')) {
564
+ if (isModuleEnabled("multi-tenant")) {
297
565
  await authScope.register(async (tenantScope) => {
298
566
  await tenantScope.register(await tenantPreHandlerSlicePlugin());
299
- await tenantScope.register(await sliceRoutePlugin('workspaceSessionRoutes'));
300
- await tenantScope.register(await sliceRoutePlugin('sessionRoutes'));
301
- await tenantScope.register(await sliceRoutePlugin('workspaceConfigRoutes'));
567
+ await tenantScope.register(
568
+ await sliceRoutePlugin("workspaceSessionRoutes"),
569
+ );
570
+ await tenantScope.register(await sliceRoutePlugin("sessionRoutes"));
571
+ await tenantScope.register(
572
+ await sliceRoutePlugin("workspaceConfigRoutes"),
573
+ );
302
574
  // Org-billing routes (#3701) — org-scoped read + Stripe customer-portal.
303
- await tenantScope.register(await sliceRoutePlugin('billingRoutes'));
575
+ await tenantScope.register(await sliceRoutePlugin("billingRoutes"));
304
576
  // Waitlist operator-dashboard read API (#4665 D7, B0) — tenant-scoped
305
577
  // list/comparison/signups reads over A1's waitlist-experiment rows.
306
578
  await tenantScope.register(
307
- await sliceRoutePlugin('waitlistExperimentRoutes'),
579
+ await sliceRoutePlugin("waitlistExperimentRoutes"),
308
580
  );
309
581
  // Project-listings dashboard (#4998) — tenant-scoped list/get/patch
310
582
  // over the founder's-application-row lifecycle.
311
- const { projectListingTenantRoutes } = await import(
312
- './routes/project-listings/index.js'
313
- );
583
+ const { projectListingTenantRoutes } =
584
+ await import("./routes/project-listings/index.js");
314
585
  await tenantScope.register(projectListingTenantRoutes);
315
586
  });
316
587
  }
@@ -325,22 +596,28 @@ export async function build() {
325
596
  // module-conditional at REGISTRATION too (S1, #5088) so an OSS build that
326
597
  // excludes the content-write slice never wires the handler in at all. Not
327
598
  // a MODULE_SLICES entry — see config/modules.ts.
328
- if (isModuleEnabled('content-write')) {
329
- await server.register(await sliceRoutePlugin('createPageRoute'));
330
- await server.register(await sliceRoutePlugin('updatePageRoute'));
331
- await server.register(await sliceRoutePlugin('deletePageRoute'));
599
+ if (isModuleEnabled("content-write")) {
600
+ await server.register(await sliceRoutePlugin("createPageRoute"));
601
+ await server.register(await sliceRoutePlugin("updatePageRoute"));
602
+ await server.register(await sliceRoutePlugin("deletePageRoute"));
603
+ }
604
+ // Module-conditional (commerce, D-47): checkout moved to its own module.
605
+ if (isModuleEnabled("commerce")) {
606
+ await server.register(await sliceRoutePlugin("checkoutRoutes"));
332
607
  }
333
- await server.register(checkoutRoutes);
334
608
  // Deliverable download — a capability-token URL (#3961 D5); the token
335
- // itself is the sole gate, so no auth preHandler applies.
336
- await server.register(deliverableDownloadRoute);
609
+ // itself is the sole gate, so no auth preHandler applies. Module-conditional
610
+ // (signal-collectors, D-48): gated PDFs and reports.
611
+ if (isModuleEnabled("signal-collectors")) {
612
+ await server.register(await sliceRoutePlugin("deliverableDownloadRoute"));
613
+ }
337
614
 
338
615
  // Webhooks routes — raw-body parser scoped to this sub-router only so the
339
616
  // global JSON parser on all other routes is unaffected.
340
617
  await server.register(async (webhooksScope) => {
341
618
  webhooksScope.addContentTypeParser(
342
- 'application/json',
343
- { parseAs: 'buffer' },
619
+ "application/json",
620
+ { parseAs: "buffer" },
344
621
  (_req, body, done) => {
345
622
  done(null, body);
346
623
  },
@@ -349,14 +626,24 @@ export async function build() {
349
626
  // field (#4999) — raw buffer here too, so the signature is verified over
350
627
  // the EXACT bytes Slack signed, never a re-serialized parse.
351
628
  webhooksScope.addContentTypeParser(
352
- 'application/x-www-form-urlencoded',
353
- { parseAs: 'buffer' },
629
+ "application/x-www-form-urlencoded",
630
+ { parseAs: "buffer" },
354
631
  (_req, body, done) => {
355
632
  done(null, body);
356
633
  },
357
634
  );
358
- await webhooksScope.register(webhooksRoutes);
359
- await webhooksScope.register(slackActionsRoute);
635
+ // Stripe webhook receiver — module-conditional (commerce, D-47). The
636
+ // scope wrapper and both raw-body parsers above stay unconditional: the
637
+ // Slack parser below needs them too (D2).
638
+ if (isModuleEnabled("commerce")) {
639
+ await webhooksScope.register(await sliceRoutePlugin("webhooksRoutes"));
640
+ }
641
+ // Project-listing operator decision (Slack Approve/Reject) — the "hand an
642
+ // operator a decision" half of D-48's grouping sentence. Module-conditional
643
+ // (signal-collectors).
644
+ if (isModuleEnabled("signal-collectors")) {
645
+ await webhooksScope.register(await sliceRoutePlugin("slackActionsRoute"));
646
+ }
360
647
  });
361
648
 
362
649
  // Leads routes — rate-limit middleware scoped to this sub-router. Own
@@ -373,20 +660,43 @@ export async function build() {
373
660
  // Project-listings anonymous surface (#4998, reshaped by #5036) — the
374
661
  // three-operation draft lifecycle plus the public page-render read, all
375
662
  // on the same rate-limited, identity-blind posture as the leads routes
376
- // above.
663
+ // above. Module-conditional (signal-collectors, D-48): the listing
664
+ // instrument's application-lifecycle half.
377
665
  //
378
666
  // Do NOT read that rate limit as a bound on anonymous draft creation. It
379
667
  // keys on `req.ip`, and `trustProxy` is on (see the note at the top of this
380
668
  // file), so a caller reaching the API directly can vary
381
669
  // `X-Forwarded-For` and get a fresh budget per request. That is a
382
670
  // pre-existing property of the limiter, not of these routes; what these
383
- // routes do is keep the cost of an unbounded call small — `start` writes
384
- // one row and sends nothing, and the Lead write with its operator
385
- // notification waits for `submit` (PR #5042 security review, finding 4).
386
- await leadsScope.register(projectListingStartRoute);
387
- await leadsScope.register(projectListingDraftPatchRoute);
388
- await leadsScope.register(projectListingSubmitRoute);
389
- await leadsScope.register(projectListingPublicRoute);
671
+ // routes do is keep the cost of an unbounded call small — the Lead write
672
+ // with its operator notification waits for `submit` (PR #5042 security
673
+ // review, finding 4).
674
+ //
675
+ // `start` DOES now send one email (#5406, the founder's resume link), so
676
+ // the older form of this note — "`start` writes one row and sends
677
+ // nothing" — no longer holds. That send is opt-in per site, and where it
678
+ // is on it is bounded in the send path rather than per IP
679
+ // (`services/project-listing-resume-email.ts`), precisely because this IP
680
+ // limiter cannot carry the weight of an outbound-mail decision. Two bounds,
681
+ // both counted in the database so they hold across instances: PER RECIPIENT
682
+ // (PR #5411 security review, finding 1) and, since #5414, PER SITE across
683
+ // every recipient — the residual the first one left, and the one that
684
+ // decides whether a flood can cost the site its sending domain.
685
+ if (isModuleEnabled("signal-collectors")) {
686
+ await leadsScope.register(await sliceRoutePlugin("projectListingStartRoute"));
687
+ await leadsScope.register(
688
+ await sliceRoutePlugin("projectListingDraftPatchRoute"),
689
+ );
690
+ await leadsScope.register(await sliceRoutePlugin("projectListingSubmitRoute"));
691
+ // #5407 — the resume link's verification write. Anonymous, and gated
692
+ // on the resume token AND the nonce that only the resume email
693
+ // carries, so it belongs in the same rate-limited scope as the other
694
+ // three.
695
+ await leadsScope.register(
696
+ await sliceRoutePlugin("projectListingVerifyEmailRoute"),
697
+ );
698
+ await leadsScope.register(await sliceRoutePlugin("projectListingPublicRoute"));
699
+ }
390
700
  });
391
701
 
392
702
  // Flows routes — anonymous surface; rate-limit middleware scoped to this
@@ -397,13 +707,23 @@ export async function build() {
397
707
  // scope #4730's bump was actually sized for; unchanged by #5111.
398
708
  await server.register(async (flowsScope) => {
399
709
  flowsScope.register(rateLimitMiddleware);
400
- await flowsScope.register(getFlowRoute);
401
- await flowsScope.register(submitFlowStepRoute);
402
- await flowsScope.register(getFlowAggregateRoute);
710
+ // Flow engine reads (#3956) — module-conditional (signal-collectors,
711
+ // D-48). The scope itself and its rate limiter above stay unconditional
712
+ // (D2) — commerce's recommender routes below need them too.
713
+ if (isModuleEnabled("signal-collectors")) {
714
+ await flowsScope.register(await sliceRoutePlugin("getFlowRoute"));
715
+ await flowsScope.register(await sliceRoutePlugin("submitFlowStepRoute"));
716
+ await flowsScope.register(await sliceRoutePlugin("getFlowAggregateRoute"));
717
+ }
403
718
  // The recommender's email-moment routes (#4841) — same anonymous,
404
- // rate-limited posture as the three above.
405
- await flowsScope.register(saveRecommendationRoute);
406
- await flowsScope.register(checkoutRecommendationRoute);
719
+ // rate-limited posture as the three above. Module-conditional (commerce,
720
+ // D-47): the recommender leaves Pro flows and moves to commerce whole.
721
+ if (isModuleEnabled("commerce")) {
722
+ await flowsScope.register(await sliceRoutePlugin("saveRecommendationRoute"));
723
+ await flowsScope.register(
724
+ await sliceRoutePlugin("checkoutRecommendationRoute"),
725
+ );
726
+ }
407
727
  });
408
728
 
409
729
  // AEO score routes (#4666) — anonymous surface, same posture as the flows
@@ -428,14 +748,18 @@ export async function build() {
428
748
  // (#5111, DEF-004). Module-conditional (S1, #5088): the scheduling module
429
749
  // owns this whole route set, so an OSS build that excludes it registers
430
750
  // neither the routes nor their imports.
431
- if (isModuleEnabled('scheduling')) {
751
+ if (isModuleEnabled("scheduling")) {
432
752
  await server.register(async (scheduleScope) => {
433
753
  scheduleScope.register(rateLimitMiddleware, {
434
754
  perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
435
755
  });
436
- await scheduleScope.register(await sliceRoutePlugin('listAvailabilityRoute'));
437
- await scheduleScope.register(await sliceRoutePlugin('getEventMetaRoute'));
438
- await scheduleScope.register(await sliceRoutePlugin('createBookingRoute'));
756
+ await scheduleScope.register(
757
+ await sliceRoutePlugin("listAvailabilityRoute"),
758
+ );
759
+ await scheduleScope.register(await sliceRoutePlugin("getEventMetaRoute"));
760
+ await scheduleScope.register(
761
+ await sliceRoutePlugin("createBookingRoute"),
762
+ );
439
763
  });
440
764
  }
441
765
 
@@ -447,7 +771,7 @@ export async function build() {
447
771
  if (shouldMountTestAuth()) {
448
772
  await server.register(registerTestAuthRoute);
449
773
  server.log.warn(
450
- 'test-auth route MOUNTED — only safe in dev/test environments',
774
+ "test-auth route MOUNTED — only safe in dev/test environments",
451
775
  );
452
776
  }
453
777