create-ailk 0.5.0 → 0.6.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 (299) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/lib/api-boot-assertion.d.ts +46 -0
  5. package/dist/lib/api-boot-assertion.js +70 -0
  6. package/dist/lib/import-graph.d.ts +29 -0
  7. package/dist/lib/import-graph.js +140 -0
  8. package/dist/module-architecture.d.ts +1 -0
  9. package/dist/module-architecture.js +253 -127
  10. package/dist/surfaces.js +105 -7
  11. package/dist/template-manifest.d.ts +33 -0
  12. package/dist/template-manifest.js +99 -16
  13. package/dist/template-names.d.ts +1 -1
  14. package/dist/template-names.js +1 -0
  15. package/package.json +3 -2
  16. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  17. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  18. package/site-templates/marketing-site/ailk-template.json +1 -1
  19. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  20. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  21. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  22. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  23. package/site-templates/minimalism-site/ailk-template.json +2 -1
  24. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  25. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  26. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  27. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  28. package/site-templates/simple-service/ailk-template.json +10 -0
  29. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  30. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  31. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  32. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  33. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  34. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  35. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  36. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  37. package/site-templates/simple-service/provenance.json +4 -0
  38. package/templates/.env.example +72 -7
  39. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  40. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  41. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  42. package/templates/apps/api/.env.example +13 -0
  43. package/templates/apps/api/CLAUDE.md +26 -2
  44. package/templates/apps/api/jest.config.cjs +3 -0
  45. package/templates/apps/api/package.json +1 -0
  46. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  47. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  48. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  49. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  50. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  51. package/templates/apps/api/src/config/index.ts +62 -0
  52. package/templates/apps/api/src/config/modules.ts +7 -3
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  54. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  55. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  56. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  57. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  58. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  59. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  60. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  61. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  62. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  63. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  64. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  65. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  66. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  67. package/templates/apps/api/src/lib/site.ts +208 -9
  68. package/templates/apps/api/src/lib/stripe.ts +66 -0
  69. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  70. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  71. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  72. package/templates/apps/api/src/openapi/spec.ts +543 -2
  73. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  74. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  75. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  76. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  77. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  78. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  79. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  80. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  81. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  82. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  83. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  84. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  85. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  87. package/templates/apps/api/src/routes/content/index.ts +13 -73
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  89. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  90. package/templates/apps/api/src/routes/content/search.ts +129 -0
  91. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  92. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  93. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  94. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  95. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  96. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  97. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  98. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  99. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  100. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  101. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  102. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  103. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  104. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  105. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  106. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  111. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  112. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  113. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  114. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  115. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  116. package/templates/apps/api/src/server.ts +439 -49
  117. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  118. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  119. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  120. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  121. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  122. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  123. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  124. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  125. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  126. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  127. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  128. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  129. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  130. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  131. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  132. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  133. package/templates/apps/api/src/services/audit-log.ts +668 -0
  134. package/templates/apps/api/src/services/consent.ts +18 -0
  135. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  136. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  137. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  138. package/templates/apps/api/src/services/entitlements.ts +80 -0
  139. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  140. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  141. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  142. package/templates/apps/api/src/services/listing-config.ts +27 -9
  143. package/templates/apps/api/src/services/project-listings.ts +201 -6
  144. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  145. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  146. package/templates/apps/api/src/services/revalidation.ts +173 -0
  147. package/templates/apps/api/src/services/verified-email.ts +71 -0
  148. package/templates/apps/api/src/vercel-handler.ts +75 -35
  149. package/templates/apps/mcp/.env.example +17 -0
  150. package/templates/apps/mcp/CLAUDE.md +57 -24
  151. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  152. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  153. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  154. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  155. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  156. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  157. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  158. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  159. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  160. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  161. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  162. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  163. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  164. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  165. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  166. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  167. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  168. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  169. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  170. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  171. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  172. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  173. package/templates/apps/mcp/jest.config.cjs +13 -0
  174. package/templates/apps/mcp/package.json +2 -0
  175. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  176. package/templates/apps/mcp/src/config/modules.ts +10 -5
  177. package/templates/apps/mcp/src/http-transport.ts +102 -29
  178. package/templates/apps/mcp/src/index.ts +73 -11
  179. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  180. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  181. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  182. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  183. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  184. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  185. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  186. package/templates/apps/mcp/src/server.ts +59 -35
  187. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  188. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  189. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  190. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  191. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  192. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  193. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  194. package/templates/apps/mcp/src/tools/index.ts +6 -4
  195. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  196. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  197. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  198. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  199. package/templates/apps/web/.env.example +71 -5
  200. package/templates/apps/web/CLAUDE.md +44 -0
  201. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  202. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  203. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  204. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  205. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  206. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  207. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  208. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  209. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  210. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  211. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  212. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  213. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  214. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  215. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  216. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  217. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  218. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  219. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  220. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  221. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  222. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  223. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  224. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  225. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  226. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  227. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  228. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  229. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  230. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  232. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  233. package/templates/apps/web/auth.ts +407 -8
  234. package/templates/apps/web/jest.config.cjs +8 -4
  235. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  236. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  237. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  238. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  239. package/templates/apps/web/lib/auth-email.ts +318 -0
  240. package/templates/apps/web/lib/auth-env.ts +85 -0
  241. package/templates/apps/web/lib/auth-routes.ts +55 -0
  242. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  243. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  244. package/templates/apps/web/messages/en.json +187 -0
  245. package/templates/apps/web/package.json +2 -1
  246. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  247. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  248. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  249. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  250. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  251. package/templates/database/CHANGELOG.md +142 -0
  252. package/templates/database/README.md +10 -3
  253. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  254. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  255. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  256. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  257. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  258. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  259. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  260. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  261. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  262. package/templates/database/__tests__/tenancy.test.ts +141 -0
  263. package/templates/database/config/schema.prisma +79 -2
  264. package/templates/database/content/schema.prisma +87 -11
  265. package/templates/database/inbox/schema.prisma +452 -9
  266. package/templates/database/jest.config.cjs +5 -3
  267. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  268. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  269. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  270. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  271. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  272. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  273. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  274. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  275. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  276. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  277. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  278. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  279. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  280. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  281. package/templates/database/package.json +1 -1
  282. package/templates/database/tenancy.ts +86 -1
  283. package/templates/database/tsconfig.json +1 -0
  284. package/versions.json +34 -34
  285. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  286. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  287. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  288. package/templates/apps/api/src/routes/content/create.ts +0 -115
  289. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  290. package/templates/apps/api/src/routes/content/update.ts +0 -112
  291. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  292. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  293. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  294. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  295. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  296. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  297. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  298. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  299. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Abuse rule orchestrator for MCP write tools.
3
3
  *
4
- * Runs all in-process heuristics for the three anonymous write tools:
5
- * capture_lead, subscribe_newsletter, request_callback.
4
+ * Runs all in-process heuristics for the anonymous write tools:
5
+ * capture_lead, subscribe_newsletter, request_callback, and — since #5971 —
6
+ * start_purchase.
6
7
  *
7
8
  * Rules applied (in order):
8
9
  * 1. User-Agent deny list (applies to all write tools)
9
10
  * 2. Honeypot field check (applies to all write tools)
10
11
  * 3. Email format validation (capture_lead, subscribe_newsletter)
11
- * 4. Per-IP daily cap (capture_lead only)
12
+ * 4. Per-IP daily cap (capture_lead and start_purchase, each on its OWN
13
+ * tracker — sharing one would let purchase attempts consume the lead
14
+ * budget and vice versa, a silent cross-tool denial of service)
12
15
  *
13
16
  * The paid layer (lead-routing.ts) extends with real abuse-protection
14
17
  * services. OSS gets basic heuristics that stop obvious bots.
@@ -24,28 +27,142 @@ import { buildDenyList, isUserAgentBlocked } from "./user-agent-deny.js";
24
27
  /** Tools that require email format validation on their input. */
25
28
  const EMAIL_VALIDATED_TOOLS = new Set(["capture_lead", "subscribe_newsletter"]);
26
29
 
27
- /** Tools that apply the per-IP daily cap. */
28
- const DAILY_CAPPED_TOOLS = new Set(["capture_lead"]);
30
+ /** The per-tool daily-cap budgets, keyed by MCP tool name. */
31
+ type DailyCapKey = "captureLead" | "startPurchase";
32
+
33
+ /**
34
+ * The typed reason codes an {@link AbuseResult} refusal can carry.
35
+ *
36
+ * The code — not the prose in `reason` — is the contract between this module
37
+ * and its callers. `http-transport.ts` maps each code to an HTTP status
38
+ * (`ABUSE_REFUSAL_STATUS`), so a reworded refusal reason can never move a
39
+ * refusal between statuses, and a NEW code fails to compile until the
40
+ * transport declares a status for it (#5994).
41
+ */
42
+ export const AbuseReasonCode = {
43
+ /** The caller's User-Agent is on the deny list. */
44
+ BLOCKED_USER_AGENT: "blocked_user_agent",
45
+ /** The submitted input filled a honeypot field. */
46
+ HONEYPOT_FILLED: "honeypot_filled",
47
+ /** The submitted input carried a malformed email address. */
48
+ INVALID_EMAIL: "invalid_email",
49
+ /** The caller's IP has spent this tool's per-IP daily budget. */
50
+ DAILY_CAP_REACHED: "daily_cap_reached",
51
+ } as const;
52
+
53
+ export type AbuseReasonCode =
54
+ (typeof AbuseReasonCode)[keyof typeof AbuseReasonCode];
55
+
56
+ /**
57
+ * Tools that apply a per-IP daily cap, each mapped to the tracker that budgets
58
+ * it and the refusal reason it answers with. One tracker per entry — never
59
+ * shared across tools, since a shared budget would let purchase attempts
60
+ * consume the lead allowance and vice versa.
61
+ *
62
+ * The reason strings here are prose for the caller and nothing more: every
63
+ * entry refuses with {@link AbuseReasonCode.DAILY_CAP_REACHED}, and that code
64
+ * is what decides the HTTP status. Reword either one freely.
65
+ */
66
+ const DAILY_CAPPED_TOOLS = new Map<
67
+ string,
68
+ { tracker: DailyCapKey; reason: string }
69
+ >([
70
+ [
71
+ "capture_lead",
72
+ {
73
+ tracker: "captureLead",
74
+ reason: "daily capture_lead cap reached for this IP",
75
+ },
76
+ ],
77
+ [
78
+ "start_purchase",
79
+ {
80
+ tracker: "startPurchase",
81
+ reason: "daily cap reached for start_purchase from this IP",
82
+ },
83
+ ],
84
+ ]);
29
85
 
30
86
  export interface AbuseConfig {
31
87
  /** Per-IP daily cap for capture_lead. Default 50. */
32
88
  captureLeadDailyCapPerIp: number;
89
+ /**
90
+ * Per-IP daily cap for start_purchase (#5971). Default 20 — lower than
91
+ * capture_lead's, because each call creates a real Stripe Checkout Session.
92
+ * Optional so existing callers that construct an AbuseConfig without it
93
+ * keep compiling; omitted falls back to the default.
94
+ */
95
+ startPurchaseDailyCapPerIp?: number;
33
96
  /** Comma-separated extra UA substrings to block. */
34
97
  blockedUserAgents: string;
35
98
  }
36
99
 
37
- export interface AbuseResult {
38
- allowed: boolean;
39
- reason?: string;
100
+ /** Default per-IP daily cap for start_purchase when none is configured. */
101
+ export const DEFAULT_START_PURCHASE_DAILY_CAP = 20;
102
+
103
+ /** Default per-IP daily cap for capture_lead when none is configured. */
104
+ export const DEFAULT_CAPTURE_LEAD_DAILY_CAP = 50;
105
+
106
+ /**
107
+ * Reads a positive-integer env var, falling back to `fallback` for anything
108
+ * that is not one.
109
+ *
110
+ * `parseInt(process.env[X] ?? "20", 10)` is NOT enough, and the gap is a
111
+ * silent fail-OPEN on a security threshold (#5971 security review round 2):
112
+ * `?? "20"` fires only when the var is UNSET, so a var that is present but
113
+ * BLANK — the shape `.env.example` itself teaches for three neighbouring
114
+ * vars — reaches `parseInt("")` and yields `NaN`. `NaN` then survives every
115
+ * downstream `??` guard (nullish coalescing catches null/undefined, not NaN),
116
+ * and `count >= NaN` is always false, so the cap silently stops refusing
117
+ * anything. A misconfigured deployment looks identical to a correct one at
118
+ * rest, which is exactly why this needs a guard rather than a convention.
119
+ */
120
+ export function readPositiveIntEnv(name: string, fallback: number): number {
121
+ const parsed = Number.parseInt(process.env[name] ?? "", 10);
122
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
123
+ }
124
+
125
+ /** Coerces a configured cap to a usable one, defaulting anything non-finite
126
+ * or non-positive. The in-process twin of {@link readPositiveIntEnv}, for a
127
+ * cap that arrives through {@link AbuseConfig} rather than the environment. */
128
+ function usableCap(value: number | undefined, fallback: number): number {
129
+ return typeof value === "number" && Number.isFinite(value) && value > 0
130
+ ? value
131
+ : fallback;
40
132
  }
41
133
 
134
+ /** A refusal: always carries both the typed code and its caller-facing prose. */
135
+ export interface AbuseRefusal {
136
+ allowed: false;
137
+ /** What kind of refusal this is — the machine-readable half. */
138
+ code: AbuseReasonCode;
139
+ /** Caller-facing prose. Free to reword; nothing keys control flow on it. */
140
+ reason: string;
141
+ }
142
+
143
+ /** The verdict of {@link AbuseGuard.validate}. */
144
+ export type AbuseResult = { allowed: true } | AbuseRefusal;
145
+
42
146
  export class AbuseGuard {
43
147
  private readonly denyList: RegExp[];
44
- private readonly captureLeadCap: DailyCapTracker;
148
+ private readonly caps: Record<DailyCapKey, DailyCapTracker>;
45
149
 
46
150
  constructor(config: AbuseConfig) {
47
151
  this.denyList = buildDenyList(config.blockedUserAgents);
48
- this.captureLeadCap = new DailyCapTracker(config.captureLeadDailyCapPerIp);
152
+ this.caps = {
153
+ captureLead: new DailyCapTracker(
154
+ usableCap(
155
+ config.captureLeadDailyCapPerIp,
156
+ DEFAULT_CAPTURE_LEAD_DAILY_CAP,
157
+ ),
158
+ ),
159
+ startPurchase: new DailyCapTracker(
160
+ usableCap(
161
+ config.startPurchaseDailyCapPerIp,
162
+ DEFAULT_START_PURCHASE_DAILY_CAP,
163
+ ),
164
+ ),
165
+ };
49
166
  }
50
167
 
51
168
  /**
@@ -65,12 +182,20 @@ export class AbuseGuard {
65
182
 
66
183
  // Rule 1: User-Agent deny list
67
184
  if (isUserAgentBlocked(ua, this.denyList)) {
68
- return { allowed: false, reason: "blocked user agent" };
185
+ return {
186
+ allowed: false,
187
+ code: AbuseReasonCode.BLOCKED_USER_AGENT,
188
+ reason: "blocked user agent",
189
+ };
69
190
  }
70
191
 
71
192
  // Rule 2: Honeypot
72
193
  if (isHoneypotTripped(input)) {
73
- return { allowed: false, reason: "honeypot field filled" };
194
+ return {
195
+ allowed: false,
196
+ code: AbuseReasonCode.HONEYPOT_FILLED,
197
+ reason: "honeypot field filled",
198
+ };
74
199
  }
75
200
 
76
201
  // Rule 3: Email format validation
@@ -78,33 +203,43 @@ export class AbuseGuard {
78
203
  const rec = input as Record<string, unknown>;
79
204
  const email = rec["email"];
80
205
  if (typeof email === "string" && !isValidEmail(email)) {
81
- return { allowed: false, reason: "invalid email format" };
82
- }
83
- }
84
-
85
- // Rule 4: Per-IP daily cap
86
- if (DAILY_CAPPED_TOOLS.has(toolName)) {
87
- if (!this.captureLeadCap.isAllowed(ip)) {
88
206
  return {
89
207
  allowed: false,
90
- reason: "daily capture_lead cap reached for this IP",
208
+ code: AbuseReasonCode.INVALID_EMAIL,
209
+ reason: "invalid email format",
91
210
  };
92
211
  }
93
212
  }
94
213
 
214
+ // Rule 4: Per-IP daily cap, on the tool's OWN tracker and with the
215
+ // tool's OWN refusal prose — one shared code, see DAILY_CAPPED_TOOLS.
216
+ const cap = DAILY_CAPPED_TOOLS.get(toolName);
217
+ if (cap !== undefined && !this.caps[cap.tracker].isAllowed(ip)) {
218
+ return {
219
+ allowed: false,
220
+ code: AbuseReasonCode.DAILY_CAP_REACHED,
221
+ reason: cap.reason,
222
+ };
223
+ }
224
+
95
225
  return { allowed: true };
96
226
  }
97
227
  }
98
228
 
99
229
  /**
100
230
  * Convenience function for constructing an AbuseGuard from env vars.
101
- * Reads MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP and MCP_ABUSE_BLOCKED_USER_AGENTS.
231
+ * Reads MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP,
232
+ * MCP_ABUSE_START_PURCHASE_DAILY_CAP and MCP_ABUSE_BLOCKED_USER_AGENTS.
102
233
  */
103
234
  export function createAbuseGuardFromEnv(): AbuseGuard {
104
235
  return new AbuseGuard({
105
- captureLeadDailyCapPerIp: parseInt(
106
- process.env["MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP"] ?? "50",
107
- 10,
236
+ captureLeadDailyCapPerIp: readPositiveIntEnv(
237
+ "MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
238
+ DEFAULT_CAPTURE_LEAD_DAILY_CAP,
239
+ ),
240
+ startPurchaseDailyCapPerIp: readPositiveIntEnv(
241
+ "MCP_ABUSE_START_PURCHASE_DAILY_CAP",
242
+ DEFAULT_START_PURCHASE_DAILY_CAP,
108
243
  ),
109
244
  blockedUserAgents: process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "",
110
245
  });
@@ -14,11 +14,16 @@
14
14
  * Recognized module keys: `docs`, `multi-tenant`, `scheduling`,
15
15
  * `signal-collectors` (the `ModuleName` vocabulary
16
16
  * `packages/create-ailk/src/module-architecture.ts` already models via
17
- * `MODULE_SLICES`) plus `content-write` — the Pro
18
- * content-write surface (`create_page`/`update_page`/`delete_page`).
19
- * `content-write` is intentionally NOT a `MODULE_SLICES` entry: it is not
20
- * (yet) a create-ailk `--modules` selector, so it stays a local flag here
21
- * rather than widening the create-ailk module vocabulary.
17
+ * `MODULE_SLICES`) plus `content-write` — the Pro content-write surface,
18
+ * which owns NO MCP tool since #5853: `create_page`/`update_page`/`delete_page`
19
+ * were retired by deletion along with their untenanted HTTP twins, and their
20
+ * replacement is identity-bound, which the identity-blind MCP surface cannot
21
+ * carry (D7, D15; the authed MCP surface is #3591). The flag is read here only
22
+ * because this reader is shared with `apps/api`, where it still gates the
23
+ * tenant-guarded editor routes. `content-write` is intentionally NOT a
24
+ * `MODULE_SLICES` entry: it is not (yet) a create-ailk `--modules` selector,
25
+ * so it stays a local flag here rather than widening the create-ailk module
26
+ * vocabulary.
22
27
  */
23
28
 
24
29
  function enabledModules(): Set<string> {
@@ -27,14 +27,55 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
27
27
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
28
28
  import Fastify, { type FastifyInstance, type FastifyRequest } from "fastify";
29
29
 
30
- import { AbuseGuard, type AbuseConfig } from "./abuse/index.js";
30
+ import {
31
+ AbuseGuard,
32
+ AbuseReasonCode,
33
+ type AbuseConfig,
34
+ } from "./abuse/index.js";
31
35
  import { createMcpServer } from "./server.js";
32
36
 
33
- /** MCP tool names that are write operations (subject to stricter CORS + rate limits). */
34
- const WRITE_TOOLS = new Set([
37
+ /**
38
+ * HTTP status for each {@link AbuseReasonCode} an AbuseGuard refusal can
39
+ * carry (#5994).
40
+ *
41
+ * A refusal of the CALLER — a denied User-Agent, an IP that has spent its
42
+ * daily budget — is **403**: the input was fine, the caller is not allowed to
43
+ * make the call right now. A refusal of the INPUT — a filled honeypot, a
44
+ * malformed email — is **422**.
45
+ *
46
+ * Keying on the code rather than on the refusal prose is what this table is
47
+ * for: the split used to be `reason.includes("daily cap")`, so a reworded
48
+ * reason silently demoted a rate-limit refusal to 422 — and `capture_lead`'s
49
+ * wording only matched by accident, because the tool's name begins with
50
+ * "cap". Typing the record over the full union also means a new reason code
51
+ * fails to compile here until it declares its status.
52
+ */
53
+ export const ABUSE_REFUSAL_STATUS: Record<AbuseReasonCode, 403 | 422> = {
54
+ [AbuseReasonCode.BLOCKED_USER_AGENT]: 403,
55
+ [AbuseReasonCode.DAILY_CAP_REACHED]: 403,
56
+ [AbuseReasonCode.HONEYPOT_FILLED]: 422,
57
+ [AbuseReasonCode.INVALID_EMAIL]: 422,
58
+ };
59
+
60
+ /**
61
+ * MCP tool names that are write operations (subject to stricter CORS + rate
62
+ * limits, and to the AbuseGuard).
63
+ *
64
+ * `start_purchase` joined in #5971: it has a side effect at Stripe and it is
65
+ * worth money to abuse, so it takes the write branch — the origin allow-list,
66
+ * the write CORS posture, and `abuseGuard.validate()` — rather than the read
67
+ * branch's UA check alone.
68
+ */
69
+ // Any name added here MUST be enforced through the batch-aware path in
70
+ // writeToolCalls() below — a request body can be a JSON-RPC ARRAY, and a
71
+ // per-request (rather than per-message) check lets N batched calls through on
72
+ // one cap unit. See the #5971 security review and the batch regression tests
73
+ // in __tests__/http-transport.test.ts.
74
+ export const WRITE_TOOLS = new Set([
35
75
  "capture_lead",
36
76
  "subscribe_newsletter",
37
77
  "request_callback",
78
+ "start_purchase",
38
79
  ]);
39
80
 
40
81
  export interface HttpTransportCorsConfig {
@@ -71,17 +112,49 @@ export interface HttpTransportConfig {
71
112
  stateless?: boolean;
72
113
  }
73
114
 
115
+ /** One write-tool invocation found in a request body. */
116
+ interface WriteToolCall {
117
+ toolName: string;
118
+ args: unknown;
119
+ }
120
+
74
121
  /**
75
- * Determine whether a tools/call payload targets a write tool.
76
- * Returns false for any other method (tools/list, initialize, etc.).
122
+ * Every write-tool invocation carried by a request body, in order.
123
+ *
124
+ * JSON-RPC 2.0 allows a BATCH: an array of messages in one POST. The MCP SDK
125
+ * honours it — `webStandardStreamableHttp.js` maps an array body to N parsed
126
+ * messages and dispatches every one. So a body must be normalised to a LIST of
127
+ * messages before any of them is classified; inspecting `body.method` on what
128
+ * may be an array silently answers "not a write" for the whole batch, and the
129
+ * request then takes the read branch: no write-origin check, and no
130
+ * `AbuseGuard` cap unit consumed by ANY of the N calls it goes on to execute.
131
+ *
132
+ * That is a real bypass of the per-IP daily cap, and for `start_purchase` each
133
+ * bypassed call creates a genuine Stripe Checkout Session on the seller's
134
+ * account. It predates `start_purchase` — `capture_lead`,
135
+ * `subscribe_newsletter` and `request_callback` have carried it since this
136
+ * transport was written — and normalising here closes it for all four at once
137
+ * (#5971 security review, High).
138
+ *
139
+ * Returns one entry PER matching message, so the caller can charge the guard
140
+ * once per call rather than once per request.
77
141
  */
78
- function isWriteToolCall(body: unknown): boolean {
79
- if (!body || typeof body !== "object") return false;
80
- const req = body as Record<string, unknown>;
81
- if (req["method"] !== "tools/call") return false;
82
- const params = req["params"] as Record<string, unknown> | undefined;
83
- if (!params) return false;
84
- return WRITE_TOOLS.has(params["name"] as string);
142
+ function writeToolCalls(body: unknown): WriteToolCall[] {
143
+ const messages = Array.isArray(body) ? body : [body];
144
+ const calls: WriteToolCall[] = [];
145
+
146
+ for (const message of messages) {
147
+ if (!message || typeof message !== "object") continue;
148
+ const req = message as Record<string, unknown>;
149
+ if (req["method"] !== "tools/call") continue;
150
+ const params = req["params"] as Record<string, unknown> | undefined;
151
+ if (!params) continue;
152
+ const toolName = params["name"];
153
+ if (typeof toolName !== "string" || !WRITE_TOOLS.has(toolName)) continue;
154
+ calls.push({ toolName, args: params["arguments"] ?? {} });
155
+ }
156
+
157
+ return calls;
85
158
  }
86
159
 
87
160
  /**
@@ -154,8 +227,12 @@ export async function buildHttpTransportApp(
154
227
  const body = request.body as unknown;
155
228
  const origin = request.headers["origin"] as string | undefined;
156
229
 
230
+ // A batch body counts as a write request when ANY of its messages names
231
+ // a write tool — see writeToolCalls().
232
+ const writeCalls = writeToolCalls(body);
233
+
157
234
  // Write-tool CORS enforcement
158
- if (isWriteToolCall(body)) {
235
+ if (writeCalls.length > 0) {
159
236
  if (!isOriginAllowedForWrite(origin, config.cors.writeAllowedOrigins)) {
160
237
  reply
161
238
  .code(403)
@@ -171,22 +248,18 @@ export async function buildHttpTransportApp(
171
248
  // TODO: per-tool write rate limit when @fastify/rate-limit v11 lands
172
249
  // a per-route override API compatible with our stateless route design.
173
250
 
174
- // Abuse heuristics
175
- const params = (body as Record<string, unknown>)["params"] as
176
- | Record<string, unknown>
177
- | undefined;
178
- const toolName = (params?.["name"] as string) ?? "";
179
- const args = params?.["arguments"] ?? {};
180
-
181
- const abuseResult = abuseGuard.validate(toolName, args, request);
182
- if (!abuseResult.allowed) {
183
- const statusCode =
184
- abuseResult.reason === "blocked user agent" ||
185
- abuseResult.reason?.includes("daily cap")
186
- ? 403
187
- : 422;
188
- reply.code(statusCode).send({ error: abuseResult.reason });
189
- return;
251
+ // Abuse heuristics — charged ONCE PER CALL, not once per request, so a
252
+ // batch of N cannot spend one cap unit and execute N times. The whole
253
+ // request is refused on the first message that fails, before the SDK
254
+ // dispatches any of them.
255
+ for (const { toolName, args } of writeCalls) {
256
+ const abuseResult = abuseGuard.validate(toolName, args, request);
257
+ if (!abuseResult.allowed) {
258
+ reply
259
+ .code(ABUSE_REFUSAL_STATUS[abuseResult.code])
260
+ .send({ error: abuseResult.reason });
261
+ return;
262
+ }
190
263
  }
191
264
  } else {
192
265
  // Read-tool or protocol message: check UA deny list only
@@ -13,11 +13,59 @@
13
13
  * MCP_RATE_LIMIT_WRITE_PER_MIN=10
14
14
  * MCP_CORS_WRITE_ALLOWED_ORIGINS= (empty = no cross-origin writes)
15
15
  * MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
16
+ * MCP_ABUSE_START_PURCHASE_DAILY_CAP=20
16
17
  * MCP_ABUSE_BLOCKED_USER_AGENTS= (comma-separated substrings)
18
+ *
19
+ * Content adapter (all optional; `CONTENT_ADAPTER` default: mdx):
20
+ * CONTENT_ADAPTER=mdx|projection|database
21
+ * DATABASE_URL= (required only when CONTENT_ADAPTER=database)
17
22
  */
18
23
 
19
- // Required to mark this file as an ES module (needed for top-level await).
20
- export {};
24
+ import { initMcpCommerceClaim } from "./lib/commerce-claim.js";
25
+ import { initMcpPublicWorkspace } from "./lib/public-workspace.js";
26
+
27
+ // Resolves the MCP server's own workspace ONCE, before either transport
28
+ // branch below — a no-op unless CONTENT_ADAPTER=database (#5876 spec D4/D6).
29
+ // This is the one `await` that covers both arms: `getMcpContentAdapter()`
30
+ // (content-adapter.ts) stays synchronous and reads the memoised result.
31
+ //
32
+ // A REJECTION here (a transient DB error — unreachable at boot, a migration
33
+ // in progress, a bad DATABASE_URL) is caught rather than left to crash the
34
+ // process: every OTHER tool (lead capture, scheduling, …) has nothing to do
35
+ // with the content adapter and must keep serving. The module's un-resolved
36
+ // `{ kind: "not-required" }` state is left in place on a caught failure —
37
+ // the SAME fail-closed shape an ambiguous workspace count already produces
38
+ // (D5) — so content tools serve their empty/not-found shape rather than the
39
+ // whole server going dark over one adapter's connectivity.
40
+ try {
41
+ await initMcpPublicWorkspace();
42
+ } catch (err) {
43
+ console.error(
44
+ "[mcp] initMcpPublicWorkspace() failed — content tools will serve their " +
45
+ "fail-closed empty shape under CONTENT_ADAPTER=database until this " +
46
+ "resolves; every other tool is unaffected:",
47
+ err,
48
+ );
49
+ }
50
+
51
+ // Resolves the deployment's `commerce` add-on claim ONCE, before either
52
+ // transport branch — the sibling of the workspace resolution above and the
53
+ // same #5876 idiom (#5971 spec D2). Without this await the memoised claim is
54
+ // never resolved and `start_purchase` is invisible on a KEYED site too.
55
+ //
56
+ // A REJECTION is caught for the same reason: a licence read that fails must
57
+ // not take the other 19 tools down. `initMcpCommerceClaim()` already resolves
58
+ // to the fail-closed "unclaimed" internally, so this catch is belt-and-braces
59
+ // for anything thrown outside its own try.
60
+ try {
61
+ await initMcpCommerceClaim();
62
+ } catch (err) {
63
+ console.error(
64
+ "[mcp] initMcpCommerceClaim() failed — start_purchase stays unregistered " +
65
+ "(fail-closed) until this resolves; every other tool is unaffected:",
66
+ err,
67
+ );
68
+ }
21
69
 
22
70
  const transport = process.env["MCP_TRANSPORT"] ?? "stdio";
23
71
 
@@ -38,27 +86,41 @@ if (transport === "http") {
38
86
  const writeAllowedOrigins = writeOriginsRaw
39
87
  ? writeOriginsRaw.split(",").map((o) => o.trim())
40
88
  : [];
41
- const captureLeadDailyCapPerIp = parseInt(
42
- process.env["MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP"] ?? "50",
43
- 10,
89
+ // Both caps go through readPositiveIntEnv rather than a bare parseInt: a
90
+ // BLANK env var would otherwise yield NaN and silently disable the cap
91
+ // (#5971 security review round 2).
92
+ const {
93
+ readPositiveIntEnv,
94
+ DEFAULT_CAPTURE_LEAD_DAILY_CAP,
95
+ DEFAULT_START_PURCHASE_DAILY_CAP,
96
+ } = await import("./abuse/index.js");
97
+ const captureLeadDailyCapPerIp = readPositiveIntEnv(
98
+ "MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
99
+ DEFAULT_CAPTURE_LEAD_DAILY_CAP,
44
100
  );
45
- const blockedUserAgents =
46
- process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "";
101
+ const startPurchaseDailyCapPerIp = readPositiveIntEnv(
102
+ "MCP_ABUSE_START_PURCHASE_DAILY_CAP",
103
+ DEFAULT_START_PURCHASE_DAILY_CAP,
104
+ );
105
+ const blockedUserAgents = process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "";
47
106
 
48
107
  await startHttpTransport({
49
108
  port,
50
109
  path,
51
110
  rateLimits: { readPerMin, writePerMin },
52
111
  cors: { writeAllowedOrigins },
53
- abuse: { captureLeadDailyCapPerIp, blockedUserAgents },
112
+ abuse: {
113
+ captureLeadDailyCapPerIp,
114
+ startPurchaseDailyCapPerIp,
115
+ blockedUserAgents,
116
+ },
54
117
  });
55
118
 
56
119
  console.log(`MCP HTTP transport listening on port ${port} at ${path}`);
57
120
  } else {
58
121
  // Default: stdio transport
59
- const { StdioServerTransport } = await import(
60
- "@modelcontextprotocol/sdk/server/stdio.js"
61
- );
122
+ const { StdioServerTransport } =
123
+ await import("@modelcontextprotocol/sdk/server/stdio.js");
62
124
  const { createMcpServer } = await import("./server.js");
63
125
 
64
126
  const server = createMcpServer();