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
@@ -1,82 +0,0 @@
1
- /**
2
- * Flow-checkouts respondent route (#4984 — the "my purchases" read behind
3
- * the respondent account surface).
4
- *
5
- * Routes:
6
- * GET /v1/flow-checkouts/me — the caller's OWN flow checkouts, newest first
7
- *
8
- * Registered INSIDE server.ts's `authScope` (auth only — NOT the inner
9
- * `tenantScope`: a respondent has no workspace membership, and the tenant
10
- * guard is default-deny) so `request.userId` is populated by
11
- * `authPreHandlerPlugin` before the handler runs. An unauthenticated caller
12
- * receives the preHandler's existing 401, untouched. The identity is
13
- * `request.userId` ONLY: the route declares no query schema and reads no
14
- * query or body key (an `email` parameter is never parsed, never read),
15
- * never enumerates, never reads across sites — see
16
- * `services/flow-checkouts.ts`, where the only join is the lead-promotion
17
- * link (`Lead.promotedUserId`), never an email match.
18
- *
19
- * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
20
- * `{ ok: false, error: { code, message } }`, real HTTP status codes:
21
- * 401 unauthorized · 403 email_unverified · 404 user_not_found ·
22
- * 500 internal_error (the 200 body failed its own contract schema — a
23
- * drift is never a leak). A verified user with nothing to show is a 200
24
- * with an empty list.
25
- *
26
- * API↔MCP parity: identity-bound (the session IS the identity; the OSS MCP
27
- * surface is identity-blind) — no MCP twin (#3591 exception, the same shape
28
- * as `/v1/billing/*`, `/v1/workspaces/*`, `/v1/waitlist-signups/me`).
29
- */
30
- import { logger } from "@working-theory/observability";
31
- import { flowCheckoutMeResponseSchema } from "@working-theory/validation";
32
- import type { FastifyPluginAsync, FastifyReply } from "fastify";
33
-
34
- import { readMyCheckouts } from "../../services/flow-checkouts.js";
35
-
36
- function unauthorized(reply: FastifyReply) {
37
- return reply.status(401).send({
38
- ok: false,
39
- error: { code: "unauthorized", message: "Authentication required" },
40
- });
41
- }
42
-
43
- const ERROR_STATUS = {
44
- not_found_user: 404,
45
- email_unverified: 403,
46
- } as const;
47
-
48
- const ERROR_CODE = {
49
- not_found_user: "user_not_found",
50
- email_unverified: "email_unverified",
51
- } as const;
52
-
53
- export const flowCheckoutRoutes: FastifyPluginAsync = async (fastify) => {
54
- fastify.get("/v1/flow-checkouts/me", async (request, reply) => {
55
- const userId = request.userId;
56
- if (!userId) return unauthorized(reply);
57
-
58
- const result = await readMyCheckouts(userId);
59
- if (!result.ok) {
60
- return reply.status(ERROR_STATUS[result.error.kind]).send({
61
- ok: false,
62
- error: { code: ERROR_CODE[result.error.kind], message: result.error.message },
63
- });
64
- }
65
-
66
- // D5 — re-validate the body against its own strict contract before it
67
- // leaves the process (the waitlist-signups idiom): a shape drift is an
68
- // internal error, never a leak. Log issues only — never the value.
69
- const parsed = flowCheckoutMeResponseSchema.safeParse(result.value);
70
- if (!parsed.success) {
71
- logger.error(
72
- { route: "flow-checkouts/me", issues: parsed.error.issues },
73
- "[flow-checkouts] response failed its own contract schema",
74
- );
75
- return reply.status(500).send({
76
- ok: false,
77
- error: { code: "internal_error", message: "Response failed contract validation" },
78
- });
79
- }
80
- return reply.status(200).send({ ok: true, data: parsed.data });
81
- });
82
- };
@@ -1,147 +0,0 @@
1
- # Flows — `apps/api/src/routes/flows/`
2
-
3
- ```
4
- GET /v1/flows/:slug — serve a validated flow config
5
- POST /v1/flows/:slug/steps — submit one step's answers; persists
6
- server-side on landing
7
- GET /v1/flows/:slug/aggregate — anonymous per-question response counts
8
- for the results reveal (#4771)
9
- POST /v1/flows/:slug/save — the recommender's "email me this plan"
10
- moment: FlowSession + Lead (+ T+0 grant)
11
- for the permalink's answers (#4841)
12
- POST /v1/flows/:slug/checkout — the purchase moment: the email moment,
13
- the FlowCheckout buyer row at a
14
- SERVER-recomputed price, then a Stripe
15
- TEST-mode Checkout Session (#4841)
16
- ```
17
-
18
- Every route is anonymous — no auth, no `siteId` on the wire (see the
19
- module docstring in `index.ts` for the single-site default). All sit
20
- behind the per-IP rate limiter (`middleware/rate-limit.ts`), registered
21
- once for the whole `flowsScope` in `server.ts`.
22
-
23
- ## The email moment (`recommender.ts`, #4841)
24
-
25
- A recommending flow captures no PII until the visitor chooses save or
26
- checkout (PRD L108). Both routes take the permalink's answer map + an
27
- email; `checkout` additionally takes `offerKey` / `period` (+ `seats`,
28
- `creditStop`) and the two redirect URLs — and **no amount of any name**:
29
- the body schema is `.strict()`, and the server recomputes `price()` from
30
- the register it served (PRD L104). Checkout refuses a register with
31
- `stripeConfigRef: null` or no `rateTable.currency` (409), refuses any key
32
- outside the `sk_test_` allow-list before any side effect (500), and only
33
- opens the Stripe session AFTER the session, lead and buyer row exist — a
34
- Stripe outage returns 502 and the record survives. The Stripe session's
35
- `metadata` carries `flowCheckoutId`, `flowSlug`, `offerKey`, `sessionId`,
36
- `leadId` and the `permalink` (chunked `permalink_2…` past 500 chars) — the
37
- join the webhook (#4838) stamps on.
38
-
39
- ## Checkout redirect origins (#4912)
40
-
41
- `successUrl` and `cancelUrl` must be on **this site's own origin**. The
42
- shared `flowRedirectUrlSchema` pins only "an `http(s)` URL with a host",
43
- so without this gate a caller posting straight at the API — bypassing the
44
- consumer's Server Action, which builds both URLs itself — could mint a
45
- real Checkout Session whose post-payment redirect lands on a host they
46
- control: an open redirect carrying the credibility of a completed
47
- payment, and `cancelUrl` is the same vector without the victim even
48
- paying.
49
-
50
- | Var | Default | Meaning |
51
- | -------------- | ----------------------- | -------------------------------------------------------------------------- |
52
- | `FRONTEND_URL` | `http://localhost:3000` | The site origin checkout redirects are locked to — the allow-list, of one. |
53
-
54
- **The allow-list source is `FRONTEND_URL`**, read per request. It is the
55
- same variable this API already uses as its CORS origin
56
- (`apps/api/src/server.ts`), for the origin check in
57
- `middleware/auth.ts`, and to compose the billing-portal return URL
58
- (`routes/billing/portal.ts`) — reused rather than inventing a second
59
- site-origin contract, exactly as the scaffolded commerce checkout route
60
- does (`packages/create-ailk/templates/.claude/skills/scaffold-commerce/
61
- templates/checkout-route.template.ts`). The default is byte-identical to
62
- the one `apps/api/src/config/index.ts` applies.
63
-
64
- The comparison is **exact origin equality** — scheme, host and port must
65
- all match. A prefix or suffix test would accept
66
- `https://<site-origin>.evil.example`. A relative path cannot arrive: the
67
- shared schema refuses it before this gate runs. A refused redirect is a
68
- `400 invalid_input` raised **before** the register read, so no session,
69
- no lead and no buyer row is written and Stripe is never called; the
70
- offending URL is never echoed back or logged.
71
-
72
- **Operator consequence.** In production, leave `FRONTEND_URL` unset and
73
- your own checkout redirects will 400 — the same fail-closed direction
74
- CORS already takes on this variable. Set it to the origin visitors reach
75
- the site on. A value that is set but not a parseable absolute URL is a
76
- `500 config-error`, never a silently widened allow-list.
77
-
78
- ## Rate limit — env var and default
79
-
80
- | Var | Default | Meaning |
81
- | ----------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
82
- | `AILK_RATE_LIMIT_PER_IP_PER_MINUTE` | `20` | Requests one visitor IP may make against a rate-limited anonymous scope (flows/leads/aeo/schedule) in a 60-second window. |
83
-
84
- **Why 20.** The default is sized to a full flow — one visitor completing
85
- the longest shipped flow in one sitting should never trip the limit. The
86
- longest shipped flow, `ailk-founder-diagnostic`, is **13 steps + 1 GET**
87
- (the initial config fetch) = 14 requests. 20 leaves headroom for an
88
- invisible `aeo_score` step's own submission (fired alongside a visible
89
- step, #4663 D7) and one legitimate resubmit (e.g. a transient failure the
90
- visitor retries).
91
-
92
- If a customer fork ships a longer flow, raise this env var to
93
- `<step count> + 1 (GET) + 1 (aeo_score, if the flow has one) + a small
94
- margin` rather than lowering the floor for every other flow.
95
-
96
- ## Visitor-IP forwarding (#4730)
97
-
98
- A consumer site's own Server Action (`apps/web/app/[locale]/flows/[slug]/
99
- flow.actions.ts`) calls these routes from ITS OWN server process — every
100
- visitor of that site shares one calling process. Without forwarding, the
101
- API would see that process's egress IP for every request, and every
102
- visitor would share ONE rate-limit bucket.
103
-
104
- The fix has two halves:
105
-
106
- 1. `apps/api/src/server.ts` sets `trustProxy: true` on the Fastify
107
- instance, so `req.ip` resolves the LEFTMOST address of a multi-hop
108
- `X-Forwarded-For` chain (Fastify's built-in `@fastify/proxy-addr`
109
- parsing) instead of a socket address that's meaningless once requests
110
- are bridged through `.inject()` (the Vercel serverless handler,
111
- `apps/api/api/index.ts`, never opens a real socket).
112
- 2. The Server Action reads ITS OWN incoming request's `x-forwarded-for`
113
- (falling back to `x-real-ip`) via Next's `headers()` and forwards it
114
- as an explicit `x-forwarded-for` header on the outbound
115
- `flows.submitStep` call. This APPENDS to, rather than replaces,
116
- whatever hop this API's own edge subsequently adds — the leftmost
117
- entry in the resulting chain is still the real visitor.
118
-
119
- **Trade-off, stated plainly.** A caller hitting these routes directly
120
- (bypassing a consumer's Server Action) can set `x-forwarded-for` to
121
- anything — there is no fixed, allowlistable upstream proxy to trust,
122
- because this API's actual "proxies" are arbitrary consumer web
123
- processes. Per-IP rate limiting on these anonymous surfaces is a
124
- best-effort abuse deterrent, not a hard security boundary; this fix
125
- trades a small amount of spoof-resistance for correctness on the
126
- overwhelmingly common path (Server-Action-forwarded traffic), which was
127
- previously broken for every visitor.
128
-
129
- ## GET config fetch is NOT IP-forwarded (yet)
130
-
131
- `apps/web/app/[locale]/flows/[slug]/page.tsx` (a Server Component) calls
132
- `flows.get(slug)` directly — it does not forward a visitor IP header.
133
- That GET therefore still buckets on whatever `req.ip` resolves to for the
134
- calling process (not the visitor), separately from the POST-step bucket
135
- above. This is a known, narrower gap than the one #4730 fixes (a
136
- single-visitor lockout across 13+ POST steps); extending `flows.get` +
137
- `page.tsx` the same way is a natural follow-up if it proves to matter in
138
- practice.
139
-
140
- **What this means concretely, so the gap isn't understated:** a site
141
- receiving more than the default 20 flow-page loads per minute from one
142
- calling process would 429 its own config GET — and that failure would
143
- surface **nowhere in the UI**, because `flows.get` (unlike
144
- `flows.submitStep`) has no 429-to-`rate_limited` mapping at all; the page
145
- would simply fail to render the flow. This is not a regression (the
146
- pre-#4730 shared bucket was smaller, at 10), and no acceptance criterion
147
- covers it — named here so the follow-up starts from the right severity.