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
@@ -0,0 +1,173 @@
1
+ /**
2
+ * @file revalidation.ts
3
+ * @description The signed webhook `apps/api` fires at the site's
4
+ * `/api/revalidate` on publish and unpublish (#5851, roadmap D6). One POST of
5
+ * `{ workspaceId, type, slug, locale }` to `CONTENT_REVALIDATE_URL`, signed
6
+ * HMAC-SHA256 over the exact body bytes with `CONTENT_REVALIDATE_SECRET`.
7
+ *
8
+ * Either var absent means "revalidation off": one `warn` and return. Both set
9
+ * means three bounded attempts (500 ms then 2000 ms backoff, 10 s per-attempt
10
+ * timeout — 32.5 s worst case), every attempt logged, and a final failure
11
+ * LOGGED rather than thrown. `notifyRevalidate` never rejects, which is what
12
+ * lets the publish path `await` it and still always complete.
13
+ *
14
+ * PII boundary: log lines carry only
15
+ * `{ event, workspaceId, type, slug, locale, attempt/attempts, status/lastStatus, errName }`.
16
+ * Never the secret, never the signature, never a response body, never page
17
+ * content.
18
+ *
19
+ * Nothing calls this yet — the publish/unpublish handlers are a sibling child
20
+ * of Epic #5323, and the receiver is Epic #5324. This module fixes the wire
21
+ * contract a receiver implements: method POST; the four-key JSON body;
22
+ * headers `content-type: application/json`, `x-ailk-revalidate-signature:
23
+ * sha256=<lowercase hex>` and `x-ailk-revalidate-event: publish | unpublish`.
24
+ * A receiver verifies by recomputing the HMAC over the RAW request body and
25
+ * comparing in constant time.
26
+ */
27
+ import { createHmac } from "node:crypto";
28
+
29
+ import { createLogger } from "@working-theory/observability";
30
+
31
+ import { config } from "../config/index.js";
32
+ import { outboundFetch } from "../lib/outbound-http.js";
33
+
34
+ const log = createLogger({ service: "revalidation" });
35
+
36
+ const SIGNATURE_HEADER = "x-ailk-revalidate-signature";
37
+ const EVENT_HEADER = "x-ailk-revalidate-event";
38
+
39
+ /**
40
+ * Backoff before attempt 2 and attempt 3. Three attempts total, no fourth:
41
+ * 3 × the wrapper's 10 s timeout plus these two sleeps is 32.5 s worst case,
42
+ * which fits inside the PRD's 60 s publish-to-live budget.
43
+ */
44
+ const RETRY_BACKOFF_MS = [500, 2000];
45
+ const MAX_ATTEMPTS = RETRY_BACKOFF_MS.length + 1;
46
+
47
+ export interface RevalidateTarget {
48
+ workspaceId: string;
49
+ type: string;
50
+ slug: string;
51
+ locale: string;
52
+ }
53
+
54
+ export interface RevalidateOptions {
55
+ event: "publish" | "unpublish";
56
+ }
57
+
58
+ function sleep(ms: number): Promise<void> {
59
+ return new Promise((resolve) => setTimeout(resolve, ms));
60
+ }
61
+
62
+ /**
63
+ * 429 and 5xx are worth another attempt. Every other non-2xx is terminal — a
64
+ * 401 from a wrong secret or a 404 from a wrong path will not succeed on a
65
+ * second try, and retrying it triples the log noise for a misconfiguration.
66
+ */
67
+ function isRetryableStatus(status: number): boolean {
68
+ return status === 429 || status >= 500;
69
+ }
70
+
71
+ /**
72
+ * Tells the site to revalidate one piece of content. Resolves in every case —
73
+ * success, exhausted retries, or unconfigured — so an `await` in the publish
74
+ * path can never fail the publish it follows.
75
+ */
76
+ export async function notifyRevalidate(
77
+ target: RevalidateTarget,
78
+ options: RevalidateOptions,
79
+ ): Promise<void> {
80
+ const url = config.CONTENT_REVALIDATE_URL;
81
+ const secret = config.CONTENT_REVALIDATE_SECRET;
82
+
83
+ // The PII-safe key set every line in this module carries.
84
+ const base = { event: options.event, ...target };
85
+
86
+ if (!url || !secret) {
87
+ // Signing with an empty key would be worse than skipping: every delivery
88
+ // would be rejected by the receiver with no local signal.
89
+ const reason = !url ? "url_unset" : "secret_unset";
90
+ log.warn({ ...base, reason }, "revalidate.skipped");
91
+ return;
92
+ }
93
+
94
+ // ONE serialization: this exact string is both signed and sent, so a
95
+ // key-order or whitespace drift between signer and sender is impossible by
96
+ // construction.
97
+ const body = JSON.stringify({
98
+ workspaceId: target.workspaceId,
99
+ type: target.type,
100
+ slug: target.slug,
101
+ locale: target.locale,
102
+ });
103
+ const signature = createHmac("sha256", secret)
104
+ .update(body, "utf8")
105
+ .digest("hex");
106
+
107
+ let attempt = 0;
108
+ let lastStatus: number | undefined;
109
+ let errName: string | undefined;
110
+
111
+ while (attempt < MAX_ATTEMPTS) {
112
+ attempt++;
113
+ let retryable: boolean;
114
+
115
+ try {
116
+ const res = await outboundFetch(url, {
117
+ method: "POST",
118
+ headers: {
119
+ "content-type": "application/json",
120
+ [SIGNATURE_HEADER]: `sha256=${signature}`,
121
+ [EVENT_HEADER]: options.event,
122
+ },
123
+ body,
124
+ // The SSRF gate is waived deliberately and narrowly: this URL comes
125
+ // from deployment environment, never from a request body or a
126
+ // database row. Leaving it on would break local development (the site
127
+ // is http://localhost:3000) and any single-host deployment.
128
+ allowPrivate: true,
129
+ });
130
+
131
+ if (res.ok) {
132
+ log.info({ ...base, attempt, status: res.status }, "revalidate.sent");
133
+ return;
134
+ }
135
+
136
+ lastStatus = res.status;
137
+ errName = undefined;
138
+ retryable = isRetryableStatus(res.status);
139
+ } catch (err) {
140
+ lastStatus = undefined;
141
+ errName = err instanceof Error ? err.name : "UnknownError";
142
+ retryable = true;
143
+ }
144
+
145
+ log.warn(
146
+ {
147
+ ...base,
148
+ attempt,
149
+ ...(lastStatus !== undefined && { status: lastStatus }),
150
+ ...(errName !== undefined && { errName }),
151
+ },
152
+ "revalidate.failed",
153
+ );
154
+
155
+ if (!retryable) break;
156
+
157
+ // Undefined on the last attempt — the schedule has one entry fewer than
158
+ // MAX_ATTEMPTS, so running out of backoffs IS the "no fourth attempt" bound.
159
+ const backoffMs = RETRY_BACKOFF_MS[attempt - 1];
160
+ if (backoffMs === undefined) break;
161
+ await sleep(backoffMs);
162
+ }
163
+
164
+ log.error(
165
+ {
166
+ ...base,
167
+ attempts: attempt,
168
+ ...(lastStatus !== undefined && { lastStatus }),
169
+ ...(errName !== undefined && { errName }),
170
+ },
171
+ "revalidate.failed",
172
+ );
173
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @file verified-email.ts
3
+ * @description The one `User {id, email, emailVerified}` read every
4
+ * email-keyed authorization path in `apps/api` goes through (#5701 D4).
5
+ *
6
+ * `emailVerified` is load-bearing wherever this codebase joins an identity to
7
+ * data by email. An unverified address is an unproven CLAIM to the identity it
8
+ * names: the operator pin it might match (`services/default-tenant.ts`), the
9
+ * anonymous Lead rows it would promote (`services/lead-promotion.ts`,
10
+ * `services/listing-promotion.ts`), the respondent rows it would read back
11
+ * (`services/waitlist-signups.ts`, `services/flow-checkouts.ts`). Each of
12
+ * those five sites wrote the same read and the same refusal by hand, and
13
+ * "the same read, written six times" is exactly the shape that let #3631
14
+ * ship a sixth one without the check.
15
+ *
16
+ * So the read lives here once, the refusal is inside it rather than at the
17
+ * call site — a service-level invariant, not a caller obligation, which is
18
+ * what makes it unbypassable by a wrong call site — and callers receive the
19
+ * email ONLY when `emailVerified === true`. The two refusals stay distinct
20
+ * (`user_not_found` vs `email_unverified`) because the callers map them to
21
+ * different outcomes on the wire.
22
+ *
23
+ * `select` names exactly the three fields, so nothing else about the User row
24
+ * is read on an authorization path; `tools/lint-email-authz.ts` is the
25
+ * mechanical backstop that fails any NEW `prisma.user` read which selects
26
+ * `email` for a decision and omits `emailVerified`.
27
+ *
28
+ * ONE KNOWN IMPRECISION IN THE CALLERS, deliberate and unchanged by this
29
+ * helper. `User.email` is `@unique` in Postgres, which is byte-exact, while
30
+ * every caller that compares the address lowercases both sides — so two
31
+ * distinct rows (`Operator@x.com` and `operator@x.com`) could both satisfy one
32
+ * comparison. It is not reachable today: `apps/web/auth.ts` configures social
33
+ * providers only, so the address is provider-asserted and normalized rather
34
+ * than caller-chosen. It becomes reachable the moment a credential provider is
35
+ * enabled, and the durable fix is case-insensitive email uniqueness in the
36
+ * schema (citext, or lowercase-at-write) so "matches" and "is unique" agree.
37
+ * Matching case-sensitively instead would be worse: it would lock out an
38
+ * operator who typed their own address with different capitalisation.
39
+ */
40
+
41
+ import { prisma } from "../lib/prisma.js";
42
+
43
+ /** Why a verified-email read refused. Callers map these to their own outcomes. */
44
+ export type VerifiedEmailRefusal = "user_not_found" | "email_unverified";
45
+
46
+ export type VerifiedEmailResult =
47
+ | { ok: true; value: { id: string; email: string } }
48
+ | { ok: false; reason: VerifiedEmailRefusal };
49
+
50
+ /**
51
+ * Read the `User` row for `userId` and return its email ONLY when the address
52
+ * is verified.
53
+ *
54
+ * @param userId the authenticated caller's id — never a request-supplied email.
55
+ * @returns `{ok: true}` with the verified address, or the refusal reason.
56
+ */
57
+ export async function findVerifiedEmail(
58
+ userId: string,
59
+ ): Promise<VerifiedEmailResult> {
60
+ const user = await prisma.user.findUnique({
61
+ where: { id: userId },
62
+ select: { id: true, email: true, emailVerified: true },
63
+ });
64
+ if (!user) {
65
+ return { ok: false, reason: "user_not_found" };
66
+ }
67
+ if (user.emailVerified !== true) {
68
+ return { ok: false, reason: "email_unverified" };
69
+ }
70
+ return { ok: true, value: { id: user.id, email: user.email } };
71
+ }
@@ -3,19 +3,19 @@
3
3
  * This file wraps the Fastify server for Vercel's serverless functions
4
4
  */
5
5
 
6
- import type { VercelRequest, VercelResponse } from '@vercel/node';
7
- import { logger } from '@working-theory/observability';
8
- import type { InjectOptions } from 'fastify';
6
+ import type { VercelRequest, VercelResponse } from "@vercel/node";
7
+ import { logger } from "@working-theory/observability";
8
+ import type { InjectOptions } from "fastify";
9
9
 
10
- import type { build as buildServer } from './server.js';
10
+ import type { build as buildServer } from "./server.js";
11
11
 
12
12
  type FastifyServer = Awaited<ReturnType<typeof buildServer>>;
13
13
 
14
14
  // Import the build function from server
15
15
  async function getBuildFunction(): Promise<typeof buildServer> {
16
16
  try {
17
- const srcModule = await import('./server.js');
18
- logger.info('[Vercel Handler] Using server from src/server.js');
17
+ const srcModule = await import("./server.js");
18
+ logger.info("[Vercel Handler] Using server from src/server.js");
19
19
  return srcModule.build;
20
20
  } catch (srcError) {
21
21
  logger.error(
@@ -23,9 +23,12 @@ async function getBuildFunction(): Promise<typeof buildServer> {
23
23
  err: srcError instanceof Error ? srcError.message : String(srcError),
24
24
  stack: srcError instanceof Error ? srcError.stack : undefined,
25
25
  },
26
- '[Vercel Handler] Failed to import server',
26
+ "[Vercel Handler] Failed to import server",
27
+ );
28
+ throw new Error(
29
+ `Cannot find server module: ${srcError instanceof Error ? srcError.message : String(srcError)}`,
30
+ { cause: srcError },
27
31
  );
28
- throw new Error(`Cannot find server module: ${srcError instanceof Error ? srcError.message : String(srcError)}`, { cause: srcError });
29
32
  }
30
33
  }
31
34
 
@@ -45,30 +48,42 @@ const buildPromise = getBuildFunction();
45
48
  let serverPromise: Promise<FastifyServer> | null = null;
46
49
 
47
50
  async function buildServerInstance(): Promise<FastifyServer> {
48
- logger.info('[Vercel Handler] Building server instance...');
51
+ logger.info("[Vercel Handler] Building server instance...");
49
52
  const startTime = Date.now();
50
53
 
51
54
  try {
52
55
  const buildTimeout = new Promise<never>((_, reject) => {
53
- setTimeout(() => reject(new Error('Server build timeout after 10s')), 10000);
56
+ setTimeout(
57
+ () => reject(new Error("Server build timeout after 10s")),
58
+ 10000,
59
+ );
54
60
  });
55
61
 
56
62
  const build = await Promise.race([buildPromise, buildTimeout]);
57
- logger.info('[Vercel Handler] Build function obtained, creating server...');
63
+ logger.info("[Vercel Handler] Build function obtained, creating server...");
58
64
 
59
65
  const initTimeout = new Promise<never>((_, reject) => {
60
- setTimeout(() => reject(new Error('Server initialization timeout after 10s')), 10000);
66
+ setTimeout(
67
+ () => reject(new Error("Server initialization timeout after 10s")),
68
+ 10000,
69
+ );
61
70
  });
62
71
 
63
72
  const serverInstance = await Promise.race([build(), initTimeout]);
64
73
 
65
74
  const buildTime = Date.now() - startTime;
66
- logger.info({ buildTime }, '[Vercel Handler] Server built successfully');
75
+ logger.info({ buildTime }, "[Vercel Handler] Server built successfully");
67
76
  return serverInstance;
68
77
  } catch (error) {
69
78
  const buildTime = Date.now() - startTime;
70
- logger.error({ err: error, buildTime }, '[Vercel Handler] Failed to build server');
71
- throw new Error(`Server build failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
79
+ logger.error(
80
+ { err: error, buildTime },
81
+ "[Vercel Handler] Failed to build server",
82
+ );
83
+ throw new Error(
84
+ `Server build failed: ${error instanceof Error ? error.message : String(error)}`,
85
+ { cause: error },
86
+ );
72
87
  }
73
88
  }
74
89
 
@@ -102,27 +117,46 @@ function getServer(): Promise<FastifyServer> {
102
117
  */
103
118
  export default async function handler(
104
119
  req: VercelRequest,
105
- res: VercelResponse
120
+ res: VercelResponse,
106
121
  ): Promise<void> {
107
122
  const timeout = setTimeout(() => {
108
123
  if (!res.headersSent) {
109
- logger.error('[Vercel Handler] Request timeout');
124
+ logger.error("[Vercel Handler] Request timeout");
110
125
  res.status(504).json({
111
- error: 'Gateway Timeout',
112
- message: 'Request took too long to process',
126
+ error: "Gateway Timeout",
127
+ message: "Request took too long to process",
113
128
  });
114
129
  }
115
130
  }, 25000);
116
131
 
117
132
  try {
118
- logger.info({ method: req.method, url: req.url }, '[Vercel Handler] Starting request');
119
-
120
- const server = await getServer();
121
-
122
- const url = new URL(req.url || '/', 'http://localhost');
133
+ // The PATHNAME, never the raw target (#5970 security review round 2, H1).
134
+ //
135
+ // This is the production entry point — `vercel.json` routes every request
136
+ // here — and it logs BEFORE Fastify's own (query-stripped) "incoming
137
+ // request" line, so it is the first and last word on what reaches the log
138
+ // drain. `req.url` is the raw target INCLUDING the query string, and a
139
+ // route may carry its credential there: `GET
140
+ // /v1/requests/:id/confirm?token=…` does, because a link in an email has
141
+ // nowhere else to put one. Logging it wrote a live seven-day credential
142
+ // into the drain on every open.
143
+ //
144
+ // The high-entropy redactor in `@working-theory/observability` is NOT the
145
+ // control here: it is a probabilistic backstop, and a 43-char base64url
146
+ // token falls under its entropy threshold often enough to leak in full.
147
+ // Stripping the query is the control; the redactor stays the backstop.
148
+ //
149
+ // Derived once, above the first log line, and used by both — the pathname
150
+ // was already computed below and logged safely there, three lines too late.
151
+ const url = new URL(req.url || "/", "http://localhost");
123
152
  const pathname = url.pathname;
124
153
 
125
- logger.info({ method: req.method, pathname }, '[Vercel Handler] Processing request');
154
+ logger.info(
155
+ { method: req.method, pathname },
156
+ "[Vercel Handler] Starting request",
157
+ );
158
+
159
+ const server = await getServer();
126
160
 
127
161
  // Strip content-length/content-encoding before forwarding. Both
128
162
  // describe the ORIGINAL byte stream the client sent; req.body here is
@@ -137,13 +171,16 @@ export default async function handler(
137
171
  // incoming header names in practice.
138
172
  const forwardedHeaders = { ...req.headers } as Record<string, string>;
139
173
  for (const key of Object.keys(forwardedHeaders)) {
140
- if (key.toLowerCase() === 'content-length' || key.toLowerCase() === 'content-encoding') {
174
+ if (
175
+ key.toLowerCase() === "content-length" ||
176
+ key.toLowerCase() === "content-encoding"
177
+ ) {
141
178
  delete forwardedHeaders[key];
142
179
  }
143
180
  }
144
181
 
145
182
  const fastifyRequest = {
146
- method: req.method || 'GET',
183
+ method: req.method || "GET",
147
184
  url: pathname,
148
185
  headers: forwardedHeaders,
149
186
  body: req.body,
@@ -152,7 +189,10 @@ export default async function handler(
152
189
 
153
190
  const response = await server.inject(fastifyRequest as InjectOptions);
154
191
 
155
- logger.info({ status: response.statusCode }, '[Vercel Handler] Response status');
192
+ logger.info(
193
+ { status: response.statusCode },
194
+ "[Vercel Handler] Response status",
195
+ );
156
196
 
157
197
  clearTimeout(timeout);
158
198
 
@@ -161,14 +201,14 @@ export default async function handler(
161
201
  Object.entries(response.headers).forEach(([key, value]) => {
162
202
  if (value !== undefined && !res.headersSent) {
163
203
  const headerValue = Array.isArray(value)
164
- ? value.map((v) => String(v)).join(', ')
204
+ ? value.map((v) => String(v)).join(", ")
165
205
  : String(value);
166
206
  res.setHeader(key, headerValue);
167
207
  }
168
208
  });
169
209
 
170
- if (!res.getHeader('Content-Type') && !res.headersSent) {
171
- res.setHeader('Content-Type', 'application/json');
210
+ if (!res.getHeader("Content-Type") && !res.headersSent) {
211
+ res.setHeader("Content-Type", "application/json");
172
212
  }
173
213
 
174
214
  if (!res.headersSent) {
@@ -179,20 +219,20 @@ export default async function handler(
179
219
  // `rawPayload` is byte-identical to what Fastify sent regardless of
180
220
  // content type, so this also covers the JSON case unchanged.
181
221
  res.send(response.rawPayload);
182
- logger.info('[Vercel Handler] Response sent successfully');
222
+ logger.info("[Vercel Handler] Response sent successfully");
183
223
  }
184
224
  } catch (error) {
185
225
  clearTimeout(timeout);
186
226
 
187
- logger.error({ err: error }, '[Vercel Handler] Error handling request');
227
+ logger.error({ err: error }, "[Vercel Handler] Error handling request");
188
228
 
189
229
  if (!res.headersSent) {
190
230
  // Generic body only (security review on #146): this catch sits above
191
231
  // Fastify's error serializer, so error.message could carry hosts,
192
232
  // paths, or driver detail. The logger.error above keeps the detail.
193
233
  res.status(500).json({
194
- error: 'Internal Server Error',
195
- message: 'Something went wrong on our end.',
234
+ error: "Internal Server Error",
235
+ message: "Something went wrong on our end.",
196
236
  });
197
237
  }
198
238
  }
@@ -53,8 +53,25 @@ MCP_CORS_WRITE_ALLOWED_ORIGINS=
53
53
  # Maximum number of capture_lead submissions per IP per day.
54
54
  MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
55
55
 
56
+ # Maximum number of start_purchase calls per IP per day (#5971). Its own
57
+ # budget, never shared with capture_lead's — each call creates a real Stripe
58
+ # hosted Checkout Session, so the default is deliberately lower.
59
+ MCP_ABUSE_START_PURCHASE_DAILY_CAP=20
60
+
56
61
  # Comma-separated list of User-Agent substrings to block (case-insensitive).
57
62
  # Built-in list already blocks curl, wget, python-requests, scrapy, etc.
58
63
  # Add extra patterns here as needed.
59
64
  # Example: BadBot,EvilCrawler
60
65
  MCP_ABUSE_BLOCKED_USER_AGENTS=
66
+
67
+ # ── Content adapter (#5876) ──────────────────────────────────────────────────────
68
+
69
+ # Which content adapter every content tool resolves: mdx (default, reads
70
+ # /content/*.mdx at the repo root) | projection (compiled manifest) | database
71
+ # (the Artifact store — the server resolves its own single workspace at
72
+ # bootstrap; a deployment that isn't unambiguously one-workspace serves no
73
+ # content rather than guessing).
74
+ # CONTENT_ADAPTER=mdx
75
+
76
+ # Required ONLY when CONTENT_ADAPTER=database.
77
+ # DATABASE_URL=
@@ -28,38 +28,67 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
28
28
  **Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366): identity-blind and free of any `requirePaidTier()` call like every tool in this list, but registered only when the signal-collectors module is present, the same shape Booking below already has
29
29
  **Booking (3):** `list_availability`, `get_event_meta`, `create_booking` — module-gated (`module: "scheduling"`)
30
30
 
31
- ### Why the catalog reports 23 and the server registers 22
32
-
33
- - **23 in the catalog / parity test**: the `TOOLS` array in
34
- `packages/metadata/src/mcp-tool-catalog.ts` defines 23 tools; the parity
35
- test counts that full catalog.
36
- - **22 registered at runtime**: `createMcpServer` (`apps/mcp/src/server.ts`)
37
- registers `getMcpToolRegistrations()` minus `OSS_DISABLED_TOOLS`, whose only
38
- entry is `start_purchase` — so 22 tools are registered on the server.
39
- - **19 free anonymous tools**: of the 22 registered, three are Pro tools
40
- (`create_page`, `update_page`, `delete_page`) gated at call time by
41
- `requirePaidTier()`, leaving the 19 free anonymous tools above.
31
+ ### Why the parity test counts 20 and a keyless server registers 19
32
+
33
+ - **20 in the `TOOLS` array / parity test**: the `TOOLS` array in
34
+ `packages/metadata/src/mcp-tool-catalog.ts` defines 20 tools; the parity
35
+ test counts that full array.
36
+ - **19 registered on a KEYLESS deployment, 20 on a commerce-claimed one**
37
+ (#5971): `createMcpServer` (`apps/mcp/src/server.ts`) registers
38
+ `getMcpToolRegistrations()` minus `OSS_DISABLED_TOOLS` (now empty), minus
39
+ module-gated tools whose module is off, minus any `addon`-tagged tool whose
40
+ claim this deployment does not hold. `start_purchase` carries
41
+ `addon: "commerce"`, so it is the one entry whose presence depends on the
42
+ licence — 19 without the claim, 20 with it.
43
+ - **19 free anonymous tools**: since #5853 retired the three content-write
44
+ tools, NO registered tool is gated at call time by `requirePaidTier()`, so
45
+ every registered tool is free and anonymous. The five carrying `tier: "pro"`
46
+ (`get_flow`, `submit_flow_step`, `list_availability`, `get_event_meta`,
47
+ `create_booking`) are MODULE-gated at registration, not payment-gated at
48
+ call time — the shape the Flow and Booking rows above describe.
42
49
  `index.ts` is the transport entry point and exports no tools — registration
43
50
  is entirely via `createMcpServer`.
44
51
 
45
52
  ## Pro + disabled tools (not part of the free surface)
46
53
 
47
- - `create_page`, `update_page`, `delete_page` — high-stakes CMS writes,
48
- `tier: "pro"`. Registered by `createMcpServer` but gated at call time by
49
- `requirePaidTier()` (`apps/mcp/src/lib/require-paid-tier.ts`) — the paid-tier
50
- check lives in the handler, never in the tool signature, per the
51
- identity-blind rule above.
52
- - `start_purchase` — transactional, Pro-only; the sole entry in
53
- `OSS_DISABLED_TOOLS`, so it is filtered out of catalog registration and the
54
- server entirely. Re-enable criteria live at
54
+ - `create_page`, `update_page`, `delete_page` — **RETIRED by #5853**, not
55
+ merely disabled. Their HTTP twins (the untenanted `/v1/content` writes) were
56
+ deleted in the same PR, so keeping the tools would have left three MCP tools
57
+ with no HTTP equivalent — a direct breach of the API↔MCP parity rule. Their
58
+ replacement is the tenant-guarded
59
+ `/v1/workspaces/:workspaceId/content` family, which resolves its scope from a
60
+ session-bound `TenantContext`; an anonymous caller supplying a `workspaceId`
61
+ is exactly the client-supplied scope that surface exists to refuse, so the
62
+ identity-blind MCP catalog can never carry it. `OSS_DISABLED_TOOLS` was the
63
+ wrong instrument: it promises a re-enable path, and there is none until the
64
+ authed MCP surface (#3591) exists.
65
+ - `start_purchase` — **no longer disabled** (#5971). It returns a
66
+ Stripe-HOSTED Checkout URL for a named offering and charges nothing in-tool;
67
+ the buyer pays on Stripe's own page. It left `OSS_DISABLED_TOOLS` — which is
68
+ now empty — and its visibility is gated instead on the deployment's
69
+ `commerce` licence claim, per its `addon: "commerce"` catalog marker. A
70
+ keyless site advertises it in neither the discovery manifest nor the running
71
+ server's tool list, and calling it by name there returns the module-disabled
72
+ `not_found` body. The claim is resolved once at boot
73
+ (`src/lib/commerce-claim.ts`, awaited in `src/index.ts`) and read
74
+ synchronously by both the registration filter and the handler's own
75
+ re-check. Calls are rate-limited per IP by the AbuseGuard
76
+ (`MCP_ABUSE_START_PURCHASE_DAILY_CAP`, default 20, on its own tracker).
77
+ The disposition of the old Phase 3 re-enable criteria — 1 and 2 retired as
78
+ inapplicable to a hosted handoff, 3 and 4 met — is recorded at
55
79
  `docs/internal/roadmaps/phase-3-readiness.md`.
56
80
 
57
81
  **Re-derive these counts rather than trusting them here** — the catalog in
58
82
  `packages/metadata/src/mcp-tool-catalog.ts` is the source; free anonymous =
59
- catalog − `OSS_DISABLED_TOOLS` − `tier: "pro"`.
83
+ catalog − `OSS_DISABLED_TOOLS` − `tier: "pro"` − any `addon` whose claim this
84
+ deployment does not hold.
60
85
 
61
- Don't add identity logic to any tool signature. Don't remove `start_purchase`
62
- from `OSS_DISABLED_TOOLS` without satisfying the Phase 3 re-enable criteria.
86
+ Don't add identity logic to any tool signature. Don't un-gate an `addon`-tagged
87
+ tool — the claim is a DEPLOYMENT property read from the licence, never a
88
+ caller-supplied argument, and every gate fails closed. Don't add a name to
89
+ `OSS_DISABLED_TOOLS` where an entitlement gate is what the tool actually
90
+ needs; and don't remove one from it without the review
91
+ `docs/internal/roadmaps/phase-3-readiness.md` describes.
63
92
 
64
93
  ## Tool implementation pattern
65
94
 
@@ -87,8 +116,12 @@ Example (lead routing):
87
116
  - Don't add a tool without the matching `apps/api` endpoint.
88
117
  - Don't put business logic in tool handlers — they're adapters.
89
118
 
90
- Last updated: 2026-07-09 — corrected the tool-count explanation (18 catalog → 17 registered → 14 free anonymous) and the Pro-tool gating (registered + `requirePaidTier()`-gated, not `OSS_DISABLED_TOOLS`-disabled).
119
+ Last updated: 2026-09-17 — #5971: `start_purchase` re-enabled behind the `commerce` licence claim. `OSS_DISABLED_TOOLS` is now empty; the registered count is 19 on a keyless deployment and 20 on a commerce-claimed one; the tool's input field is `productCode` (was `priceId`), matching its HTTP twin; a per-IP daily cap (`MCP_ABUSE_START_PURCHASE_DAILY_CAP`) covers it.
91
120
 
92
- Last updated: 2026-08-26 — doc-sync: tool-count explanation "18 catalog → 17 registered → 14 free anonymous" → "22 catalog → 21 registered → 18 free anonymous" (derived from `packages/metadata/src/mcp-tool-catalog.ts`: `get_flow`/`submit_flow_step` landed via PR #3973, `list_availability`/`create_booking` landed via PR #4421). Added the Booking category to the free-tool enumeration above. `CLAUDE.md` and `docs/ARCHITECTURE.md` re-verified as non-drift (already correct); `packages/create-ailk/templates/apps/mcp/CLAUDE.md` no longer exists in the scaffolder template tree (structure changed since the count was scoped there).
121
+ Prior: 2026-07-09 — corrected the tool-count explanation (18 catalog → 17 registered → 14 free anonymous) and the Pro-tool gating (registered + `requirePaidTier()`-gated, not `OSS_DISABLED_TOOLS`-disabled).
122
+
123
+ Last updated: 2026-09-17 — doc-sync: tool counts "23 catalog → 22 registered → 19 free anonymous" → "20 in TOOLS → 19 registered → 19 free anonymous", and the Pro-tools section rewritten to record that `create_page`/`update_page`/`delete_page` were RETIRED by #5853 rather than disabled (D7) — derived from `packages/metadata/src/mcp-tool-catalog.ts` and verified against `apps/mcp/src/server.ts`.
124
+
125
+ Prior: 2026-08-26 — doc-sync: tool-count explanation "18 catalog → 17 registered → 14 free anonymous" → "22 catalog → 21 registered → 18 free anonymous" (derived from `packages/metadata/src/mcp-tool-catalog.ts`: `get_flow`/`submit_flow_step` landed via PR #3973, `list_availability`/`create_booking` landed via PR #4421). Added the Booking category to the free-tool enumeration above. `CLAUDE.md` and `docs/ARCHITECTURE.md` re-verified as non-drift (already correct); `packages/create-ailk/templates/apps/mcp/CLAUDE.md` no longer exists in the scaffolder template tree (structure changed since the count was scoped there).
93
126
 
94
127
  Last updated: 2026-08-31 — doc-sync: tool counts 22/21/18 → 23/22/19 and Booking (2) → Booking (3): `get_event_meta` added (#4633), the meta-only scheduling read twinning `GET /v1/schedule/event-meta` — derived from `packages/metadata/src/mcp-tool-catalog.ts`.
@@ -25,7 +25,10 @@ import {
25
25
  entityMarkup,
26
26
  faqHowtoIncentive,
27
27
  jsonldValidity,
28
+ offerOnPricedPage,
29
+ policiesPresent,
28
30
  schemaCoverage,
31
+ taxCodeWhenStripeTax,
29
32
  type Rule,
30
33
  } from "@working-theory/aeo";
31
34
 
@@ -44,6 +47,9 @@ const RULES: Rule[] = [
44
47
  jsonldValidity,
45
48
  faqHowtoIncentive,
46
49
  entityMarkup,
50
+ policiesPresent,
51
+ offerOnPricedPage,
52
+ taxCodeWhenStripeTax,
47
53
  ];
48
54
 
49
55
  const FIXTURE_SLUG = "home";