create-ailk 0.2.0 → 0.3.0

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