create-ailk 0.2.0 → 0.3.0

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