create-ailk 0.2.0 → 0.4.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 (292) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/cli.js +19 -9
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +51 -3
  6. package/dist/lib/extract-module-bundle.js +327 -28
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/fetch-template.d.ts +20 -0
  10. package/dist/lib/fetch-template.js +129 -0
  11. package/dist/lib/module-license-gate.d.ts +10 -0
  12. package/dist/lib/module-license-gate.js +13 -4
  13. package/dist/lib/paid-paths.d.ts +16 -0
  14. package/dist/lib/paid-paths.js +90 -0
  15. package/dist/module-architecture.d.ts +197 -23
  16. package/dist/module-architecture.js +820 -25
  17. package/dist/parse-args.d.ts +9 -2
  18. package/dist/parse-args.js +46 -1
  19. package/dist/programmatic.d.ts +28 -2
  20. package/dist/programmatic.js +115 -8
  21. package/dist/surfaces.d.ts +12 -36
  22. package/dist/surfaces.js +77 -5
  23. package/dist/sync-routes.js +31 -1
  24. package/dist/template-manifest.d.ts +92 -0
  25. package/dist/template-manifest.js +168 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +44 -0
  28. package/package.json +3 -2
  29. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  30. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  31. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site-free/provenance.json +4 -0
  34. package/templates/.claude/agents/web.md +2 -3
  35. package/templates/.claude/rules/architecture.md +5 -5
  36. package/templates/.claude/skills/README.md +2 -1
  37. package/templates/.env.example +21 -9
  38. package/templates/CONVENTIONS.md +6 -7
  39. package/templates/apps/api/.env.example +9 -0
  40. package/templates/apps/api/CLAUDE.md +33 -5
  41. package/templates/apps/api/package.json +0 -1
  42. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  43. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  44. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  45. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  46. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  47. package/templates/apps/api/src/config/index.ts +19 -0
  48. package/templates/apps/api/src/config/modules.ts +12 -14
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  50. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  51. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  52. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  53. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  54. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  55. package/templates/apps/api/src/openapi/index.ts +9 -3
  56. package/templates/apps/api/src/openapi/spec.ts +505 -65
  57. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  58. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  59. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  60. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  61. package/templates/apps/api/src/routes/content/index.ts +38 -11
  62. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  63. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  64. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  65. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  66. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  67. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  68. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  69. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  70. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  71. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  72. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  73. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  74. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  75. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  76. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  77. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  78. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  79. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  80. package/templates/apps/api/src/server.ts +504 -137
  81. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  82. package/templates/apps/api/src/services/listing-config.ts +58 -0
  83. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  84. package/templates/apps/api/src/services/project-listings.ts +229 -4
  85. package/templates/apps/api/src/vercel-handler.ts +60 -26
  86. package/templates/apps/mcp/.env.example +8 -0
  87. package/templates/apps/mcp/CLAUDE.md +2 -2
  88. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  89. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  90. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  91. package/templates/apps/mcp/src/config/modules.ts +14 -13
  92. package/templates/apps/mcp/src/server.ts +8 -4
  93. package/templates/apps/mcp/src/tools/index.ts +7 -22
  94. package/templates/apps/web/.env.example +15 -0
  95. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  96. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  97. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  102. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  105. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  106. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  107. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  108. package/templates/apps/web/jest.config.cjs +19 -13
  109. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  110. package/templates/apps/web/lib/server-api.ts +28 -12
  111. package/templates/apps/web/messages/en.json +58 -0
  112. package/templates/apps/web/next.config.mjs +1 -3
  113. package/templates/apps/web/package.json +0 -6
  114. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  115. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  116. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  117. package/templates/apps/web/public/favicon-16x16.png +0 -0
  118. package/templates/apps/web/public/favicon-32x32.png +0 -0
  119. package/templates/apps/web/public/favicon.ico +0 -0
  120. package/templates/apps/web/public/favicon.svg +6 -3
  121. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  122. package/templates/apps/web/public/logomark.svg +4 -0
  123. package/templates/apps/web/public/site.webmanifest +2 -2
  124. package/templates/apps/web/public/wordmark.svg +4 -0
  125. package/templates/database/CHANGELOG.md +47 -0
  126. package/templates/database/inbox/schema.prisma +67 -0
  127. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  128. package/templates/database/package.json +1 -1
  129. package/templates/package.json +1 -1
  130. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  131. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  132. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  133. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  134. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  135. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  136. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  137. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  138. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  139. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  140. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  141. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  142. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  143. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  144. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  145. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  146. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  147. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  148. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  149. package/templates/apps/api/src/lib/stripe.ts +0 -52
  150. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  151. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  152. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  153. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  154. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  155. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  156. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  157. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  158. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  159. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  160. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  161. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  162. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  163. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  164. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  165. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  166. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  167. package/templates/apps/api/src/routes/flows/README.md +0 -147
  168. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  169. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  170. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  171. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  172. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  173. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  174. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  175. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  176. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  177. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  178. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  179. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  180. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  181. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  182. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  183. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  184. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  185. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  186. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  187. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  188. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  189. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  190. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  191. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  192. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  193. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  194. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  195. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  196. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  197. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  198. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  199. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  200. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  201. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  202. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  203. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  204. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  205. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  206. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  207. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  208. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  209. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  210. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  211. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  212. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  213. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  214. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  215. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  216. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  217. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  218. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  219. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  220. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  221. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  222. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  223. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  224. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  225. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  226. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  227. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  228. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  229. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  230. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  231. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  232. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  233. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  234. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  235. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  236. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  237. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  238. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  239. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  240. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  241. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  242. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  243. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  244. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  245. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  246. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  247. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  248. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  249. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  250. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  258. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  259. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  260. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  261. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  266. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  267. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  268. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  269. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  270. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  271. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  272. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  273. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  274. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  275. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  276. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  277. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  278. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  279. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  280. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  281. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  282. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  283. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  284. package/templates/apps/web/content/en/blog/README.txt +0 -25
  285. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  286. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  287. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  288. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  289. package/templates/apps/web/lib/source.ts +0 -34
  290. package/templates/apps/web/public/logomark-dark.svg +0 -4
  291. package/templates/apps/web/public/logomark-light.svg +0 -4
  292. package/templates/apps/web/source.config.ts +0 -78
@@ -3,10 +3,16 @@
3
3
  */
4
4
  import type { FastifyPluginAsync } from "fastify";
5
5
 
6
- import { buildOpenApiSpec } from "./spec.js";
6
+ import { buildOpenApiSpec, readRegisteredRoutes } from "./spec.js";
7
7
 
8
8
  export const openapiRoutes: FastifyPluginAsync = async (fastify) => {
9
- fastify.get("/v1/openapi.json", async (_request, reply) => {
10
- return reply.send(buildOpenApiSpec());
9
+ fastify.get("/v1/openapi.json", async (request, reply) => {
10
+ // The route table comes off the instance that served this request (#5314),
11
+ // never from module state — two servers built concurrently in one process
12
+ // would otherwise share, and corrupt, one table. Absent (a bare Fastify
13
+ // instance with no `build()`) degrades to the hand-authored paths.
14
+ return reply.send(
15
+ buildOpenApiSpec(undefined, readRegisteredRoutes(request.server)),
16
+ );
11
17
  });
12
18
  };
@@ -7,104 +7,544 @@
7
7
  *
8
8
  * When this surface grows beyond a handful of routes, swap in a generator
9
9
  * and delete this file; the route handlers don't depend on the spec shape.
10
+ *
11
+ * Hand-authored does NOT mean self-described: this file is byte-synced into
12
+ * every consuming site, so the site's own name and the deployment's own API
13
+ * origin are resolved at serve time (#5306, #5307) rather than written in as
14
+ * literals. Everything a literal can correctly say about every deployment —
15
+ * route shapes, schemas — stays a literal.
10
16
  */
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
11
20
  import { SCHEMA_ORG_TYPES } from "@working-theory/content-adapters";
21
+ import { logger } from "@working-theory/observability";
22
+ import { parse as parseYaml } from "yaml";
23
+
24
+ import { config } from "../config/index.js";
25
+ import { isModuleEnabled } from "../config/modules.js";
26
+ import { getContentRoot } from "../lib/content-adapter.js";
12
27
 
28
+ import { toOpenApiPath, type RegisteredRoute } from "./surface-drift.js";
29
+
30
+ /**
31
+ * The version of the PUBLIC API CONTRACT — the `/v1` route prefix this spec
32
+ * documents — and deliberately not the version of any npm package (#5307
33
+ * AC-4). The `@working-theory/*` packages move on their own 0.x line and are
34
+ * released per-change; the contract an agent codes against moves only when a
35
+ * route's shape does. Syncing this to a package version would make every
36
+ * package release look like an API change to a caller that pins on it.
37
+ *
38
+ * Bump the major only alongside a new `/vN` prefix.
39
+ */
13
40
  export const API_VERSION = "1.0.0";
14
41
 
15
- export function buildOpenApiSpec() {
42
+ /** Name used when the site's own identity cannot be resolved (#5307 AC-3). */
43
+ const NEUTRAL_DISPLAY_NAME = "Content API";
44
+
45
+ /** Spec title used when the site's own name cannot be resolved (#5307 AC-3). */
46
+ const NEUTRAL_SPEC_TITLE = "Public Content API";
47
+
48
+ /** Root-index description used when `_site.mdx` carries none. */
49
+ const NEUTRAL_DESCRIPTION = "Local auth + session backend + public content API";
50
+
51
+ /**
52
+ * How this deployment describes itself — resolved from the site's own config
53
+ * and this deployment's own env, never from a literal naming the kit.
54
+ */
55
+ export interface ApiIdentity {
56
+ /** `siteName` from `_site.mdx`. Absent when the file is missing/unreadable. */
57
+ siteName?: string;
58
+ /** `siteIdentity.description` from `_site.mdx`, when present. */
59
+ siteDescription?: string;
60
+ /**
61
+ * The public origin this API is reachable at, from `AILK_PUBLIC_API_URL`.
62
+ *
63
+ * NOT `_site.mdx`'s `baseUrl` (#5306 AC-3). `baseUrl` is the SITE origin;
64
+ * the API is deployed on a different host — `samhenry.org` serves the site
65
+ * and `api.samhenry.org` serves this API. Pointing `servers` at `baseUrl`
66
+ * would look right in review, pass every test that does not deploy, and
67
+ * send agents to the site's web origin instead of its API. The two are
68
+ * separate facts and only the deployment knows the second one.
69
+ */
70
+ publicApiUrl?: string;
71
+ }
72
+
73
+ let cachedIdentity: ApiIdentity | undefined;
74
+
75
+ /**
76
+ * Resolve this deployment's identity, reading `_site.mdx` once per process.
77
+ *
78
+ * Deliberately synchronous, and deliberately a local frontmatter read rather
79
+ * than `@working-theory/site-config`'s async `loadSiteConfig`: the spec is
80
+ * built inside a sync route handler, and only two scalar fields are needed.
81
+ * This mirrors `lib/locales.ts`, which reads `project.yaml` the same way and
82
+ * for the same boundary reason.
83
+ *
84
+ * Every failure path degrades to the neutral default (#5307 AC-3) — a missing
85
+ * or malformed `_site.mdx` must never take the API down, and must never fall
86
+ * back to the kit's name.
87
+ */
88
+ export function resolveApiIdentity(): ApiIdentity {
89
+ if (!cachedIdentity) cachedIdentity = readApiIdentity();
90
+ return cachedIdentity;
91
+ }
92
+
93
+ /** Test/utility helper — drop the cached identity so the next call re-reads. */
94
+ export function resetApiIdentity(): void {
95
+ cachedIdentity = undefined;
96
+ }
97
+
98
+ function readApiIdentity(): ApiIdentity {
99
+ const identity: ApiIdentity = {};
100
+
101
+ const publicApiUrl = normaliseOrigin(config.AILK_PUBLIC_API_URL);
102
+ if (publicApiUrl) identity.publicApiUrl = publicApiUrl;
103
+
104
+ const frontmatter = readSiteFrontmatter();
105
+ const siteName = frontmatter?.["siteName"];
106
+ if (typeof siteName === "string" && siteName.trim()) {
107
+ identity.siteName = siteName.trim();
108
+ }
109
+
110
+ const siteIdentity = frontmatter?.["siteIdentity"];
111
+ const description =
112
+ siteIdentity && typeof siteIdentity === "object"
113
+ ? (siteIdentity as Record<string, unknown>)["description"]
114
+ : undefined;
115
+ if (typeof description === "string" && description.trim()) {
116
+ identity.siteDescription = description.trim();
117
+ }
118
+
119
+ if (!identity.siteName) {
120
+ logger.warn(
121
+ { neutralName: NEUTRAL_DISPLAY_NAME },
122
+ "No readable siteName in _site.mdx — the API will describe itself with a neutral default",
123
+ );
124
+ }
125
+
126
+ return identity;
127
+ }
128
+
129
+ /** Read and parse `_site.mdx` frontmatter, or `undefined` on any failure. */
130
+ function readSiteFrontmatter(): Record<string, unknown> | undefined {
131
+ const path = join(getContentRoot(), "_site.mdx");
132
+ if (!existsSync(path)) return undefined;
133
+
134
+ try {
135
+ const raw = readFileSync(path, "utf8");
136
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
137
+ if (!match?.[1]) return undefined;
138
+
139
+ const parsed: unknown = parseYaml(match[1]);
140
+ return parsed && typeof parsed === "object"
141
+ ? (parsed as Record<string, unknown>)
142
+ : undefined;
143
+ } catch {
144
+ return undefined;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Accept an absolute http(s) origin and strip a trailing slash. Anything else
150
+ * resolves to `undefined` so the caller degrades to a document-relative
151
+ * server rather than publishing a malformed absolute URL (#5306 AC-2).
152
+ */
153
+ function normaliseOrigin(value: string | undefined): string | undefined {
154
+ const trimmed = value?.trim();
155
+ if (!trimmed) return undefined;
156
+
157
+ try {
158
+ const url = new URL(trimmed);
159
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
160
+ logger.warn(
161
+ { AILK_PUBLIC_API_URL: trimmed },
162
+ "AILK_PUBLIC_API_URL is not an http(s) URL — the OpenAPI servers block will fall back to a document-relative origin",
163
+ );
164
+ return undefined;
165
+ }
166
+ return trimmed.replace(/\/+$/, "");
167
+ } catch {
168
+ logger.warn(
169
+ { AILK_PUBLIC_API_URL: trimmed },
170
+ "AILK_PUBLIC_API_URL is not a parseable URL — the OpenAPI servers block will fall back to a document-relative origin",
171
+ );
172
+ return undefined;
173
+ }
174
+ }
175
+
176
+ /** The name this API introduces itself by on the root index. */
177
+ export function apiDisplayName(identity: ApiIdentity): string {
178
+ return identity.siteName ? `${identity.siteName} API` : NEUTRAL_DISPLAY_NAME;
179
+ }
180
+
181
+ /** The description this API introduces itself by on the root index. */
182
+ export function apiDescription(identity: ApiIdentity): string {
183
+ return identity.siteDescription ?? NEUTRAL_DESCRIPTION;
184
+ }
185
+
186
+ /**
187
+ * Resolve the `servers` block.
188
+ *
189
+ * With an origin configured, one entry naming this deployment. Without one,
190
+ * a single document-relative entry: per OpenAPI 3.1 a `/` server resolves
191
+ * against the origin the document was served from, so an agent that fetched
192
+ * this spec from `https://api.example.com/v1/openapi.json` still reaches the
193
+ * right host. That makes absence a correct-by-construction degradation rather
194
+ * than a silently wrong URL (#5306 AC-2), and the entry's own description
195
+ * names the unset variable so the reason travels with the served document.
196
+ */
197
+ function resolveServers(
198
+ publicApiUrl: string | undefined,
199
+ ): Array<{ url: string; description: string }> {
200
+ const origin = normaliseOrigin(publicApiUrl);
201
+ if (origin) {
202
+ return [{ url: origin, description: "This deployment" }];
203
+ }
204
+ return [
205
+ {
206
+ url: "/",
207
+ description:
208
+ "This deployment — relative to the origin that served this document " +
209
+ "(AILK_PUBLIC_API_URL is unset)",
210
+ },
211
+ ];
212
+ }
213
+
214
+ /**
215
+ * The route table a built server registered (#5314).
216
+ *
217
+ * Carried on the Fastify instance (`server.decorate`) and threaded into the
218
+ * two surfaces as an argument — deliberately NOT module-level state. A
219
+ * module-level array is shared by every server built in one process, and
220
+ * `apps/api/src/vercel-handler.ts`'s `getServer()` is a check-then-set: it
221
+ * tests `!serverInstance`, then awaits twice before assigning, so two
222
+ * concurrent invocations can both call `build()`. With shared state, one
223
+ * build's reset would land mid-fill of the other and leave a truncated or
224
+ * doubled table — the surfaces would then silently misreport, which is the
225
+ * exact failure this card exists to remove. Per-instance state makes that
226
+ * unreachable whether or not the race fires.
227
+ *
228
+ * Registration is CONDITIONAL — `isModuleEnabled()` gates paid-module routes
229
+ * and `shouldMountTestAuth()` gates the test-auth route — so this is what THIS
230
+ * deployment serves, not a catalogue of what the build could serve (spec D1).
231
+ * A route absent because its module is off is absent from both surfaces too,
232
+ * which is why a disabled slice never reads as drift.
233
+ */
234
+
235
+ /** The decorator key both surfaces read the route table from. */
236
+ export const REGISTERED_ROUTES_DECORATOR = "registeredRoutes";
237
+
238
+ /**
239
+ * Read the route table off a Fastify instance (or anything carrying the
240
+ * decorator), returning `[]` when it is absent.
241
+ *
242
+ * Empty is a supported state, not a bug: `buildOpenApiSpec()` is called
243
+ * directly by unit tests with no server in play, and this file is byte-synced
244
+ * into every consuming site. `paths` degrades to its hand-authored entries
245
+ * rather than publishing an empty document.
246
+ */
247
+ export function readRegisteredRoutes(source: unknown): RegisteredRoute[] {
248
+ const routes = (source as Record<string, unknown> | null | undefined)?.[
249
+ REGISTERED_ROUTES_DECORATOR
250
+ ];
251
+ return Array.isArray(routes) ? (routes as RegisteredRoute[]) : [];
252
+ }
253
+
254
+ /**
255
+ * Root-index keys this API has already published, by Fastify path (#5314 D6).
256
+ *
257
+ * Deriving the root index without this overlay would rename all eleven keys a
258
+ * consumer may already read, which ADR-0020 counts as a breaking change and
259
+ * would owe a `/v2`. With it, every published key keeps its name and only new
260
+ * keys appear, so the change is purely additive.
261
+ *
262
+ * `billing` and `billingPortalSession` become CONDITIONAL rather than
263
+ * disappearing: they are emitted exactly when their routes register, which is
264
+ * the fix for a root index that advertised both on a build where neither
265
+ * registers and both 404.
266
+ */
267
+ const CURATED_ROOT_INDEX_KEYS: Record<string, string> = {
268
+ "/health": "health",
269
+ "/v1/openapi.json": "openapi",
270
+ "/v1/auth/tokens": "tokens",
271
+ "/v1/user/profile": "profile",
272
+ "/v1/user/data/:key": "userData",
273
+ "/v1/content/types": "contentTypes",
274
+ "/v1/content/:type": "contentList",
275
+ "/v1/content/:type/:slug": "contentPage",
276
+ "/v1/checkout/sessions": "checkoutSessions",
277
+ "/v1/billing": "billing",
278
+ "/v1/billing/portal-session": "billingPortalSession",
279
+ };
280
+
281
+ /**
282
+ * Canonical method order for a root-index value, so `GET/POST /v1/x` is
283
+ * stable across runs rather than following registration order.
284
+ */
285
+ const METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
286
+
287
+ function byCanonicalMethod(a: string, b: string): number {
288
+ const ai = METHOD_ORDER.indexOf(a);
289
+ const bi = METHOD_ORDER.indexOf(b);
290
+ return (
291
+ (ai === -1 ? METHOD_ORDER.length : ai) -
292
+ (bi === -1 ? METHOD_ORDER.length : bi) || a.localeCompare(b)
293
+ );
294
+ }
295
+
296
+ /**
297
+ * A deterministic root-index key for a path with no curated name:
298
+ * `/v1/project-listings/public` becomes `projectListingsPublic`, and a
299
+ * `:param` segment contributes its bare name.
300
+ */
301
+ function generatedRootIndexKey(path: string): string {
302
+ const segments = path
303
+ .replace(/^\/v1\//, "/")
304
+ .split("/")
305
+ .filter(Boolean)
306
+ .map((segment) => segment.replace(/^:/, ""));
307
+ if (segments.length === 0) return "root";
308
+
309
+ return segments
310
+ .map((segment, index) => {
311
+ const camel = segment
312
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_m, c: string) => c.toUpperCase())
313
+ .replace(/[^a-zA-Z0-9]/g, "");
314
+ return index === 0
315
+ ? camel.charAt(0).toLowerCase() + camel.slice(1)
316
+ : camel.charAt(0).toUpperCase() + camel.slice(1);
317
+ })
318
+ .join("");
319
+ }
320
+
321
+ /**
322
+ * The root index `endpoints` object, derived from the routes this process
323
+ * registered (#5314 AC-1).
324
+ *
325
+ * One entry per PATH, valued `"METHOD[/METHOD...] /path"` — the shape this
326
+ * surface has always published (`"GET/PATCH /v1/user/profile"`). The SHAPE is
327
+ * preserved, not every value: `tokens` goes from `"POST/GET/DELETE
328
+ * /v1/auth/tokens"` to `"GET/POST /v1/auth/tokens"`, because `DELETE` was
329
+ * never served at that path (it is `/v1/auth/tokens/:id`, now its own entry).
330
+ * Key NAMES are preserved outright, which is what keeps this additive under
331
+ * ADR-0020; the value corrections are the drift being fixed. Curated keys win; everything else gets a
332
+ * generated one, de-duplicated by suffix so two paths can never collapse onto
333
+ * one key and silently hide a route.
334
+ *
335
+ * An empty route table yields an empty object rather than a stale literal: the
336
+ * only caller is the `GET /` handler on a built server, whose own route is in
337
+ * the table by request time, so an empty result would mean the hook had failed
338
+ * and is better surfaced than papered over.
339
+ */
340
+ export function deriveRootEndpoints(
341
+ routes: RegisteredRoute[],
342
+ ): Record<string, string> {
343
+ const methodsByPath = new Map<string, string[]>();
344
+ for (const { method, url } of routes) {
345
+ const methods = methodsByPath.get(url) ?? [];
346
+ if (!methods.includes(method)) methods.push(method);
347
+ methodsByPath.set(url, methods);
348
+ }
349
+
350
+ const endpoints: Record<string, string> = {};
351
+ const used = new Set<string>();
352
+ for (const [path, methods] of methodsByPath) {
353
+ const base = CURATED_ROOT_INDEX_KEYS[path] ?? generatedRootIndexKey(path);
354
+ let key = base;
355
+ for (let n = 2; used.has(key); n += 1) key = `${base}${n}`;
356
+ used.add(key);
357
+ endpoints[key] =
358
+ `${[...methods].sort(byCanonicalMethod).join("/")} ${path}`;
359
+ }
360
+ return endpoints;
361
+ }
362
+
363
+ /** An OpenAPI `operationId` for a route with no hand-authored operation. */
364
+ function derivedOperationId(method: string, path: string): string {
365
+ const suffix = generatedRootIndexKey(path);
366
+ return `${method.toLowerCase()}${suffix.charAt(0).toUpperCase()}${suffix.slice(1)}`;
367
+ }
368
+
369
+ /**
370
+ * The minimal OpenAPI 3.1 operation a route table can honestly produce.
371
+ *
372
+ * No route in this app carries a Fastify `schema` — validation is Zod inside
373
+ * the handler — so the route table knows a route's method and path and
374
+ * nothing else (#5314 D2). Inventing a request or response schema here would
375
+ * be worse than omitting one: an agent codes against what the document says.
376
+ */
377
+ function derivedOperation(method: string, fastifyPath: string) {
378
+ const parameters = fastifyPath
379
+ .split("/")
380
+ .filter((segment) => segment.startsWith(":"))
381
+ .map((segment) => ({
382
+ name: segment.slice(1),
383
+ in: "path",
384
+ required: true,
385
+ schema: { type: "string" },
386
+ }));
387
+
388
+ return {
389
+ operationId: derivedOperationId(method, fastifyPath),
390
+ summary: `${method.toUpperCase()} ${toOpenApiPath(fastifyPath)}`,
391
+ description:
392
+ "Derived from this deployment's route table (#5314). Request and " +
393
+ "response shapes are not described: this route carries no schema the " +
394
+ "route table can read.",
395
+ ...(parameters.length > 0 && { parameters }),
396
+ responses: { "200": { description: "OK" } },
397
+ };
398
+ }
399
+
400
+ /**
401
+ * `paths` = every registered route, overlaid with the hand-authored operations.
402
+ *
403
+ * The derived half makes the endpoint SET complete and correct by construction
404
+ * (#5314 AC-1/AC-4); the overlay keeps every hand-authored schema, example and
405
+ * description exactly as written (#5314 D2). Hand-authored wins per method, and
406
+ * path-level keys on a hand-authored item are preserved.
407
+ *
408
+ * With an EMPTY registry the hand-authored object is returned untouched. That
409
+ * is the byte-sync degradation floor (#5314 FR7): `buildOpenApiSpec()` is
410
+ * called directly by unit tests with no server in play, and a consuming site
411
+ * that somehow builds no route table still serves the seven documented paths
412
+ * rather than an empty document.
413
+ */
414
+ function mergeDerivedPaths(
415
+ routes: RegisteredRoute[],
416
+ handAuthored: Record<string, Record<string, unknown>>,
417
+ ): Record<string, Record<string, unknown>> {
418
+ if (routes.length === 0) return handAuthored;
419
+
420
+ const paths: Record<string, Record<string, unknown>> = {};
421
+ for (const { method, url } of routes) {
422
+ const path = toOpenApiPath(url);
423
+ paths[path] = {
424
+ ...paths[path],
425
+ [method.toLowerCase()]: derivedOperation(method, url),
426
+ };
427
+ }
428
+
429
+ for (const [path, item] of Object.entries(handAuthored)) {
430
+ paths[path] = { ...paths[path], ...item };
431
+ }
432
+ return paths;
433
+ }
434
+
435
+ export function buildOpenApiSpec(
436
+ identity: ApiIdentity = resolveApiIdentity(),
437
+ registeredRoutes: RegisteredRoute[] = [],
438
+ ) {
16
439
  const typeEnum = [...SCHEMA_ORG_TYPES];
17
440
 
18
441
  return {
19
442
  openapi: "3.1.0",
20
443
  info: {
21
- title: "AI Launch Kit — Public Content API",
444
+ title: identity.siteName
445
+ ? `${identity.siteName} — ${NEUTRAL_SPEC_TITLE}`
446
+ : NEUTRAL_SPEC_TITLE,
22
447
  version: API_VERSION,
23
448
  description:
24
- "HTTP API exposing AILK content as schema.org JSON-LD, plus a small set " +
25
- "of anonymous write routes (lead capture, checkout). Designed for agent " +
26
- "consumption (AX) — every content response carries `@context` and " +
27
- "`@type` at the document root.",
28
- license: { name: "MIT" },
449
+ "HTTP API exposing this site's content as schema.org JSON-LD, plus a " +
450
+ "small set of anonymous write routes (lead capture, checkout). Designed " +
451
+ "for agent consumption (AX) — every content response carries `@context` " +
452
+ "and `@type` at the document root.",
453
+ // No `license`: this file is byte-synced into every consuming site, so a
454
+ // literal here would assert a licence over a deployment that never chose
455
+ // one (#5307 AC-2). OpenAPI 3.1 makes `info.license` optional; a site
456
+ // that wants to publish one declares it where its own licence lives.
29
457
  },
30
- servers: [
31
- { url: "http://localhost:3002", description: "Local development" },
32
- { url: "https://api.ailaunchkit.ai", description: "Production" },
33
- ],
34
- paths: {
35
- "/v1/checkout/sessions": {
36
- post: {
37
- operationId: "createCheckoutSession",
38
- summary: "Create a Stripe hosted-page checkout session",
39
- description:
40
- "Resolves a productCode to a Stripe price via lookup_key and creates a " +
41
- "hosted-page checkout session. Returns the session ID and redirect URL. " +
42
- "Sandbox-only: live Stripe keys are rejected at runtime.",
43
- requestBody: {
44
- required: true,
45
- content: {
46
- "application/json": {
47
- schema: {
48
- $ref: "#/components/schemas/CheckoutSessionRequest",
49
- },
50
- },
51
- },
52
- },
53
- responses: {
54
- "201": {
55
- description: "Checkout session created",
458
+ servers: resolveServers(identity.publicApiUrl),
459
+ paths: mergeDerivedPaths(registeredRoutes, {
460
+ // Module-conditional (commerce, D-47 / #5364) — mirroring the CONDITIONAL
461
+ // treatment `CURATED_ROOT_INDEX_KEYS`'s doc comment above already
462
+ // describes for `billing`/`billingPortalSession`. Those two have no
463
+ // hand-authored `paths` entry here at all (mergeDerivedPaths's DERIVED
464
+ // half is what publishes them, so they only ever appear when actually
465
+ // registered); `/v1/checkout/sessions` is the one hand-authored entry
466
+ // in this object, and mergeDerivedPaths always merges hand-authored
467
+ // paths in regardless of registration — so without this gate, a build
468
+ // with commerce off still advertises the operation while `registeredRoutes`
469
+ // has no matching entry, which is exactly the orphaned-surface drift
470
+ // surface-drift.test.ts exists to catch (#5314).
471
+ ...(isModuleEnabled("commerce") && {
472
+ "/v1/checkout/sessions": {
473
+ post: {
474
+ operationId: "createCheckoutSession",
475
+ summary: "Create a Stripe hosted-page checkout session",
476
+ description:
477
+ "Resolves a productCode to a Stripe price via lookup_key and creates a " +
478
+ "hosted-page checkout session. Returns the session ID and redirect URL. " +
479
+ "Sandbox-only: live Stripe keys are rejected at runtime.",
480
+ requestBody: {
481
+ required: true,
56
482
  content: {
57
483
  "application/json": {
58
484
  schema: {
59
- $ref: "#/components/schemas/CheckoutSessionResponse",
485
+ $ref: "#/components/schemas/CheckoutSessionRequest",
60
486
  },
61
487
  },
62
488
  },
63
489
  },
64
- "400": {
65
- description: "Validation error — invalid request body",
66
- content: {
67
- "application/json": {
68
- schema: { $ref: "#/components/schemas/ValidationErrorResponse" },
490
+ responses: {
491
+ "201": {
492
+ description: "Checkout session created",
493
+ content: {
494
+ "application/json": {
495
+ schema: {
496
+ $ref: "#/components/schemas/CheckoutSessionResponse",
497
+ },
498
+ },
69
499
  },
70
500
  },
71
- },
72
- "404": {
73
- description:
74
- "Unknown product code — no active Stripe price for the given productCode",
75
- content: {
76
- "application/json": {
77
- schema: {
78
- $ref: "#/components/schemas/UnknownProductCodeResponse",
501
+ "400": {
502
+ description: "Validation error — invalid request body",
503
+ content: {
504
+ "application/json": {
505
+ schema: {
506
+ $ref: "#/components/schemas/ValidationErrorResponse",
507
+ },
79
508
  },
80
509
  },
81
510
  },
82
- },
83
- "500": {
84
- description:
85
- "Live-mode forbidden — STRIPE_SECRET_KEY is a live key",
86
- content: {
87
- "application/json": {
88
- schema: {
89
- $ref: "#/components/schemas/LiveModeForbiddenResponse",
511
+ "404": {
512
+ description:
513
+ "Unknown product code — no active Stripe price for the given productCode",
514
+ content: {
515
+ "application/json": {
516
+ schema: {
517
+ $ref: "#/components/schemas/UnknownProductCodeResponse",
518
+ },
90
519
  },
91
520
  },
92
521
  },
93
- },
94
- "502": {
95
- description:
96
- "Checkout unavailable — Stripe returned an unexpected error",
97
- content: {
98
- "application/json": {
99
- schema: {
100
- $ref: "#/components/schemas/CheckoutUnavailableResponse",
522
+ "500": {
523
+ description:
524
+ "Live-mode forbidden — STRIPE_SECRET_KEY is a live key",
525
+ content: {
526
+ "application/json": {
527
+ schema: {
528
+ $ref: "#/components/schemas/LiveModeForbiddenResponse",
529
+ },
530
+ },
531
+ },
532
+ },
533
+ "502": {
534
+ description:
535
+ "Checkout unavailable — Stripe returned an unexpected error",
536
+ content: {
537
+ "application/json": {
538
+ schema: {
539
+ $ref: "#/components/schemas/CheckoutUnavailableResponse",
540
+ },
101
541
  },
102
542
  },
103
543
  },
104
544
  },
105
545
  },
106
546
  },
107
- },
547
+ }),
108
548
  "/v1/leads/submit": {
109
549
  post: {
110
550
  operationId: "submitLead",
@@ -391,7 +831,7 @@ export function buildOpenApiSpec() {
391
831
  },
392
832
  },
393
833
  },
394
- },
834
+ }),
395
835
  components: {
396
836
  schemas: {
397
837
  CheckoutSessionRequest: {