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
@@ -0,0 +1,89 @@
1
+ /**
2
+ * #5334 — `loadSlice()` is the slice-module load boundary.
3
+ *
4
+ * The regression that matters most here is the LAST block: a slice that throws
5
+ * while evaluating must keep its own error object, type and stack. Wrapping it
6
+ * would turn a real bug in a slice into a misleading "could not be resolved"
7
+ * report, which is the opposite of what this boundary exists to do.
8
+ */
9
+ import { loadSlice } from "../slice-load.js";
10
+
11
+ function moduleNotFound(code: string, message: string): Error {
12
+ const error = new Error(message) as Error & { code: string };
13
+ error.code = code;
14
+ return error;
15
+ }
16
+
17
+ describe("loadSlice (#5334)", () => {
18
+ it("passes a resolving thunk's value straight through", async () => {
19
+ const mod = { workspaceContextRoutes: () => undefined };
20
+ await expect(loadSlice("./routes/workspaces/index.js", async () => mod))
21
+ .resolves.toBe(mod);
22
+ });
23
+
24
+ it("awaits the thunk rather than the specifier — the specifier is never imported", async () => {
25
+ const load = jest.fn(async () => ({ ok: true }));
26
+ await loadSlice("./never-imported.js", load);
27
+ expect(load).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ describe.each([
31
+ ["ERR_MODULE_NOT_FOUND", "the ESM resolver's code"],
32
+ ["MODULE_NOT_FOUND", "the CJS resolver's code"],
33
+ ])("on %s (%s)", (code) => {
34
+ const original = moduleNotFound(
35
+ code,
36
+ "Cannot find module '/x/apps/api/dist/list.js'",
37
+ );
38
+ const failing = async () => {
39
+ throw original;
40
+ };
41
+
42
+ it("names the specifier in the message", async () => {
43
+ await expect(loadSlice("./list.js", failing)).rejects.toThrow(
44
+ /AILK slice module '\.\/list\.js'/,
45
+ );
46
+ });
47
+
48
+ it("names BOTH causes, so a reader can tell a tracer miss from a bundled build", async () => {
49
+ const error = await loadSlice("./list.js", failing).catch(
50
+ (e: unknown) => e as Error,
51
+ );
52
+ expect(error.message).toMatch(/not SHIPPED/);
53
+ expect(error.message).toMatch(/slice-reachability:check/);
54
+ expect(error.message).toMatch(/BUNDLED/);
55
+ expect(error.message).toMatch(/apps\/api\/CLAUDE\.md/);
56
+ });
57
+
58
+ it("preserves the original error as `cause`", async () => {
59
+ const error = await loadSlice("./list.js", failing).catch(
60
+ (e: unknown) => e as Error,
61
+ );
62
+ expect(error.cause).toBe(original);
63
+ });
64
+ });
65
+
66
+ it("rethrows a non-resolution error IDENTICALLY — same object, not a wrapper", async () => {
67
+ const boom = new Error("slice blew up while evaluating");
68
+ const caught = await loadSlice("./list.js", async () => {
69
+ throw boom;
70
+ }).catch((e: unknown) => e);
71
+ expect(caught).toBe(boom);
72
+ expect((caught as Error).message).toBe("slice blew up while evaluating");
73
+ });
74
+
75
+ it("rethrows an errno-shaped error that is not a module-resolution code", async () => {
76
+ const denied = moduleNotFound("EACCES", "permission denied");
77
+ const caught = await loadSlice("./list.js", async () => {
78
+ throw denied;
79
+ }).catch((e: unknown) => e);
80
+ expect(caught).toBe(denied);
81
+ });
82
+
83
+ it("rethrows a non-Error throw untouched", async () => {
84
+ const caught = await loadSlice("./list.js", async () => {
85
+ throw "a string, because JavaScript";
86
+ }).catch((e: unknown) => e);
87
+ expect(caught).toBe("a string, because JavaScript");
88
+ });
89
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Slice-module load boundary (#5334 D4).
3
+ *
4
+ * Every paid-module SLICE in `apps/api` is reached through an indirect dynamic
5
+ * `import()` — a specifier the call site never writes as a literal, because
6
+ * `tsc` would then resolve it and the OSS strip would break (#5182 D1). #5304
7
+ * moved those specifiers onto module-level constants, the one shape that is
8
+ * simultaneously unresolvable to `tsc` and traceable to `@vercel/nft`, so the
9
+ * files reach a Vercel deployment.
10
+ *
11
+ * WHAT THIS FILE IS FOR. That shape is correct for the deploy path the scaffold
12
+ * ships, and it cannot be made to work under a BUNDLER. esbuild bundles a
13
+ * dynamic `import()` iff its argument is a string literal at the call site —
14
+ * exactly the predicate `tsc` resolves on, and exactly the one #5182 D1
15
+ * requires be false. So in a bundled build the specifier survives verbatim and
16
+ * Node resolves it against the BUNDLE OUTPUT directory rather than the source
17
+ * module's: `"./list.js"`, written in `routes/project-listings/index.ts`,
18
+ * becomes `dist/list.js` and does not exist. `build()` throws during
19
+ * CONSTRUCTION, so every route 500s rather than one.
20
+ *
21
+ * The failure is otherwise silent on every channel: esbuild emits no warning
22
+ * for the imports it declines to bundle, and a consuming frontend degrades to
23
+ * an empty list and still answers 200. `salessmyth/samhenry` spent nine hours
24
+ * on it. This boundary is what turns that into a one-line diagnosis — it does
25
+ * not make bundling work, because nothing in this file could.
26
+ *
27
+ * WHY IT TAKES A THUNK AND NOT A SPECIFIER. `loadSlice("./list.js")` doing the
28
+ * `import()` in here would be the obvious signature and would silently undo
29
+ * #5304. `enumerateSliceSpecifiers()` treats a file as an "indirect file" only
30
+ * if it carries a dynamic `import()` with a non-literal argument; move the call
31
+ * out of the callers and they stop qualifying, their specifier constants stop
32
+ * being candidates, and `check-slice-reachability.ts` passes vacuously over an
33
+ * empty set. Worse, `@vercel/nft` constant-propagates `WORKSPACES_INDEX` into
34
+ * `import(WORKSPACES_INDEX)` only because the constant and the call sit in one
35
+ * module — hoist the call and there is nothing to propagate, so every slice
36
+ * file goes untraced and #5304's outage returns with its own gate green.
37
+ *
38
+ * So the `import()` stays at the call site, in the caller's module, and this
39
+ * function only awaits the thunk and interprets the failure.
40
+ *
41
+ * Gated by `scripts/check-bundled-build-diagnoses.ts`, which bundles `apps/api`
42
+ * and asserts that the boot fails WITH this diagnostic rather than without one.
43
+ */
44
+
45
+ /**
46
+ * Node reports an unresolvable ESM specifier as `ERR_MODULE_NOT_FOUND` and an
47
+ * unresolvable CJS one as `MODULE_NOT_FOUND`. A bundle can produce either,
48
+ * since a bundled graph mixes both.
49
+ */
50
+ const MODULE_NOT_FOUND_CODES = new Set(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"]);
51
+
52
+ function isModuleNotFound(error: unknown): boolean {
53
+ const code = (error as { code?: unknown } | null | undefined)?.code;
54
+ return typeof code === "string" && MODULE_NOT_FOUND_CODES.has(code);
55
+ }
56
+
57
+ /**
58
+ * Await a slice module's loader thunk, turning an unresolvable module into a
59
+ * diagnosis instead of a bare `ERR_MODULE_NOT_FOUND` on a path that never
60
+ * existed.
61
+ *
62
+ * Anything that is not a module-resolution failure is rethrown UNTOUCHED — the
63
+ * same object, not a wrapper. A slice that throws while evaluating is a real
64
+ * bug in the slice and must keep its own stack and type.
65
+ *
66
+ * @param specifier the module specifier the thunk imports, for the message.
67
+ * Every call site already has it in scope, so nothing is enumerated here.
68
+ * @param load the caller's own `() => import(CONST)` thunk. Must close over a
69
+ * constant in the CALLER's module — see the file header.
70
+ */
71
+ export async function loadSlice(
72
+ specifier: string,
73
+ load: () => Promise<unknown>,
74
+ ): Promise<unknown> {
75
+ try {
76
+ return await load();
77
+ } catch (error) {
78
+ if (!isModuleNotFound(error)) throw error;
79
+ throw new Error(
80
+ `AILK slice module '${specifier}' could not be resolved at runtime. ` +
81
+ `This throws during server construction, so every route fails, not one. Two causes produce it:\n` +
82
+ ` 1. The module was compiled but not SHIPPED — the deploy-time file tracer did not emit it (#5304). ` +
83
+ `Check with \`pnpm slice-reachability:check\`.\n` +
84
+ ` 2. This build was BUNDLED. apps/api does not support bundling (#5334): a bundler resolves this ` +
85
+ `specifier against the bundle's own output directory rather than its source module's, and esbuild ` +
86
+ `leaves it unbundled without a warning. See apps/api/CLAUDE.md § Deploy path.`,
87
+ { cause: error },
88
+ );
89
+ }
90
+ }
@@ -28,10 +28,10 @@ describe("GET /api/openapi.json", () => {
28
28
 
29
29
  const spec = JSON.parse(res.body);
30
30
  expect(spec.openapi).toBe("3.1.0");
31
- expect(spec.info).toMatchObject({
32
- title: "AI Launch KitPublic Content API",
33
- version: "1.0.0",
34
- });
31
+ // The title is resolved from the serving site's own `_site.mdx` (#5307),
32
+ // so assert its SHAPE rather than a literal site name the same build
33
+ // serves a different title on every consuming site.
34
+ expect(spec.info.title).toMatch(/Public Content API$/);
35
35
  expect(spec.info.version).toBe("1.0.0");
36
36
  expect(typeof spec.paths).toBe("object");
37
37
  });
@@ -54,39 +54,59 @@ describe("GET /api/openapi.json", () => {
54
54
  );
55
55
  });
56
56
 
57
- it("documents the checkout sessions endpoint with operationId createCheckoutSession", async () => {
58
- const res = await app.inject({
59
- method: "GET",
60
- url: "/v1/openapi.json",
57
+ // The checkout path's OpenAPI documentation is module-conditional (commerce,
58
+ // D-47 / #5364) — mirroring the route's own registration gate in server.ts —
59
+ // so these two assertions need the module enabled, exactly as a real
60
+ // commerce-installed deployment would have it.
61
+ describe("checkout endpoint (commerce module enabled)", () => {
62
+ const PRIOR_ENABLED_MODULES = process.env.AILK_ENABLED_MODULES;
63
+
64
+ beforeEach(() => {
65
+ process.env.AILK_ENABLED_MODULES = "commerce";
61
66
  });
62
67
 
63
- const spec = JSON.parse(res.body);
64
- expect(spec.paths["/v1/checkout/sessions"].post.operationId).toBe(
65
- "createCheckoutSession",
66
- );
67
- });
68
+ afterEach(() => {
69
+ if (PRIOR_ENABLED_MODULES === undefined) {
70
+ delete process.env.AILK_ENABLED_MODULES;
71
+ } else {
72
+ process.env.AILK_ENABLED_MODULES = PRIOR_ENABLED_MODULES;
73
+ }
74
+ });
68
75
 
69
- it("documents 201, 400, 404, 500, 502 responses for the checkout endpoint", async () => {
70
- const res = await app.inject({
71
- method: "GET",
72
- url: "/v1/openapi.json",
76
+ it("documents the checkout sessions endpoint with operationId createCheckoutSession", async () => {
77
+ const res = await app.inject({
78
+ method: "GET",
79
+ url: "/v1/openapi.json",
80
+ });
81
+
82
+ const spec = JSON.parse(res.body);
83
+ expect(spec.paths["/v1/checkout/sessions"].post.operationId).toBe(
84
+ "createCheckoutSession",
85
+ );
73
86
  });
74
87
 
75
- const spec = JSON.parse(res.body);
76
- const responses = spec.paths["/v1/checkout/sessions"].post.responses;
77
- expect(responses["201"].description).toBe("Checkout session created");
78
- expect(responses["400"].description).toBe(
79
- "Validation error — invalid request body",
80
- );
81
- expect(responses["404"].description).toBe(
82
- "Unknown product code — no active Stripe price for the given productCode",
83
- );
84
- expect(responses["500"].description).toBe(
85
- "Live-mode forbiddenSTRIPE_SECRET_KEY is a live key",
86
- );
87
- expect(responses["502"].description).toBe(
88
- "Checkout unavailable — Stripe returned an unexpected error",
89
- );
88
+ it("documents 201, 400, 404, 500, 502 responses for the checkout endpoint", async () => {
89
+ const res = await app.inject({
90
+ method: "GET",
91
+ url: "/v1/openapi.json",
92
+ });
93
+
94
+ const spec = JSON.parse(res.body);
95
+ const responses = spec.paths["/v1/checkout/sessions"].post.responses;
96
+ expect(responses["201"].description).toBe("Checkout session created");
97
+ expect(responses["400"].description).toBe(
98
+ "Validation errorinvalid request body",
99
+ );
100
+ expect(responses["404"].description).toBe(
101
+ "Unknown product code no active Stripe price for the given productCode",
102
+ );
103
+ expect(responses["500"].description).toBe(
104
+ "Live-mode forbidden — STRIPE_SECRET_KEY is a live key",
105
+ );
106
+ expect(responses["502"].description).toBe(
107
+ "Checkout unavailable — Stripe returned an unexpected error",
108
+ );
109
+ });
90
110
  });
91
111
 
92
112
  it("declares query and path parameters for content endpoints", async () => {
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @file spec-identity.test.ts
3
+ *
4
+ * The served API's self-description (#5307) and the `servers` block (#5306).
5
+ *
6
+ * These assert the BUILDER's output given a config rather than a deployed
7
+ * response: the defect is that a deployed site serves the kit's identity and
8
+ * the kit's host, and the builder is the only place that can be proved
9
+ * without deploying.
10
+ */
11
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import {
16
+ API_VERSION,
17
+ apiDescription,
18
+ apiDisplayName,
19
+ buildOpenApiSpec,
20
+ resetApiIdentity,
21
+ resolveApiIdentity,
22
+ } from "../spec";
23
+
24
+ /** Write a throwaway content root carrying the given `_site.mdx` frontmatter. */
25
+ function withContentRoot(frontmatter: string): string {
26
+ const root = mkdtempSync(join(tmpdir(), "ailk-site-"));
27
+ mkdirSync(root, { recursive: true });
28
+ writeFileSync(join(root, "_site.mdx"), `---\n${frontmatter}\n---\n\nbody\n`);
29
+ return root;
30
+ }
31
+
32
+ describe("buildOpenApiSpec — site identity (#5307)", () => {
33
+ it("titles the spec after the site, not after the kit", () => {
34
+ const spec = buildOpenApiSpec({ siteName: "Sam Henry" });
35
+
36
+ expect(spec.info.title).toBe("Sam Henry — Public Content API");
37
+ expect(JSON.stringify(spec.info)).not.toMatch(/AI Launch Kit|AILK/i);
38
+ });
39
+
40
+ it("degrades to a named neutral title when no site name is resolvable", () => {
41
+ const spec = buildOpenApiSpec({});
42
+
43
+ expect(spec.info.title).toBe("Public Content API");
44
+ expect(spec.info.title).not.toMatch(/AI Launch Kit|AILK/i);
45
+ });
46
+
47
+ it("asserts no licence over a deployment that has not chosen one", () => {
48
+ expect(buildOpenApiSpec({ siteName: "Sam Henry" }).info).not.toHaveProperty(
49
+ "license",
50
+ );
51
+ });
52
+
53
+ it("keeps API_VERSION as the /v1 contract version", () => {
54
+ expect(API_VERSION).toBe("1.0.0");
55
+ expect(buildOpenApiSpec({}).info.version).toBe(API_VERSION);
56
+ });
57
+ });
58
+
59
+ describe("buildOpenApiSpec — servers block (#5306)", () => {
60
+ it("names the deployment's own API origin", () => {
61
+ const spec = buildOpenApiSpec({ publicApiUrl: "https://api.samhenry.org" });
62
+
63
+ expect(spec.servers).toEqual([
64
+ { url: "https://api.samhenry.org", description: expect.any(String) },
65
+ ]);
66
+ });
67
+
68
+ it("never serves the kit's own host", () => {
69
+ const spec = buildOpenApiSpec({
70
+ siteName: "Sam Henry",
71
+ publicApiUrl: "https://api.samhenry.org",
72
+ });
73
+
74
+ expect(JSON.stringify(spec)).not.toContain("ailaunchkit.ai");
75
+ });
76
+
77
+ it("normalises a trailing slash", () => {
78
+ expect(
79
+ buildOpenApiSpec({ publicApiUrl: "https://api.samhenry.org/" }).servers[0]
80
+ ?.url,
81
+ ).toBe("https://api.samhenry.org");
82
+ });
83
+
84
+ it("degrades to a document-relative origin when the value is unset", () => {
85
+ const spec = buildOpenApiSpec({});
86
+
87
+ expect(spec.servers).toHaveLength(1);
88
+ expect(spec.servers[0]?.url).toBe("/");
89
+ // The degradation is named in the served document itself, so an agent
90
+ // reading the spec learns why rather than following a wrong absolute URL.
91
+ expect(spec.servers[0]?.description).toMatch(/AILK_PUBLIC_API_URL/);
92
+ });
93
+
94
+ it("degrades rather than serving a malformed origin", () => {
95
+ expect(buildOpenApiSpec({ publicApiUrl: "not-a-url" }).servers[0]?.url).toBe(
96
+ "/",
97
+ );
98
+ expect(
99
+ buildOpenApiSpec({ publicApiUrl: "ftp://api.example.com" }).servers[0]
100
+ ?.url,
101
+ ).toBe("/");
102
+ });
103
+ });
104
+
105
+ describe("resolveApiIdentity — reads _site.mdx", () => {
106
+ const originalContentRoot = process.env.CONTENT_ROOT;
107
+ let root: string | undefined;
108
+
109
+ afterEach(() => {
110
+ if (originalContentRoot === undefined) delete process.env.CONTENT_ROOT;
111
+ else process.env.CONTENT_ROOT = originalContentRoot;
112
+ if (root) rmSync(root, { recursive: true, force: true });
113
+ root = undefined;
114
+ resetApiIdentity();
115
+ });
116
+
117
+ it("sources the site name and description from _site.mdx", () => {
118
+ root = withContentRoot(
119
+ [
120
+ 'version: "1"',
121
+ "siteName: Sam Henry",
122
+ "baseUrl: https://samhenry.org",
123
+ "siteIdentity:",
124
+ " kind: Person",
125
+ " name: Sam Henry",
126
+ " description: Independent software consultant.",
127
+ ].join("\n"),
128
+ );
129
+ process.env.CONTENT_ROOT = root;
130
+ resetApiIdentity();
131
+
132
+ const identity = resolveApiIdentity();
133
+
134
+ expect(identity.siteName).toBe("Sam Henry");
135
+ expect(identity.siteDescription).toBe("Independent software consultant.");
136
+ expect(apiDisplayName(identity)).toBe("Sam Henry API");
137
+ expect(apiDescription(identity)).toBe("Independent software consultant.");
138
+ });
139
+
140
+ // #5306 AC-3 — the trap this card exists to refuse.
141
+ it("does NOT use _site.mdx baseUrl as the API origin", () => {
142
+ root = withContentRoot(
143
+ [
144
+ 'version: "1"',
145
+ "siteName: Sam Henry",
146
+ // The SITE origin. The API is deployed on a different host
147
+ // (api.samhenry.org), so this value must never reach `servers`.
148
+ "baseUrl: https://samhenry.org",
149
+ ].join("\n"),
150
+ );
151
+ process.env.CONTENT_ROOT = root;
152
+ resetApiIdentity();
153
+
154
+ const identity = resolveApiIdentity();
155
+ expect(identity.publicApiUrl).not.toBe("https://samhenry.org");
156
+
157
+ const spec = buildOpenApiSpec({ ...identity, publicApiUrl: undefined });
158
+ expect(JSON.stringify(spec.servers)).not.toContain("samhenry.org");
159
+ });
160
+
161
+ it("degrades to a named neutral default when _site.mdx is unreadable", () => {
162
+ root = mkdtempSync(join(tmpdir(), "ailk-empty-"));
163
+ process.env.CONTENT_ROOT = root;
164
+ resetApiIdentity();
165
+
166
+ const identity = resolveApiIdentity();
167
+
168
+ expect(identity.siteName).toBeUndefined();
169
+ expect(apiDisplayName(identity)).toBe("Content API");
170
+ expect(apiDisplayName(identity)).not.toMatch(/AI Launch Kit|AILK/i);
171
+ expect(buildOpenApiSpec(identity).info.title).toBe("Public Content API");
172
+ });
173
+
174
+ it("degrades when _site.mdx frontmatter is malformed", () => {
175
+ root = withContentRoot("siteName: [unclosed");
176
+ process.env.CONTENT_ROOT = root;
177
+ resetApiIdentity();
178
+
179
+ expect(resolveApiIdentity().siteName).toBeUndefined();
180
+ expect(apiDisplayName(resolveApiIdentity())).toBe("Content API");
181
+ });
182
+ });