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
@@ -28,23 +28,180 @@
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.
32
65
  */
33
- import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
66
+ import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
34
67
  import { tmpdir } from "node:os";
35
68
  import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
36
69
  import { pipeline } from "node:stream/promises";
37
70
  import { createGunzip } from "node:zlib";
38
71
  import * as tar from "tar";
72
+ import { computeModulePatches, writeModulePatches } from "./apply-module-patches.js";
39
73
  /** Soft limits to prevent decompression-bomb / zip-bomb attacks. Same values as extractBundle. */
40
74
  const MAX_ENTRIES = 1000;
41
75
  const MAX_TOTAL_SIZE_BYTES = 50 * 1024 * 1024; // 50 MiB
76
+ /** The bundle-root manifest file (spec § D7). Never copied into the scaffold. */
77
+ const AILK_MODULE_JSON = "ailk-module.json";
78
+ /** The op names valid for each patch `kind` (spec 5418 § 3 D1's ModulePatchOp union, partitioned). */
79
+ const VALID_OPS_BY_KIND = {
80
+ json: new Set(["merge-keys", "append-unique"]),
81
+ "next-config": new Set(["add-transpile-package", "wrap-export-with"]),
82
+ "jest-config": new Set([
83
+ "add-module-name-mapper",
84
+ "add-transform-ignore-pattern",
85
+ "add-transform",
86
+ ]),
87
+ };
88
+ /** True iff `child` is the same path as `parent` or a descendant of it. */
89
+ function isPathInside(parent, child) {
90
+ const rel = relative(parent, child);
91
+ if (rel === "")
92
+ return true;
93
+ if (rel.startsWith(".."))
94
+ return false;
95
+ if (isAbsolute(rel))
96
+ return false;
97
+ return true;
98
+ }
99
+ /**
100
+ * Reject an absolute path or any `..` segment — the same shape check
101
+ * `safeExtractTarGz` applies to tar entries, reused here for `installMap`
102
+ * keys/values, which come from bundle-controlled JSON rather than tar
103
+ * entries and so need the same fail-closed treatment.
104
+ */
105
+ function hasUnsafeSegment(p) {
106
+ if (isAbsolute(p))
107
+ return true;
108
+ const normalized = normalize(p);
109
+ return (normalized === ".." ||
110
+ normalized.startsWith(`..${sep}`) ||
111
+ normalized.split(sep).includes(".."));
112
+ }
113
+ /**
114
+ * Read `ailk-module.json` from the validated staging tree, if present.
115
+ * Malformed JSON is treated as absent — the manifest is written by the
116
+ * producer (D7), never hand-authored, so a parse failure is a producer bug,
117
+ * not a security boundary to fail closed on with a rejected install.
118
+ */
119
+ function readAilkModuleManifest(extractedRoot) {
120
+ const manifestPath = join(extractedRoot, AILK_MODULE_JSON);
121
+ if (!existsSync(manifestPath))
122
+ return null;
123
+ try {
124
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ /**
131
+ * Validate every `installMap` entry against the staged tree and the target
132
+ * scaffold, throwing on the first violation — called BEFORE any copy, so a
133
+ * bad entry rejects the whole bundle with nothing written to `targetDir`.
134
+ * Returns the map unchanged (validation only) for the caller to apply.
135
+ */
136
+ function validateInstallMap(installMap, extractedRoot, targetAbs, moduleName) {
137
+ for (const [source, destination] of Object.entries(installMap)) {
138
+ if (hasUnsafeSegment(source)) {
139
+ throw new Error(`module "${moduleName}": unsafe installMap source "${source}"`);
140
+ }
141
+ const sourceAbs = resolve(extractedRoot, source);
142
+ if (!isPathInside(extractedRoot, sourceAbs)) {
143
+ throw new Error(`module "${moduleName}": installMap source "${source}" resolves outside staging`);
144
+ }
145
+ let sourceStat;
146
+ try {
147
+ sourceStat = statSync(sourceAbs);
148
+ }
149
+ catch {
150
+ throw new Error(`module "${moduleName}": installMap source "${source}" is not a file the bundle produced`);
151
+ }
152
+ if (!sourceStat.isFile()) {
153
+ throw new Error(`module "${moduleName}": installMap source "${source}" is not a file the bundle produced`);
154
+ }
155
+ if (hasUnsafeSegment(destination)) {
156
+ throw new Error(`module "${moduleName}": unsafe installMap destination "${destination}"`);
157
+ }
158
+ const destAbs = resolve(targetAbs, destination);
159
+ if (!isPathInside(targetAbs, destAbs)) {
160
+ throw new Error(`module "${moduleName}": installMap destination "${destination}" resolves outside target`);
161
+ }
162
+ }
163
+ }
164
+ /**
165
+ * Validate every `patches` entry against `targetDir` — the same fail-closed
166
+ * point and containment guard `validateInstallMap` above uses, extended
167
+ * (spec 5418 § 3 D6): `target` must resolve inside `targetDir`; `kind` and
168
+ * every `op.op` must be a recognized vocabulary member. Any violation
169
+ * refuses the WHOLE bundle before `copyTree` runs — this is D4 class 4
170
+ * (unknown kind/op), resolved here at validation time, never inside
171
+ * `applyModulePatches` (which never sees an invalid kind/op).
172
+ */
173
+ function validatePatches(patches, targetAbs, moduleName) {
174
+ for (const patch of patches) {
175
+ if (hasUnsafeSegment(patch.target)) {
176
+ throw new Error(`module "${moduleName}": unsafe patch target "${patch.target}"`);
177
+ }
178
+ const targetAbsPath = resolve(targetAbs, patch.target);
179
+ if (!isPathInside(targetAbs, targetAbsPath)) {
180
+ throw new Error(`module "${moduleName}": patch target "${patch.target}" resolves outside target`);
181
+ }
182
+ const validOps = VALID_OPS_BY_KIND[patch.kind];
183
+ if (!validOps) {
184
+ throw new Error(`invalid module manifest: unknown patch kind "${patch.kind}" for target ${patch.target}`);
185
+ }
186
+ for (const op of patch.ops) {
187
+ if (!validOps.has(op.op)) {
188
+ throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${patch.target}`);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ /** C0 (0x00-0x1F) and C1 (0x7F-0x9F) control characters, newline excepted. */
194
+ function stripControlCharacters(text) {
195
+ // eslint-disable-next-line no-control-regex -- deliberately stripping control chars
196
+ return text.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, "");
197
+ }
42
198
  /**
43
199
  * Extract a fetched module bundle into `targetDir`, merging its files onto
44
200
  * 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).
201
+ * decompress error, limit overrun, invalid `installMap`, or `patches`
202
+ * failure (D4) `targetDir` is left untouched on failure in every one of
203
+ * those cases: validation AND patch computation both complete before any
204
+ * file is copied or written into place.
48
205
  */
49
206
  export async function extractModuleBundle(options) {
50
207
  const { bytes, targetDir, moduleName } = options;
@@ -59,10 +216,71 @@ export async function extractModuleBundle(options) {
59
216
  const extractedRoot = join(stagingDir, "extracted");
60
217
  mkdirSync(extractedRoot, { recursive: true });
61
218
  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);
219
+ // installMap validation runs BEFORE any copy, so a bad entry rejects the
220
+ // whole bundle with nothing written to targetDir (same posture as the
221
+ // tar-entry validation above).
222
+ const manifest = readAilkModuleManifest(extractedRoot);
223
+ const installMap = manifest?.installMap ?? {};
224
+ validateInstallMap(installMap, extractedRoot, targetAbs, moduleName);
225
+ // patches validation (spec 5418 § 3 D6) runs at the same point, before
226
+ // any copy — a bad target/kind/op rejects the whole bundle with nothing
227
+ // written to targetDir, same posture as installMap and the tar-entry
228
+ // validation above.
229
+ const patches = manifest?.patches ?? [];
230
+ validatePatches(patches, targetAbs, moduleName);
231
+ // Compute every patch target's new content BEFORE any write to
232
+ // targetDir — including before copyTree/installMap below, not only
233
+ // before the patch step's own write (spec 5418 § 3 D4/§ 6.2): a
234
+ // conflict, missing-target, or unparseable-target failure must leave
235
+ // the scaffold exactly as it was, which includes the bundle's own
236
+ // plain content and installMap destinations never landing either. A
237
+ // patch target the bundle itself delivers (as plain content or as an
238
+ // installMap destination) is read from the STAGED tree here, since
239
+ // targetDir would not have it yet at this point.
240
+ const installMapDestinationToSource = new Map(Object.entries(installMap).map(([source, destination]) => [destination, source]));
241
+ const readPatchTarget = (target) => {
242
+ const mappedSource = installMapDestinationToSource.get(target);
243
+ if (mappedSource !== undefined) {
244
+ const sourceAbs = resolve(extractedRoot, mappedSource);
245
+ return existsSync(sourceAbs) ? readFileSync(sourceAbs, "utf8") : undefined;
246
+ }
247
+ const stagedAbs = resolve(extractedRoot, target);
248
+ if (existsSync(stagedAbs) && statSync(stagedAbs).isFile()) {
249
+ return readFileSync(stagedAbs, "utf8");
250
+ }
251
+ const targetAbsPath = resolve(targetAbs, target);
252
+ return existsSync(targetAbsPath) ? readFileSync(targetAbsPath, "utf8") : undefined;
253
+ };
254
+ const computedPatches = computeModulePatches(patches, {
255
+ versions: options.versions ?? {},
256
+ readTarget: readPatchTarget,
257
+ });
258
+ // Full validation AND patch computation passed — only now does anything
259
+ // get written to targetDir. Copies only (never deletes anything): a
260
+ // fetched module's files ADD to the free scaffold, they never remove
261
+ // what the free copy already wrote. ailk-module.json and every
262
+ // installMap source are excluded from the whole-tree copy — the
263
+ // manifest never lands in the scaffold, and a mapped source lands ONLY
264
+ // at its mapped destination, never also at its original path.
265
+ const excluded = new Set([
266
+ AILK_MODULE_JSON,
267
+ ...Object.keys(installMap),
268
+ ]);
269
+ copyTree(extractedRoot, targetAbs, excluded);
270
+ for (const [source, destination] of Object.entries(installMap)) {
271
+ const destAbs = resolve(targetAbs, destination);
272
+ mkdirSync(join(destAbs, ".."), { recursive: true });
273
+ copyFileSync(resolve(extractedRoot, source), destAbs);
274
+ }
275
+ // patches write AFTER every file the bundle brought is in place (D5) —
276
+ // after installMap, before postInstall — so a patch can never race a
277
+ // file its own ownedFiles/installMap just delivered, and (since it was
278
+ // COMPUTED above, before either ran) overwrites rather than conflicts
279
+ // with content copyTree/installMap just placed at the same path.
280
+ writeModulePatches(computedPatches, targetAbs);
281
+ if (manifest?.postInstall) {
282
+ process.stdout.write(`${stripControlCharacters(manifest.postInstall)}\n`);
283
+ }
66
284
  }
67
285
  finally {
68
286
  rmSync(stagingDir, { recursive: true, force: true });
@@ -70,11 +288,14 @@ export async function extractModuleBundle(options) {
70
288
  }
71
289
  /**
72
290
  * 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.
291
+ * directories as needed, except any path in `excluded` (relative to
292
+ * `srcRoot`, POSIX-normalized) the bundle manifest and any `installMap`
293
+ * source, which land only at their mapped destination. Runs ONLY after
294
+ * `safeExtractTarGz` has validated every entry, so no additional
295
+ * path-safety check is needed here — `srcRoot` is our own staging tree, not
296
+ * attacker-controlled input.
76
297
  */
77
- function copyTree(srcRoot, destRoot) {
298
+ function copyTree(srcRoot, destRoot, excluded = new Set()) {
78
299
  const stack = [srcRoot];
79
300
  while (stack.length > 0) {
80
301
  const dir = stack.pop();
@@ -89,6 +310,8 @@ function copyTree(srcRoot, destRoot) {
89
310
  stack.push(srcPath);
90
311
  }
91
312
  else if (entry.isFile()) {
313
+ if (excluded.has(rel.split(sep).join("/")))
314
+ continue;
92
315
  mkdirSync(join(destPath, ".."), { recursive: true });
93
316
  copyFileSync(srcPath, destPath);
94
317
  }
@@ -171,14 +394,3 @@ async function safeExtractTarGz(tarGzPath, targetDir, moduleName) {
171
394
  throw captured.error;
172
395
  }
173
396
  }
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
  }
@@ -35,6 +35,8 @@ export interface ResolvePaidModulesOptions {
35
35
  licenseKey?: string | undefined;
36
36
  /** Forwarded to `fetchModule`; defaults to `AILK_MODULE_REGISTRY_URL` / the built-in default. */
37
37
  registryUrl?: string;
38
+ /** Forwarded to `fetchModule`; defaults to this package's own version. */
39
+ version?: string;
38
40
  /** Forwarded to `fetchModule`; defaults to `globalThis.fetch`. Never a live call in tests. */
39
41
  fetchImpl?: typeof fetch;
40
42
  }
@@ -39,6 +39,7 @@ export async function resolvePaidModules(options) {
39
39
  ...(options.registryUrl !== undefined
40
40
  ? { registryUrl: options.registryUrl }
41
41
  : {}),
42
+ ...(options.version !== undefined ? { version: options.version } : {}),
42
43
  ...(options.fetchImpl !== undefined
43
44
  ? { fetchImpl: options.fetchImpl }
44
45
  : {}),
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The derived set of scaffold-relative paths a keyless scaffold must not
3
+ * contain — sorted, deduped.
4
+ *
5
+ * Throws if the result is empty: `PAID_MODULES` is never empty (it is
6
+ * `RECOGNIZED_MODULES` minus `FREE_MODULES`) and every paid module registered
7
+ * today owns at least one file under `apps/` or a whole app, so an empty
8
+ * result means the derivation itself broke — not that there is nothing to
9
+ * check (DEF-007: a silent `[]` must fail loudly, not pass the gate).
10
+ */
11
+ export declare function derivePaidScaffoldPaths(): string[];
12
+ /**
13
+ * Every derived paid path that exists under `scaffoldDir` — the leaked set,
14
+ * sorted. Empty means the scaffold is clean.
15
+ */
16
+ export declare function findLeakedPaidPaths(scaffoldDir: string): string[];
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Derived paid-path list (R2-5374 — the no-paid-code CI gate, D-44).
3
+ *
4
+ * The set of scaffold-relative paths a keyless (Free) `create-ailk` scaffold
5
+ * must never contain, computed from `MODULE_SLICES` / `PAID_MODULES` at call
6
+ * time — never hand-maintained, so it grows automatically once commerce
7
+ * (D-47) and signal-collectors (D-48) gain their own `MODULE_SLICES` entries.
8
+ *
9
+ * Only two of `ModuleSlice`'s shapes name a path that can actually appear in a
10
+ * scaffold's own output tree (`build-templates.mjs`'s `SNAPSHOT_DIRS` is
11
+ * `["apps", "database"]` — see its own header comment; `packages/*` is never
12
+ * copied into a scaffold, it is consumed as an npm dependency):
13
+ *
14
+ * - `ownedFiles` entries that already sit under `apps/` — the scaffold
15
+ * copies that tree verbatim, so the path is identical on both sides.
16
+ * - `apps[name].path` — a paid module's OWN app directory. Multi-tenant's
17
+ * `workspace` app has been extracted out of this repo's own `apps/` tree
18
+ * into a sibling paid-apps root (its `ownedFiles` entry is a
19
+ * whole-directory glob naming that monorepo-internal location, not a
20
+ * scaffold one), so the app it introduces into a SCAFFOLD is named here
21
+ * instead, via the slice's own declared path. `api` / `mcp` / `web` are
22
+ * excluded: they are base apps present in
23
+ * EVERY scaffold regardless of licence — a module only ever grants
24
+ * import permissions into them, never introduces the directory, and the
25
+ * paid FILES inside them are already covered by the `ownedFiles` branch
26
+ * above.
27
+ *
28
+ * `ownedFiles` entries under `packages/` are excluded for the reason above.
29
+ * `namespaces` (database/*) are excluded too: each slice's own doc comment
30
+ * (see multi-tenant's in `module-architecture.ts`) records that a namespace
31
+ * directory mixes base and paid Prisma models in one `schema.prisma` file, so
32
+ * the DIRECTORY itself is not paid-owned even though some models inside it
33
+ * are — asserting it absent would be wrong on every scaffold, paid or free.
34
+ */
35
+ import { existsSync } from "node:fs";
36
+ import { join } from "node:path";
37
+ import { MODULE_SLICES } from "../module-architecture.js";
38
+ import { PAID_MODULES } from "./module-tier.js";
39
+ /** Base app paths present in every scaffold regardless of the selected modules. */
40
+ const BASE_APP_PATHS = new Set([
41
+ "apps/web",
42
+ "apps/api",
43
+ "apps/mcp",
44
+ ]);
45
+ /** A slice's `ownedFiles` entries that live under `apps/` — copied verbatim into a scaffold. */
46
+ function ownedFilesUnderApps(slice) {
47
+ return (slice.ownedFiles ?? [])
48
+ .filter((entry) => entry.startsWith("apps/"))
49
+ .map((entry) => entry.endsWith("/**") ? entry.slice(0, -"/**".length) : entry);
50
+ }
51
+ /** A slice's own app directories, excluding the base apps every scaffold ships. */
52
+ function introducedAppPaths(slice) {
53
+ return Object.values(slice.apps ?? {})
54
+ .map((app) => app.path)
55
+ .filter((path) => !BASE_APP_PATHS.has(path));
56
+ }
57
+ /**
58
+ * The derived set of scaffold-relative paths a keyless scaffold must not
59
+ * contain — sorted, deduped.
60
+ *
61
+ * Throws if the result is empty: `PAID_MODULES` is never empty (it is
62
+ * `RECOGNIZED_MODULES` minus `FREE_MODULES`) and every paid module registered
63
+ * today owns at least one file under `apps/` or a whole app, so an empty
64
+ * result means the derivation itself broke — not that there is nothing to
65
+ * check (DEF-007: a silent `[]` must fail loudly, not pass the gate).
66
+ */
67
+ export function derivePaidScaffoldPaths() {
68
+ const paths = new Set();
69
+ for (const moduleName of PAID_MODULES) {
70
+ const slice = MODULE_SLICES[moduleName];
71
+ for (const p of ownedFilesUnderApps(slice))
72
+ paths.add(p);
73
+ for (const p of introducedAppPaths(slice))
74
+ paths.add(p);
75
+ }
76
+ const sorted = [...paths].sort();
77
+ if (sorted.length === 0) {
78
+ throw new Error("derivePaidScaffoldPaths() computed an empty list — MODULE_SLICES and " +
79
+ "PAID_MODULES have drifted out of sync (DEF-007: this must never " +
80
+ "silently pass the no-paid-code gate)");
81
+ }
82
+ return sorted;
83
+ }
84
+ /**
85
+ * Every derived paid path that exists under `scaffoldDir` — the leaked set,
86
+ * sorted. Empty means the scaffold is clean.
87
+ */
88
+ export function findLeakedPaidPaths(scaffoldDir) {
89
+ return derivePaidScaffoldPaths().filter((relPath) => existsSync(join(scaffoldDir, relPath)));
90
+ }