create-ailk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/cli.js +19 -9
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +51 -3
  6. package/dist/lib/extract-module-bundle.js +327 -28
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/fetch-template.d.ts +20 -0
  10. package/dist/lib/fetch-template.js +129 -0
  11. package/dist/lib/module-license-gate.d.ts +10 -0
  12. package/dist/lib/module-license-gate.js +13 -4
  13. package/dist/lib/paid-paths.d.ts +16 -0
  14. package/dist/lib/paid-paths.js +90 -0
  15. package/dist/module-architecture.d.ts +197 -23
  16. package/dist/module-architecture.js +820 -25
  17. package/dist/parse-args.d.ts +9 -2
  18. package/dist/parse-args.js +46 -1
  19. package/dist/programmatic.d.ts +28 -2
  20. package/dist/programmatic.js +115 -8
  21. package/dist/surfaces.d.ts +12 -36
  22. package/dist/surfaces.js +77 -5
  23. package/dist/sync-routes.js +31 -1
  24. package/dist/template-manifest.d.ts +92 -0
  25. package/dist/template-manifest.js +168 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +44 -0
  28. package/package.json +3 -2
  29. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  30. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  31. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site-free/provenance.json +4 -0
  34. package/templates/.claude/agents/web.md +2 -3
  35. package/templates/.claude/rules/architecture.md +5 -5
  36. package/templates/.claude/skills/README.md +2 -1
  37. package/templates/.env.example +21 -9
  38. package/templates/CONVENTIONS.md +6 -7
  39. package/templates/apps/api/.env.example +9 -0
  40. package/templates/apps/api/CLAUDE.md +33 -5
  41. package/templates/apps/api/package.json +0 -1
  42. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  43. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  44. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  45. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  46. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  47. package/templates/apps/api/src/config/index.ts +19 -0
  48. package/templates/apps/api/src/config/modules.ts +12 -14
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  50. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  51. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  52. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  53. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  54. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  55. package/templates/apps/api/src/openapi/index.ts +9 -3
  56. package/templates/apps/api/src/openapi/spec.ts +505 -65
  57. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  58. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  59. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  60. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  61. package/templates/apps/api/src/routes/content/index.ts +38 -11
  62. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  63. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  64. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  65. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  66. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  67. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  68. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  69. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  70. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  71. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  72. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  73. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  74. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  75. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  76. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  77. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  78. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  79. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  80. package/templates/apps/api/src/server.ts +504 -137
  81. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  82. package/templates/apps/api/src/services/listing-config.ts +58 -0
  83. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  84. package/templates/apps/api/src/services/project-listings.ts +229 -4
  85. package/templates/apps/api/src/vercel-handler.ts +60 -26
  86. package/templates/apps/mcp/.env.example +8 -0
  87. package/templates/apps/mcp/CLAUDE.md +2 -2
  88. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  89. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  90. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  91. package/templates/apps/mcp/src/config/modules.ts +14 -13
  92. package/templates/apps/mcp/src/server.ts +8 -4
  93. package/templates/apps/mcp/src/tools/index.ts +7 -22
  94. package/templates/apps/web/.env.example +15 -0
  95. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  96. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  97. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  102. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  105. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  106. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  107. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  108. package/templates/apps/web/jest.config.cjs +19 -13
  109. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  110. package/templates/apps/web/lib/server-api.ts +28 -12
  111. package/templates/apps/web/messages/en.json +58 -0
  112. package/templates/apps/web/next.config.mjs +1 -3
  113. package/templates/apps/web/package.json +0 -6
  114. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  115. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  116. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  117. package/templates/apps/web/public/favicon-16x16.png +0 -0
  118. package/templates/apps/web/public/favicon-32x32.png +0 -0
  119. package/templates/apps/web/public/favicon.ico +0 -0
  120. package/templates/apps/web/public/favicon.svg +6 -3
  121. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  122. package/templates/apps/web/public/logomark.svg +4 -0
  123. package/templates/apps/web/public/site.webmanifest +2 -2
  124. package/templates/apps/web/public/wordmark.svg +4 -0
  125. package/templates/database/CHANGELOG.md +47 -0
  126. package/templates/database/inbox/schema.prisma +67 -0
  127. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  128. package/templates/database/package.json +1 -1
  129. package/templates/package.json +1 -1
  130. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  131. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  132. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  133. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  134. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  135. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  136. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  137. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  138. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  139. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  140. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  141. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  142. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  143. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  144. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  145. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  146. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  147. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  148. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  149. package/templates/apps/api/src/lib/stripe.ts +0 -52
  150. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  151. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  152. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  153. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  154. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  155. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  156. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  157. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  158. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  159. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  160. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  161. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  162. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  163. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  164. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  165. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  166. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  167. package/templates/apps/api/src/routes/flows/README.md +0 -147
  168. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  169. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  170. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  171. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  172. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  173. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  174. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  175. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  176. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  177. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  178. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  179. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  180. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  181. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  182. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  183. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  184. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  185. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  186. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  187. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  188. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  189. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  190. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  191. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  192. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  193. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  194. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  195. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  196. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  197. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  198. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  199. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  200. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  201. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  202. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  203. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  204. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  205. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  206. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  207. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  208. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  209. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  210. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  211. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  212. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  213. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  214. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  215. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  216. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  217. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  218. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  219. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  220. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  221. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  222. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  223. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  224. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  225. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  226. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  227. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  228. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  229. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  230. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  231. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  232. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  233. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  234. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  235. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  236. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  237. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  238. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  239. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  240. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  241. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  242. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  243. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  244. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  245. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  246. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  247. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  248. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  249. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  250. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  258. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  259. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  260. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  261. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  266. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  267. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  268. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  269. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  270. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  271. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  272. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  273. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  274. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  275. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  276. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  277. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  278. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  279. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  280. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  281. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  282. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  283. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  284. package/templates/apps/web/content/en/blog/README.txt +0 -25
  285. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  286. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  287. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  288. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  289. package/templates/apps/web/lib/source.ts +0 -34
  290. package/templates/apps/web/public/logomark-dark.svg +0 -4
  291. package/templates/apps/web/public/logomark-light.svg +0 -4
  292. package/templates/apps/web/source.config.ts +0 -78
@@ -28,26 +28,243 @@
28
28
  * `extractModuleBundle` therefore extracts into a throwaway temp dir first
29
29
  * (same validation, same guards), and ONLY on full success copies each
30
30
  * validated file into its place under `targetDir` — so a rejected or
31
- * partially-decompressed bundle never touches the scaffold at all.
31
+ * partially-decompressed bundle never touches the scaffold at all. This
32
+ * guarantee extends all the way through `patches` (below): NOTHING is
33
+ * written to `targetDir` — not the bundle's own plain content, not an
34
+ * `installMap` destination, not a patched file — until every patch target
35
+ * has been computed successfully in memory.
36
+ *
37
+ * `installMap` / `postInstall` (spec 5373 § D7): a bundle may carry an
38
+ * `ailk-module.json` at its root, `{name, version, installMap, postInstall}`.
39
+ * `installMap` remaps specific staged files to a different scaffold
40
+ * destination (e.g. the docs-blog `llms-route.ts` landing on
41
+ * `app/llms.txt/route.ts`) — every source must be a file the validated
42
+ * extraction produced in staging (an absolute path, a `..` segment, or a
43
+ * path absent from staging rejects the WHOLE bundle, nothing copied), and
44
+ * every destination gets the same fail-closed containment check as a tar
45
+ * entry. A mapped source is copied to its destination only, never also to
46
+ * its original path. `postInstall` is printed to stdout after a successful
47
+ * merge, with C0/C1 control characters (other than newline) stripped first
48
+ * so a bundle cannot inject terminal escape sequences. `ailk-module.json`
49
+ * itself is never copied into the scaffold.
50
+ *
51
+ * `patches` (spec 5418 § 3 D1-D6): a sibling manifest field naming
52
+ * declarative edits to files the buyer's scaffold already has (or that this
53
+ * SAME bundle is about to deliver — see below). Validated at the same
54
+ * fail-closed point as `installMap` (D6), then COMPUTED — every patch
55
+ * target's new content resolved in memory, throwing on the first conflict /
56
+ * missing-target / unparseable-target failure (D4) — before `copyTree` or
57
+ * the `installMap` copy loop runs at all. A patch target the bundle itself
58
+ * delivers (as plain content or as an `installMap` destination) is read
59
+ * from the STAGED tree for this computation, since `targetDir` would not
60
+ * have it yet; every other target is read from the already-scaffolded
61
+ * `targetDir` file. Only once every target computes successfully does
62
+ * `copyTree`/`installMap` run, followed by one write pass for the computed
63
+ * patches — so a patch failure leaves `targetDir` exactly as it was before
64
+ * this call, including files THIS bundle would otherwise have added.
65
+ *
66
+ * SITE TEMPLATES (spec 5481; parent record D9, D10, DD4, DD7). A template's
67
+ * root manifest is `ailk-template.json` — `ailk-module.json`'s shape plus a
68
+ * mandatory `requires`, cross-checked against the client registry's list so a
69
+ * stale client cannot land a template built for a different module set. Two
70
+ * differences from the module path, both deliberate and both confined to
71
+ * reading the manifest: a template's manifest MUST be present and parseable
72
+ * (a module's absent-or-unparseable manifest legitimately degrades to a plain
73
+ * content merge; a template's `requires` is mandatory, so treat-as-absent
74
+ * would make the cross-check optional at the bundle's own discretion), and a
75
+ * `requires` disagreement rejects. Everything after that — traversal,
76
+ * containment, link rejection, the limits, the atomicity ordering — is the
77
+ * same code, reached through `installBundleTree`, which the keyless Free path
78
+ * calls directly with `site-templates/<name>/` as its source root.
32
79
  */
33
- import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
80
+ import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
34
81
  import { tmpdir } from "node:os";
35
82
  import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
36
83
  import { pipeline } from "node:stream/promises";
37
84
  import { createGunzip } from "node:zlib";
38
85
  import * as tar from "tar";
86
+ import { computeModulePatches, writeModulePatches } from "./apply-module-patches.js";
39
87
  /** Soft limits to prevent decompression-bomb / zip-bomb attacks. Same values as extractBundle. */
40
88
  const MAX_ENTRIES = 1000;
41
89
  const MAX_TOTAL_SIZE_BYTES = 50 * 1024 * 1024; // 50 MiB
90
+ /** The bundle-root manifest file (spec § D7). Never copied into the scaffold. */
91
+ const AILK_MODULE_JSON = "ailk-module.json";
92
+ /**
93
+ * A SITE TEMPLATE's root manifest (spec 5481 § 4; parent record D10) — the
94
+ * same shape as `ailk-module.json` plus one mandatory field, `requires`.
95
+ * Never copied into the scaffold either.
96
+ */
97
+ const AILK_TEMPLATE_JSON = "ailk-template.json";
98
+ /** The op names valid for each patch `kind` (spec 5418 § 3 D1's ModulePatchOp union, partitioned). */
99
+ const VALID_OPS_BY_KIND = {
100
+ json: new Set(["merge-keys", "append-unique"]),
101
+ "next-config": new Set(["add-transpile-package", "wrap-export-with"]),
102
+ "jest-config": new Set([
103
+ "add-module-name-mapper",
104
+ "add-transform-ignore-pattern",
105
+ "add-transform",
106
+ ]),
107
+ };
108
+ /** True iff `child` is the same path as `parent` or a descendant of it. */
109
+ function isPathInside(parent, child) {
110
+ const rel = relative(parent, child);
111
+ if (rel === "")
112
+ return true;
113
+ if (rel.startsWith(".."))
114
+ return false;
115
+ if (isAbsolute(rel))
116
+ return false;
117
+ return true;
118
+ }
119
+ /**
120
+ * Reject an absolute path or any `..` segment — the same shape check
121
+ * `safeExtractTarGz` applies to tar entries, reused here for `installMap`
122
+ * keys/values, which come from bundle-controlled JSON rather than tar
123
+ * entries and so need the same fail-closed treatment.
124
+ */
125
+ function hasUnsafeSegment(p) {
126
+ if (isAbsolute(p))
127
+ return true;
128
+ const normalized = normalize(p);
129
+ return (normalized === ".." ||
130
+ normalized.startsWith(`..${sep}`) ||
131
+ normalized.split(sep).includes(".."));
132
+ }
133
+ /**
134
+ * Read `ailk-module.json` from the validated staging tree, if present.
135
+ * Malformed JSON is treated as absent — the manifest is written by the
136
+ * producer (D7), never hand-authored, so a parse failure is a producer bug,
137
+ * not a security boundary to fail closed on with a rejected install.
138
+ */
139
+ function readAilkModuleManifest(extractedRoot) {
140
+ const manifestPath = join(extractedRoot, AILK_MODULE_JSON);
141
+ if (!existsSync(manifestPath))
142
+ return null;
143
+ try {
144
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ /**
151
+ * Read `ailk-template.json` from a template tree's root and cross-check its
152
+ * `requires` against the client registry's list for that template.
153
+ *
154
+ * REFUSES where `readAilkModuleManifest` degrades (spec 5481 § 3 DD4). A
155
+ * module's manifest fields are all optional, so an absent or unparseable one
156
+ * legitimately means "a plain content merge". A template's `requires` is
157
+ * mandatory (record D10) — inheriting treat-as-absent would make the
158
+ * cross-check optional at the bundle's own discretion, since a bundle
159
+ * carrying no manifest would never be checked at all.
160
+ *
161
+ * The disagreement check is a boundary check, and the boundary is the bundle:
162
+ * `expectedRequires` comes from the caller's own `TEMPLATE_MANIFESTS` entry
163
+ * (internal, compile-checked against `RECOGNIZED_MODULES`), while `requires`
164
+ * is whatever the archive says. Compared as SETS, not as sequences — the
165
+ * registry's declaration order is not part of the contract.
166
+ */
167
+ function readAilkTemplateManifest(sourceRoot, name, expectedRequires) {
168
+ const manifestPath = join(sourceRoot, AILK_TEMPLATE_JSON);
169
+ if (!existsSync(manifestPath)) {
170
+ throw new Error(`template "${name}": bundle carries no ${AILK_TEMPLATE_JSON} — a template manifest is mandatory`);
171
+ }
172
+ let manifest;
173
+ try {
174
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
175
+ }
176
+ catch {
177
+ throw new Error(`template "${name}": ${AILK_TEMPLATE_JSON} is not readable JSON`);
178
+ }
179
+ const declared = manifest.requires;
180
+ if (!Array.isArray(declared)) {
181
+ throw new Error(`template "${name}": ${AILK_TEMPLATE_JSON} declares no "requires" list`);
182
+ }
183
+ const expected = [...new Set(expectedRequires)].sort();
184
+ const actual = [...new Set(declared)].sort();
185
+ if (expected.join(",") !== actual.join(",")) {
186
+ throw new Error(`template "${name}": bundle requires [${actual.join(", ")}] but this create-ailk's registry requires [${expected.join(", ")}] — upgrade create-ailk`);
187
+ }
188
+ return manifest;
189
+ }
190
+ /**
191
+ * Validate every `installMap` entry against the staged tree and the target
192
+ * scaffold, throwing on the first violation — called BEFORE any copy, so a
193
+ * bad entry rejects the whole bundle with nothing written to `targetDir`.
194
+ * Returns the map unchanged (validation only) for the caller to apply.
195
+ */
196
+ function validateInstallMap(installMap, extractedRoot, targetAbs, subject) {
197
+ for (const [source, destination] of Object.entries(installMap)) {
198
+ if (hasUnsafeSegment(source)) {
199
+ throw new Error(`${subject}: unsafe installMap source "${source}"`);
200
+ }
201
+ const sourceAbs = resolve(extractedRoot, source);
202
+ if (!isPathInside(extractedRoot, sourceAbs)) {
203
+ throw new Error(`${subject}: installMap source "${source}" resolves outside staging`);
204
+ }
205
+ let sourceStat;
206
+ try {
207
+ sourceStat = statSync(sourceAbs);
208
+ }
209
+ catch {
210
+ throw new Error(`${subject}: installMap source "${source}" is not a file the bundle produced`);
211
+ }
212
+ if (!sourceStat.isFile()) {
213
+ throw new Error(`${subject}: installMap source "${source}" is not a file the bundle produced`);
214
+ }
215
+ if (hasUnsafeSegment(destination)) {
216
+ throw new Error(`${subject}: unsafe installMap destination "${destination}"`);
217
+ }
218
+ const destAbs = resolve(targetAbs, destination);
219
+ if (!isPathInside(targetAbs, destAbs)) {
220
+ throw new Error(`${subject}: installMap destination "${destination}" resolves outside target`);
221
+ }
222
+ }
223
+ }
224
+ /**
225
+ * Validate every `patches` entry against `targetDir` — the same fail-closed
226
+ * point and containment guard `validateInstallMap` above uses, extended
227
+ * (spec 5418 § 3 D6): `target` must resolve inside `targetDir`; `kind` and
228
+ * every `op.op` must be a recognized vocabulary member. Any violation
229
+ * refuses the WHOLE bundle before `copyTree` runs — this is D4 class 4
230
+ * (unknown kind/op), resolved here at validation time, never inside
231
+ * `applyModulePatches` (which never sees an invalid kind/op).
232
+ */
233
+ function validatePatches(patches, targetAbs, subject) {
234
+ for (const patch of patches) {
235
+ if (hasUnsafeSegment(patch.target)) {
236
+ throw new Error(`${subject}: unsafe patch target "${patch.target}"`);
237
+ }
238
+ const targetAbsPath = resolve(targetAbs, patch.target);
239
+ if (!isPathInside(targetAbs, targetAbsPath)) {
240
+ throw new Error(`${subject}: patch target "${patch.target}" resolves outside target`);
241
+ }
242
+ const validOps = VALID_OPS_BY_KIND[patch.kind];
243
+ if (!validOps) {
244
+ throw new Error(`invalid module manifest: unknown patch kind "${patch.kind}" for target ${patch.target}`);
245
+ }
246
+ for (const op of patch.ops) {
247
+ if (!validOps.has(op.op)) {
248
+ throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${patch.target}`);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ /** C0 (0x00-0x1F) and C1 (0x7F-0x9F) control characters, newline excepted. */
254
+ function stripControlCharacters(text) {
255
+ // eslint-disable-next-line no-control-regex -- deliberately stripping control chars
256
+ return text.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, "");
257
+ }
42
258
  /**
43
259
  * Extract a fetched module bundle into `targetDir`, merging its files onto
44
260
  * the tree `copyTemplate` already wrote. Throws on any unsafe entry,
45
- * decompress error, or limit overrun — `targetDir` is left untouched on
46
- * failure (validation completes in a throwaway staging dir before any file
47
- * is copied into place).
261
+ * decompress error, limit overrun, invalid `installMap`, or `patches`
262
+ * failure (D4) — `targetDir` is left untouched on failure in every one of
263
+ * those cases: validation AND patch computation both complete before any
264
+ * file is copied or written into place.
48
265
  */
49
266
  export async function extractModuleBundle(options) {
50
- const { bytes, targetDir, moduleName } = options;
267
+ const { bytes, targetDir, moduleName, templateRequires } = options;
51
268
  const targetAbs = resolve(targetDir);
52
269
  if (!existsSync(targetAbs)) {
53
270
  throw new Error(`extractModuleBundle: target directory does not exist: ${targetAbs}`);
@@ -58,23 +275,114 @@ export async function extractModuleBundle(options) {
58
275
  writeFileSync(stagingTar, bytes);
59
276
  const extractedRoot = join(stagingDir, "extracted");
60
277
  mkdirSync(extractedRoot, { recursive: true });
61
- await safeExtractTarGz(stagingTar, extractedRoot, moduleName);
62
- // Full validation passed — merge into targetDir. Copies only (never
63
- // deletes anything): a fetched module's files ADD to the free scaffold,
64
- // they never remove what the free copy already wrote.
65
- copyTree(extractedRoot, targetAbs);
278
+ await safeExtractTarGz(stagingTar, extractedRoot, templateRequires !== undefined
279
+ ? `template "${moduleName}"`
280
+ : `module "${moduleName}"`);
281
+ installBundleTree({
282
+ sourceRoot: extractedRoot,
283
+ targetDir: targetAbs,
284
+ name: moduleName,
285
+ ...(templateRequires !== undefined ? { templateRequires } : {}),
286
+ ...(options.versions !== undefined ? { versions: options.versions } : {}),
287
+ });
66
288
  }
67
289
  finally {
68
290
  rmSync(stagingDir, { recursive: true, force: true });
69
291
  }
70
292
  }
293
+ /**
294
+ * Apply one resolved manifest's `installMap`, plain content and `patches`
295
+ * from `sourceRoot` into `targetDir`.
296
+ *
297
+ * Extracted from `extractModuleBundle` so the Free template path — which has
298
+ * no archive to unpack, only a local tree — runs the SAME step (DD7). The
299
+ * atomicity guarantee is this function's, not its callers': validation AND
300
+ * patch computation both complete before the first byte is written, so a
301
+ * rejected tree leaves `targetDir` exactly as it was.
302
+ */
303
+ export function installBundleTree(options) {
304
+ const { sourceRoot, name, templateRequires } = options;
305
+ const targetAbs = resolve(options.targetDir);
306
+ const isTemplate = templateRequires !== undefined;
307
+ const subject = isTemplate ? `template "${name}"` : `module "${name}"`;
308
+ // installMap validation runs BEFORE any copy, so a bad entry rejects the
309
+ // whole tree with nothing written to targetDir (same posture as the
310
+ // tar-entry validation in safeExtractTarGz).
311
+ const manifest = isTemplate
312
+ ? readAilkTemplateManifest(sourceRoot, name, templateRequires)
313
+ : readAilkModuleManifest(sourceRoot);
314
+ const installMap = manifest?.installMap ?? {};
315
+ validateInstallMap(installMap, sourceRoot, targetAbs, subject);
316
+ // patches validation (spec 5418 § 3 D6) runs at the same point, before
317
+ // any copy — a bad target/kind/op rejects the whole tree with nothing
318
+ // written to targetDir, same posture as installMap and the tar-entry
319
+ // validation above.
320
+ const patches = manifest?.patches ?? [];
321
+ validatePatches(patches, targetAbs, subject);
322
+ // Compute every patch target's new content BEFORE any write to
323
+ // targetDir — including before copyTree/installMap below, not only
324
+ // before the patch step's own write (spec 5418 § 3 D4/§ 6.2): a
325
+ // conflict, missing-target, or unparseable-target failure must leave
326
+ // the scaffold exactly as it was, which includes the bundle's own
327
+ // plain content and installMap destinations never landing either. A
328
+ // patch target the bundle itself delivers (as plain content or as an
329
+ // installMap destination) is read from the STAGED tree here, since
330
+ // targetDir would not have it yet at this point.
331
+ const installMapDestinationToSource = new Map(Object.entries(installMap).map(([source, destination]) => [destination, source]));
332
+ const readPatchTarget = (target) => {
333
+ const mappedSource = installMapDestinationToSource.get(target);
334
+ if (mappedSource !== undefined) {
335
+ const sourceAbs = resolve(sourceRoot, mappedSource);
336
+ return existsSync(sourceAbs) ? readFileSync(sourceAbs, "utf8") : undefined;
337
+ }
338
+ const stagedAbs = resolve(sourceRoot, target);
339
+ if (existsSync(stagedAbs) && statSync(stagedAbs).isFile()) {
340
+ return readFileSync(stagedAbs, "utf8");
341
+ }
342
+ const targetAbsPath = resolve(targetAbs, target);
343
+ return existsSync(targetAbsPath) ? readFileSync(targetAbsPath, "utf8") : undefined;
344
+ };
345
+ const computedPatches = computeModulePatches(patches, {
346
+ versions: options.versions ?? {},
347
+ readTarget: readPatchTarget,
348
+ });
349
+ // Full validation AND patch computation passed — only now does anything
350
+ // get written to targetDir. Copies only (never deletes anything): a
351
+ // fetched module's or template's files ADD to the free scaffold, they
352
+ // never remove what the free copy already wrote. The root manifest and
353
+ // every installMap source are excluded from the whole-tree copy — the
354
+ // manifest never lands in the scaffold, and a mapped source lands ONLY
355
+ // at its mapped destination, never also at its original path.
356
+ const excluded = new Set([
357
+ isTemplate ? AILK_TEMPLATE_JSON : AILK_MODULE_JSON,
358
+ ...Object.keys(installMap),
359
+ ]);
360
+ copyTree(sourceRoot, targetAbs, excluded);
361
+ for (const [source, destination] of Object.entries(installMap)) {
362
+ const destAbs = resolve(targetAbs, destination);
363
+ mkdirSync(join(destAbs, ".."), { recursive: true });
364
+ copyFileSync(resolve(sourceRoot, source), destAbs);
365
+ }
366
+ // patches write AFTER every file the tree brought is in place (D5) —
367
+ // after installMap, before postInstall — so a patch can never race a
368
+ // file its own ownedFiles/installMap just delivered, and (since it was
369
+ // COMPUTED above, before either ran) overwrites rather than conflicts
370
+ // with content copyTree/installMap just placed at the same path.
371
+ writeModulePatches(computedPatches, targetAbs);
372
+ if (manifest?.postInstall) {
373
+ process.stdout.write(`${stripControlCharacters(manifest.postInstall)}\n`);
374
+ }
375
+ }
71
376
  /**
72
377
  * Recursively copy every file from `srcRoot` into `destRoot`, creating parent
73
- * directories as needed. Runs ONLY after `safeExtractTarGz` has validated
74
- * every entry, so no additional path-safety check is needed here — `srcRoot`
75
- * is our own staging tree, not attacker-controlled input.
378
+ * directories as needed, except any path in `excluded` (relative to
379
+ * `srcRoot`, POSIX-normalized) — the bundle manifest and any `installMap`
380
+ * source, which land only at their mapped destination. Runs ONLY after
381
+ * `safeExtractTarGz` has validated every entry, so no additional
382
+ * path-safety check is needed here — `srcRoot` is our own staging tree, not
383
+ * attacker-controlled input.
76
384
  */
77
- function copyTree(srcRoot, destRoot) {
385
+ function copyTree(srcRoot, destRoot, excluded = new Set()) {
78
386
  const stack = [srcRoot];
79
387
  while (stack.length > 0) {
80
388
  const dir = stack.pop();
@@ -89,6 +397,8 @@ function copyTree(srcRoot, destRoot) {
89
397
  stack.push(srcPath);
90
398
  }
91
399
  else if (entry.isFile()) {
400
+ if (excluded.has(rel.split(sep).join("/")))
401
+ continue;
92
402
  mkdirSync(join(destPath, ".."), { recursive: true });
93
403
  copyFileSync(srcPath, destPath);
94
404
  }
@@ -101,14 +411,14 @@ function copyTree(srcRoot, destRoot) {
101
411
  * file doc for why this is a port and not an import). Extracts `tarGzPath`
102
412
  * into `targetDir`, which the caller guarantees is empty.
103
413
  */
104
- async function safeExtractTarGz(tarGzPath, targetDir, moduleName) {
414
+ async function safeExtractTarGz(tarGzPath, targetDir, subject) {
105
415
  const resolvedTarget = resolve(targetDir);
106
416
  let entryCount = 0;
107
417
  let totalSize = 0;
108
418
  const captured = { error: null };
109
419
  const reject = (msg) => {
110
420
  if (captured.error === null) {
111
- captured.error = new Error(`module "${moduleName}": ${msg}`);
421
+ captured.error = new Error(`${subject}: ${msg}`);
112
422
  }
113
423
  return false;
114
424
  };
@@ -171,14 +481,3 @@ async function safeExtractTarGz(tarGzPath, targetDir, moduleName) {
171
481
  throw captured.error;
172
482
  }
173
483
  }
174
- /** True iff `child` is the same path as `parent` or a descendant of it. */
175
- function isPathInside(parent, child) {
176
- const rel = relative(parent, child);
177
- if (rel === "")
178
- return true;
179
- if (rel.startsWith(".."))
180
- return false;
181
- if (isAbsolute(rel))
182
- return false;
183
- return true;
184
- }
@@ -1,47 +1,12 @@
1
- /**
2
- * Authenticated paid-module-bundle fetch (K1, #5089 — Flavor 3).
3
- *
4
- * Posture lifted verbatim from `packages/cli/src/lib/fetch-bundle.ts`
5
- * (`fetchBundle`, the skill-registry twin of this function):
6
- * - Bearer-JWT auth on every request.
7
- * - `redirect: "error"` so a maliciously redirected response can't drop us
8
- * onto an unintended host — the CLI must hit the configured module
9
- * distribution only.
10
- * - Empty-token guard before any network call (defense in depth — the
11
- * caller should already have gated on an empty/absent key).
12
- * - Status-code mapping: 401 → license invalid/revoked/lapsed, 404 →
13
- * module content not yet available, other 4xx/5xx → upstream body.
14
- *
15
- * Deliberately DOES NOT depend on `@working-theory/license` or
16
- * `@working-theory/cli` — both are `publishConfig.access: "restricted"` and
17
- * `create-ailk` is the one package in this monorepo published to PUBLIC npm
18
- * (Apache-2.0, A1 disposed it `oss`). A runtime dependency on a restricted
19
- * package would break `pnpm install` for every free-tier consumer, not just
20
- * gate the paid modules — so this file re-states fetchBundle's POSTURE
21
- * rather than importing it. Named in the K1 PR body as the public-package
22
- * boundary this card had to draw.
23
- *
24
- * Also deliberately skips fetchBundle's caller-side `checkLicense()` local
25
- * JWT pre-check (install-skill.ts's step 2): that local verification lives
26
- * in `@working-theory/license`, which for the same publish-boundary reason
27
- * above cannot be a runtime dependency here. Instead the module distribution
28
- * endpoint's own 401 response IS the license verdict — exactly the signal
29
- * `fetchBundle` already surfaces via `isLicenseError`. This trades one local,
30
- * no-network check for one authoritative, server-verified one; the caller-
31
- * visible contract (present-but-invalid key ⟹ the honest "does not cover
32
- * this" message) is unchanged.
33
- *
34
- * Returns the response body bytes on 200; throws a descriptive
35
- * `FetchModuleError` on any failure path so the caller can map to an honest
36
- * exit uniformly.
37
- */
38
1
  export declare const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/api/modules";
39
2
  export interface FetchModuleOptions {
40
3
  moduleName: string;
41
4
  /** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
42
5
  token: string;
43
- /** Registry root; the module name is appended as a single path segment. */
6
+ /** Registry root; `<name>/<version>` is appended as two path segments. */
44
7
  registryUrl?: string;
8
+ /** The module version to request. Defaults to this package's own version. */
9
+ version?: string;
45
10
  /** Override for tests; defaults to `globalThis.fetch`. Never a live call in CI. */
46
11
  fetchImpl?: typeof fetch;
47
12
  }
@@ -35,7 +35,20 @@
35
35
  * `FetchModuleError` on any failure path so the caller can map to an honest
36
36
  * exit uniformly.
37
37
  */
38
+ import pkg from "../../package.json" with { type: "json" };
38
39
  export const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/api/modules";
40
+ /**
41
+ * This package's own `package.json` `"version"`. Deliberately a static JSON
42
+ * import (`resolveJsonModule`), not `import.meta.url` + `fileURLToPath` (the
43
+ * technique `cli.ts:56-58`'s `readPackageVersion` uses) — `fetch-module.ts`
44
+ * is loaded directly by `fetch-module.test.ts`, and `import.meta` is a
45
+ * SyntaxError under ts-jest's CommonJS transform (the same reason
46
+ * `module-architecture.ts`'s `paidModuleTemplateExclusions` doc gives for
47
+ * avoiding it in any jest-reachable file). A bundle overlays the scaffold
48
+ * `create-ailk` just copied from its own `templates/`, so the default
49
+ * version request must be the running package's own.
50
+ */
51
+ const CREATE_AILK_VERSION = pkg.version;
39
52
  export class FetchModuleError extends Error {
40
53
  isLicenseError;
41
54
  constructor(message,
@@ -47,25 +60,26 @@ export class FetchModuleError extends Error {
47
60
  }
48
61
  }
49
62
  /**
50
- * Build the fetch URL by joining `registryUrl` with `moduleName` as a single
51
- * trailing path segment. `encodeURIComponent` on the module name is defense
52
- * in depth — `parse-args.ts`'s `parseModules` already restricts the value to
53
- * `RECOGNIZED_MODULES`, but a hostile value can never smuggle a path
54
- * separator or query string here regardless.
63
+ * Build the fetch URL by joining `registryUrl` with `moduleName` and
64
+ * `version` as two trailing path segments. `encodeURIComponent` on each is
65
+ * defense in depth — `parse-args.ts`'s `parseModules` already restricts
66
+ * `moduleName` to `RECOGNIZED_MODULES`, but a hostile value can never smuggle
67
+ * a path separator or query string here regardless.
55
68
  */
56
- function buildUrl(registryUrl, moduleName) {
69
+ function buildUrl(registryUrl, moduleName, version) {
57
70
  const trimmed = registryUrl.replace(/\/+$/, "");
58
- return `${trimmed}/${encodeURIComponent(moduleName)}`;
71
+ return `${trimmed}/${encodeURIComponent(moduleName)}/${encodeURIComponent(version)}`;
59
72
  }
60
73
  export async function fetchModule(options) {
61
74
  const { moduleName, token } = options;
62
75
  if (token.length === 0) {
63
76
  throw new FetchModuleError("internal error: fetchModule called without a token");
64
77
  }
78
+ const version = options.version ?? CREATE_AILK_VERSION;
65
79
  const registryUrl = options.registryUrl ??
66
80
  process.env["AILK_MODULE_REGISTRY_URL"] ??
67
81
  DEFAULT_AILK_MODULE_REGISTRY_URL;
68
- const url = buildUrl(registryUrl, moduleName);
82
+ const url = buildUrl(registryUrl, moduleName, version);
69
83
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
70
84
  let response;
71
85
  try {
@@ -86,6 +100,10 @@ export async function fetchModule(options) {
86
100
  throw new FetchModuleError(`your licence does not cover the "${moduleName}" module (invalid, revoked, or lapsed)`,
87
101
  /* isLicenseError */ true);
88
102
  }
103
+ if (response.status === 403) {
104
+ throw new FetchModuleError(`your licence does not include the "${moduleName}" module, or its term ended before create-ailk ${version} was released — use a create-ailk version released during your term`,
105
+ /* isLicenseError */ true);
106
+ }
89
107
  if (response.status === 404) {
90
108
  throw new FetchModuleError(`"${moduleName}" module content not yet available — ping us at hello@ailaunchkit.ai`);
91
109
  }
@@ -0,0 +1,20 @@
1
+ export declare const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://ailaunchkit.ai/api/templates";
2
+ export interface FetchTemplateOptions {
3
+ templateName: string;
4
+ /** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
5
+ token: string;
6
+ /** Registry root; `<name>/<version>` is appended as two path segments. */
7
+ registryUrl?: string;
8
+ /** The template version to request. Defaults to this package's own version. */
9
+ version?: string;
10
+ /** Override for tests; defaults to `globalThis.fetch`. Never a live call in CI. */
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+ export declare class FetchTemplateError extends Error {
14
+ /** True iff the failure was a license-gating signal (HTTP 401 or 403). */
15
+ readonly isLicenseError: boolean;
16
+ constructor(message: string,
17
+ /** True iff the failure was a license-gating signal (HTTP 401 or 403). */
18
+ isLicenseError?: boolean);
19
+ }
20
+ export declare function fetchTemplate(options: FetchTemplateOptions): Promise<Uint8Array>;