create-ailk 0.1.1 → 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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  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 +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
@@ -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: {