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,92 +1,175 @@
1
1
  /**
2
2
  * AGENT TOOL — purchase (`start_purchase`)
3
3
  *
4
- * DISABLED AT OSS CATALOG (S15a, 2026-05-12, issue #610).
4
+ * Starts a purchase for a named offering and returns a **Stripe-hosted
5
+ * Checkout URL**. Nothing is charged here: the tool builds the payload the
6
+ * HTTP twin `POST /v1/checkout/sessions` validates, and the buyer completes
7
+ * payment on Stripe's own page. That hosted handoff is the money boundary
8
+ * `.claude/rules/vocabulary.md` § Commerce requires — no card data reaches
9
+ * us, and we are not a Stripe Connect platform.
5
10
  *
6
- * The implementation below is preserved for the eventual Phase 3 re-enable.
7
- * The MCP catalog at `packages/metadata/src/mcp-tool-catalog.ts` filters this
8
- * tool out via `OSS_DISABLED_TOOLS`, and `apps/mcp/src/server.ts` applies the
9
- * same filter defensively so neither the discovery manifest nor the running
10
- * server advertises it.
11
+ * VISIBILITY — the commerce add-on claim (#5971, superseding S15a/#610).
12
+ * This tool carries `addon: "commerce"` in the catalog, and since #5971 that
13
+ * marker is its visibility gate: it reaches neither the discovery manifest
14
+ * nor the running server's tool list unless the deployment positively holds
15
+ * the `commerce` licence claim. Two gates enforce that, deliberately:
11
16
  *
12
- * Re-enablement requires the Phase 3 criteria documented at
13
- * `docs/internal/roadmaps/phase-3-readiness.md`:
17
+ * 1. REGISTRATION — `shouldRegister()` in `../server.js` drops the entry.
18
+ * 2. CALL TIME — the first statement below re-reads the claim.
14
19
  *
15
- * 1. Identity verification confirmed at session level
16
- * 2. Payment-authorization handshake with Stripe Connect (or equivalent)
17
- * 3. Threat-model entry M_purchase reclassified from "Disabled" to
18
- * "Remediated"
19
- * 4. Commercial-tier flag gating registered to allow re-enable only on
20
- * Pro / Agencies
20
+ * Gate 2 is not redundant. The HTTP transport's stateless mode builds a fresh
21
+ * `McpServer` per request, so the gate must be re-read rather than captured;
22
+ * `ailkPurchase` is an exported function a scaffold or a test can call
23
+ * directly, bypassing registration entirely; and it gives "calling it by name
24
+ * on a keyless site" a deterministic, asserted body rather than whatever the
25
+ * SDK emits for an unregistered name. That body is `notFoundError("Not
26
+ * found")` — the MCP twin of the checkout route's own
27
+ * `commerceDisabledResponse()`, same code, same message, no hint that a
28
+ * licence exists.
21
29
  *
22
- * To re-enable: remove `"start_purchase"` from `OSS_DISABLED_TOOLS` in
23
- * `packages/metadata/src/mcp-tool-catalog.ts`. The regression tests at
24
- * `apps/mcp/__tests__/purchase-disabled.test.ts` and
25
- * `packages/metadata/__tests__/purchase-not-in-catalog.test.ts` will fail
26
- * loudly, forcing a deliberate review against the criteria above.
30
+ * REDIRECTS — never caller-supplied on this surface (#6001). An anonymous
31
+ * caller has no way to prove it controls a redirect target, so
32
+ * `successUrl` / `cancelUrl` are ALWAYS derived from `AILK_BASE_URL` below,
33
+ * never accepted from the input, and `allowPromotionCodes` is never accepted
34
+ * either. This function's own gate is `purchaseMcpInputSchema` — a
35
+ * `.strict()` `.pick()` of the shared `purchaseInputSchema` exposing exactly
36
+ * `productCode` + `quantity` — so a DIRECT call to `ailkPurchase` (the
37
+ * surface this file's own tests exercise, per the note above) carrying any
38
+ * of the three fails validation outright. A call through the registered SDK
39
+ * transport has them stripped one layer earlier, by the SDK's own arg parse
40
+ * against the registered shape, before this function ever runs — they are
41
+ * simply not part of that shape. Either path is safe: the payload built
42
+ * below can never carry a caller-supplied redirect. The shared
43
+ * `purchaseInputSchema` (and the Server Action / API route that use it) is
44
+ * unaffected: those are not anonymous surfaces and keep accepting a
45
+ * caller-supplied redirect.
27
46
  *
28
- * --- Original Phase 3 U2 implementation notes ---
47
+ * The file stays in the BASE tree and is never added to
48
+ * `MODULE_SLICES.commerce.ownedFiles`: `../server.js` statically imports
49
+ * `ailkPurchase`, so a slice that deleted it from a keyless scaffold would
50
+ * produce `ERR_MODULE_NOT_FOUND` inside the server bootstrap. A keyless
51
+ * scaffold carries the file and registers nothing — exactly how it already
52
+ * carries `apps/api/src/routes/checkout/sessions.ts`.
29
53
  *
30
- * Proxies POST /api/checkout/sessions and returns the Stripe hosted-checkout
31
- * URL. Paired with the `purchaseAction` Server Action in apps/web per ADR 0013.
54
+ * Schemas live in `@working-theory/validation` per ADR 0013. Network calls go
55
+ * through `@working-theory/api-client` per the architecture invariants (apps
56
+ * may not import each other; the shared HTTP surface lives in packages/).
32
57
  *
33
- * Schemas live in `@working-theory/validation` per ADR 0013.
34
- * Network calls go through `@working-theory/api-client` per architecture invariants
35
- * (apps may not import from each other; shared HTTP surface lives in packages/).
58
+ * Spec: `docs/specs/5971-start-purchase-hosted-checkout-spec.md`.
36
59
  */
37
60
 
38
61
  import { checkout } from "@working-theory/api-client";
39
62
  import {
40
- purchaseInputSchema,
41
- type PurchaseInput,
63
+ purchaseMcpInputSchema,
64
+ type PurchaseMcpInput,
42
65
  type PurchaseOutput,
43
66
  } from "@working-theory/validation";
44
67
 
68
+ import { mcpCommerceClaim } from "../lib/commerce-claim.js";
69
+ import {
70
+ notFoundError,
71
+ validationError,
72
+ type ErrorBody,
73
+ } from "../lib/errors.js";
74
+
75
+ /**
76
+ * The module-disabled body. Identical to what a build without commerce
77
+ * produces and to the checkout route's own `commerceDisabledResponse()` —
78
+ * it must not leak that a licence gate is what answered.
79
+ */
80
+ function commerceDisabledBody(): ErrorBody {
81
+ return notFoundError("Not found");
82
+ }
83
+
45
84
  /**
46
85
  * Export name `ailkPurchase` satisfies the parity-test naming convention
47
86
  * (ailk<PascalVerb>) so the harness can discover it.
48
87
  *
49
- * Paired with `purchaseAction` Server Action in apps/web/app/dev/purchase/.
88
+ * Paired with `purchaseAction` Server Action in
89
+ * apps/web/app/[locale]/dev/purchase/.
90
+ *
91
+ * Returns a typed error BODY rather than throwing on every failure an agent
92
+ * can act on (FR-2) — MCP tools surface errors as their result body, the
93
+ * pattern `lib/require-paid-tier.ts` already establishes.
50
94
  */
51
95
  export async function ailkPurchase(
52
- input: PurchaseInput,
53
- ): Promise<PurchaseOutput> {
54
- // Validate input before any side effect (security requirement).
55
- purchaseInputSchema.parse(input);
96
+ input: PurchaseMcpInput,
97
+ ): Promise<PurchaseOutput | ErrorBody> {
98
+ // Gate 2 — call time. Before validation, before any side effect.
99
+ if (mcpCommerceClaim() !== "claimed") {
100
+ return commerceDisabledBody();
101
+ }
102
+
103
+ // Validate input before any side effect (security requirement). Strict
104
+ // (#6001): a DIRECT call carrying successUrl, cancelUrl or
105
+ // allowPromotionCodes fails here — this function never accepts a
106
+ // caller-supplied redirect. (A call through the registered SDK transport
107
+ // never reaches this line with those fields present at all — see the
108
+ // REDIRECTS note above.) Every field this schema DOES accept mirrors the
109
+ // route's own validation (spec D5), so an input this accepts is an input
110
+ // the route accepts.
111
+ const parsed = purchaseMcpInputSchema.safeParse(input);
112
+ if (!parsed.success) {
113
+ return validationError("Invalid start_purchase input", {
114
+ issues: parsed.error.issues.map((issue) => ({
115
+ path: issue.path as (string | number)[],
116
+ message: issue.message,
117
+ })),
118
+ });
119
+ }
56
120
 
57
121
  const baseUrl =
58
122
  process.env.AILK_BASE_URL ||
59
123
  process.env.NEXT_PUBLIC_BASE_URL ||
60
124
  "http://localhost:3000";
61
125
 
62
- const successUrl =
63
- input.successUrl ?? `${baseUrl}/purchase/success`;
64
- const cancelUrl =
65
- input.cancelUrl ?? `${baseUrl}/purchase/cancel`;
126
+ const { productCode, quantity } = parsed.data;
66
127
 
128
+ // successUrl/cancelUrl are ALWAYS derived from baseUrl (#6001) — never
129
+ // accepted from the caller. allowPromotionCodes is never forwarded: it is
130
+ // not part of purchaseMcpInputSchema, so it can never be present here.
131
+ //
132
+ // #6084 D1 — `successUrl` carries Stripe's own `{CHECKOUT_SESSION_ID}`
133
+ // template token, substituted into the redirect at completion time. This
134
+ // is what lets `/purchase/success` read the session back (D1). `cancelUrl`
135
+ // deliberately carries NO template: that page reads nothing (D4), and
136
+ // Stripe's substitution contract is documented for `success_url` only.
67
137
  const result = await checkout.createSession({
68
- productCode: input.priceId,
69
- ...(input.quantity !== undefined && { quantity: input.quantity }),
70
- successUrl,
71
- cancelUrl,
72
- ...(input.allowPromotionCodes !== undefined && {
73
- allowPromotionCodes: input.allowPromotionCodes,
74
- }),
138
+ productCode,
139
+ ...(quantity !== undefined && { quantity }),
140
+ successUrl: `${baseUrl}/purchase/success?session_id={CHECKOUT_SESSION_ID}`,
141
+ cancelUrl: `${baseUrl}/purchase/cancel`,
75
142
  });
76
143
 
77
144
  if (!result.ok) {
78
- throw new Error(
79
- `checkout-unavailable: POST /api/checkout/sessions returned ${result.status}`,
145
+ // An unknown offering is the route's 404 `unknown-product-code`, and
146
+ // `packages/stripe-client` throws it BEFORE `stripe.checkout.sessions
147
+ // .create` — so no Checkout Session exists (FR-2).
148
+ if (result.status === 404) {
149
+ // Deliberately does NOT echo the caller's productCode. The HTTP twin
150
+ // states and tests that invariant ("Error responses never echo user
151
+ // input", apps/api/src/routes/checkout/sessions.ts), and parity means
152
+ // the pair answers alike.
153
+ return notFoundError("No offering matches that product code.");
154
+ }
155
+ return validationError(
156
+ `checkout-unavailable: POST /v1/checkout/sessions returned ${result.status}`,
157
+ { status: result.status },
80
158
  );
81
159
  }
82
160
 
161
+ // Only `url` is surfaced. The route's `sessionId` is deliberately withheld:
162
+ // an anonymous agent has no use for it and it is the one field that
163
+ // identifies a specific buyer's session (spec D5).
83
164
  return { url: result.body.url };
84
165
  }
85
166
 
86
167
  // Re-export schemas/types from @working-theory/validation for barrel consumers.
87
168
  export {
88
169
  purchaseInputSchema,
170
+ purchaseMcpInputSchema,
89
171
  purchaseOutputSchema,
90
172
  type PurchaseInput,
173
+ type PurchaseMcpInput,
91
174
  type PurchaseOutput,
92
175
  } from "@working-theory/validation";
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * read_content — returns a single page's frontmatter + raw MDX body.
3
3
  *
4
- * Pure read: imports the MDX adapter from @working-theory/content-adapters (ADR 0011).
4
+ * Pure read: resolves its adapter through `getMcpMdxContentAdapter()` — the
5
+ * MDX-narrowed twin of the one resolver seam (#5805, ADR 0011; needed here
6
+ * for `getPageSource()`'s raw body, which the base `ContentAdapter` contract
7
+ * doesn't carry), MDX by default.
5
8
  * Composes getPageBySlug (frontmatter) + getPageSource (body) into a single
6
9
  * output shape. Returns null when the slug is not found — never throws for
7
10
  * a missing page.
@@ -10,14 +13,15 @@
10
13
  * allowlist; invalid or unknown values silently fall back to the project's
11
14
  * default locale (AC-i18n-07). The adapter applies its own fallback chain.
12
15
  *
13
- * Input: { type: SchemaOrgType, slug: string, locale?: string }
16
+ * Input: { type: SchemaOrgType, slug: string, locale?: string } — `type`
17
+ * admits the full SchemaOrgType set (#6030 D1), mirrored in
18
+ * @working-theory/validation's MCP_SCHEMA_ORG_TYPES.
14
19
  * Output: { slug, type, frontmatter, body } | null
15
20
  * Session/scope: none — read-only public content.
16
21
  */
17
22
 
18
23
  import { join } from "node:path";
19
24
 
20
- import { createMdxAdapter } from "@working-theory/content-adapters";
21
25
  import {
22
26
  readContentInputSchema,
23
27
  readContentOutputSchema,
@@ -25,6 +29,7 @@ import {
25
29
  type ReadContentOutput,
26
30
  } from "@working-theory/validation";
27
31
 
32
+ import { getMcpMdxContentAdapter } from "../lib/content-adapter.js";
28
33
  import { resolveLocale } from "../lib/resolve-locale.js";
29
34
 
30
35
  export { readContentInputSchema, readContentOutputSchema };
@@ -47,14 +52,23 @@ export async function ailkReadContent(
47
52
  const root = contentRoot ?? join(process.cwd(), "content");
48
53
  const locale = resolveLocale(input.locale);
49
54
 
50
- const adapter = createMdxAdapter({ contentRoot: root, defaultLocale: locale });
55
+ const adapter = getMcpMdxContentAdapter({ contentRoot: root, locale });
51
56
 
52
57
  const source = await adapter.getPageSource(input.type, input.slug, locale);
53
58
  if (source === null) return null;
54
59
 
55
60
  return readContentOutputSchema.parse({
56
- slug: source.frontmatter.slug,
57
- type: source.frontmatter.type,
61
+ // Prefer the stored frontmatter's slug/type; fall back to the
62
+ // request-validated input (#5876 spec D9). On the MDX path the
63
+ // frontmatter always carries both — required, non-optional fields of
64
+ // FrontmatterSchema — so this is inert there and the mdx payload stays
65
+ // byte-identical. The fallback matters for cross-adapter identity: the
66
+ // Epic #5324 AC-WCE-04 comparison needs `read_content`'s payload to be
67
+ // identical across adapters, which a request-validated value guarantees
68
+ // and a stored-bookkeeping value only guarantees if the write path
69
+ // agrees.
70
+ slug: source.frontmatter.slug ?? input.slug,
71
+ type: source.frontmatter.type ?? input.type,
58
72
  frontmatter: source.frontmatter as Record<string, unknown>,
59
73
  body: source.body,
60
74
  });
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * search_content — full-text search across published content pages.
3
3
  *
4
- * Pure read: imports the MDX adapter from @working-theory/content-adapters (ADR 0011).
4
+ * Pure read: resolves its adapter through `getMcpContentAdapter()` — the one
5
+ * resolver seam (#5805, ADR 0011), MDX by default.
5
6
  * Delegates to adapter.searchPages which scores by title/description/body match.
6
7
  * Limit defaults to 10; max 50. Query capped at 200 chars (enforced by Zod).
7
8
  *
@@ -9,14 +10,18 @@
9
10
  * requested locale's content directory is searched. Invalid or unknown values
10
11
  * silently fall back to the project's default locale (AC-i18n-07).
11
12
  *
12
- * Input: { query: string, type?: SchemaOrgType, limit?: number, locale?: string }
13
+ * #5831: optional `category` parameter — a leaf slug from the site-category
14
+ * registry. Unlike `locale`, an unrecognised value is a validation error, not
15
+ * a silent fallback: the registry is closed, so a bad slug is a caller mistake.
16
+ *
17
+ * Input: { query: string, type?: SchemaOrgType, category?: CategorySlug,
18
+ * limit?: number, locale?: string }
13
19
  * Output: { results: Array<{ slug, type, title, snippet, score }> }
14
20
  * Session/scope: none — read-only public content.
15
21
  */
16
22
 
17
23
  import { join } from "node:path";
18
24
 
19
- import { createMdxAdapter } from "@working-theory/content-adapters";
20
25
  import {
21
26
  searchContentInputSchema,
22
27
  searchContentOutputSchema,
@@ -24,6 +29,7 @@ import {
24
29
  type SearchContentOutput,
25
30
  } from "@working-theory/validation";
26
31
 
32
+ import { getMcpContentAdapter } from "../lib/content-adapter.js";
27
33
  import { resolveLocale } from "../lib/resolve-locale.js";
28
34
 
29
35
  export { searchContentInputSchema, searchContentOutputSchema };
@@ -45,11 +51,12 @@ export async function ailkSearchContent(
45
51
 
46
52
  const root = contentRoot ?? join(process.cwd(), "content");
47
53
  const locale = resolveLocale(input.locale);
48
- const adapter = createMdxAdapter({ contentRoot: root, defaultLocale: locale });
54
+ const adapter = getMcpContentAdapter({ contentRoot: root, locale });
49
55
 
50
56
  const options = {
51
57
  limit: input.limit ?? DEFAULT_LIMIT,
52
58
  ...(input.type !== undefined && { type: input.type }),
59
+ ...(input.category !== undefined && { category: input.category }),
53
60
  locale,
54
61
  };
55
62
 
@@ -5,16 +5,25 @@
5
5
  # overrides apps/web reads at runtime.
6
6
 
7
7
  # =============================================================================
8
- # Content backend
8
+ # Content adapter
9
9
  # =============================================================================
10
10
  # Which content adapter apps/web uses.
11
11
  # mdx — reads /content/<Type>/<slug>.mdx (default)
12
- # pro-cms — separate adapter, not bundled with the OSS template
13
- CONTENT_BACKEND=mdx
12
+ # projection — compiled build-time manifest
13
+ # database — the Artifact store (needs a workspaceId)
14
+ CONTENT_ADAPTER=mdx
15
+ # CONTENT_BACKEND is deprecated — read for one release, then removed.
14
16
 
15
17
  # Override the on-disk content root. Defaults to <repo-root>/content.
16
18
  # AILK_CONTENT_ROOT=
17
19
 
20
+ # The shared secret /api/revalidate verifies apps/api's publish webhook
21
+ # against — the SAME value as the API's CONTENT_REVALIDATE_SECRET (#5877).
22
+ # Needed when CONTENT_ADAPTER=database. Unset means the route returns 503 and
23
+ # the site is not push-revalidated: a publish changes nothing user-visible
24
+ # until a rebuild.
25
+ # CONTENT_REVALIDATE_SECRET=
26
+
18
27
  # =============================================================================
19
28
  # apps/api base URL (browser-side lead-capture forms) — #3965
20
29
  # =============================================================================
@@ -46,9 +55,22 @@ CONTENT_BACKEND=mdx
46
55
  # =============================================================================
47
56
  # Better Auth (apps/web)
48
57
  # =============================================================================
49
- # Base URL Better Auth uses for OAuth callback construction.
58
+ # Base URL Better Auth uses for OAuth callback construction — and, since #5962,
59
+ # the ORIGIN the password-reset link is built on and the sole member of the
60
+ # trustedOrigins allow-list.
61
+ #
62
+ # REQUIRED for password reset, and not as a convenience: with this unset Better
63
+ # Auth recomputes both the base URL and the allow-list PER REQUEST from the
64
+ # request itself, so `Host: evil.tld` plus `redirectTo: https://evil.tld/x`
65
+ # passes its own origin check and the emailed reset link is built on the
66
+ # attacker's host. It is a conjunct of resetSendConfigured(), so a deployment
67
+ # that has not pinned its origin does not offer reset at all.
50
68
  # BETTER_AUTH_URL=http://localhost:3000
51
- # Secret used to sign session cookies. REQUIRED in production.
69
+ #
70
+ # Secret used to sign session cookies AND the email-verification tokens.
71
+ # REQUIRED in production: apps/web/auth.ts now THROWS at boot without it rather
72
+ # than falling back to a development string that is published in this
73
+ # repository. Also a conjunct of resetSendConfigured().
52
74
  # BETTER_AUTH_SECRET=
53
75
  #
54
76
  # OAuth provider env vars — set both id + secret to activate a provider.
@@ -57,6 +79,50 @@ CONTENT_BACKEND=mdx
57
79
  # GOOGLE_CLIENT_SECRET=
58
80
  # GITHUB_CLIENT_ID=
59
81
  # GITHUB_CLIENT_SECRET=
82
+ #
83
+ # Auth-surface flags (#5963) — what the (auth) pages OFFER, read through
84
+ # apps/web/lib/auth-env.ts. Both default OFF, which is what a scaffold is:
85
+ # auth.ts does not enable emailAndPassword, so there is no public sign-up and
86
+ # no password-reset flow to advertise. Set to "1" or "true" once the
87
+ # server-side flow is wired.
88
+ # Open public sign-up: /sign-up renders the form instead of the invitation copy.
89
+ # AILK_AUTH_SIGNUP_OPEN=
90
+ # Password reset available: /forgot-password and /reset-password render their
91
+ # forms instead of the not-available state.
92
+ # AILK_AUTH_PASSWORD_RESET=
93
+ #
94
+ # -----------------------------------------------------------------------------
95
+ # Password reset — the server-side flow (#5962)
96
+ # -----------------------------------------------------------------------------
97
+ # Shared secret for the signed hop to apps/api's /v1/auth/transactional-email,
98
+ # which is what actually sends auth mail: architecture.yaml forbids apps/web
99
+ # from importing packages/email, so apps/web signs a body and POSTs it. Must
100
+ # match AUTH_EMAIL_WEBHOOK_SECRET on the apps/api deployment. Deployment-only —
101
+ # never NEXT_PUBLIC_, and never committed.
102
+ #
103
+ # Reset is offered only when ALL FIVE of these hold — AILK_AUTH_PASSWORD_RESET,
104
+ # the password method (AUTH_EMAIL_SERVER + AUTH_EMAIL_FROM), this secret plus
105
+ # AUTH_EMAIL_FROM, BETTER_AUTH_URL, and BETTER_AUTH_SECRET. Fail closed: a site
106
+ # must never tell a visitor "check your email" for mail nothing can send.
107
+ # AUTH_EMAIL_WEBHOOK_SECRET=
108
+ #
109
+ # Comma-separated CIDRs of YOUR edge/proxy, for Better Auth's rate-limit IP key.
110
+ #
111
+ # Unset is the honest default — this repo cannot know your deployment's edge
112
+ # topology — but read both failure modes before leaving it that way:
113
+ #
114
+ # * Multi-hop X-Forwarded-For with this UNSET: Better Auth resolves the
115
+ # client IP to null and every caller collapses into ONE SHARED BUCKET for
116
+ # the whole deployment (keyed on the constant "no-trusted-ip"). Because a
117
+ # shared bucket turns a long window into a site-wide lockout anyone can
118
+ # hold open, apps/web does NOT apply its widened reset windows in this
119
+ # state — it falls through to Better Auth's own defaults for those paths
120
+ # (60 s / 3) so an outage recovers by itself. Set this variable to get the
121
+ # stricter per-caller limits (5 per 900 s on the reset request).
122
+ # * Single-hop X-Forwarded-For with this UNSET: the client's own header is
123
+ # trusted, so an attacker mints a fresh rate-limit bucket per request.
124
+ #
125
+ # AILK_AUTH_TRUSTED_PROXIES=
60
126
 
61
127
  # =============================================================================
62
128
  # Observability
@@ -66,6 +66,50 @@ Don't ship a page after one render. The expected per-page loop:
66
66
  - Fix mismatches. Be specific in self-review: "heading is 32px, reference shows 24px", not "looks off."
67
67
  - Repeat until no visible differences and no responsive issues at any of the four breakpoints.
68
68
 
69
+ ## The `(auth)` route group — which predicate an affordance keys on
70
+
71
+ `lib/auth-env.ts` exports two reset predicates, and they are not
72
+ interchangeable. Picking the wrong one is how this surface has broken twice
73
+ (#6018, then #6061 — the follow-up #6068 documents the rule here so there is
74
+ no third time).
75
+
76
+ | Predicate | Question it answers | Keys |
77
+ | --- | --- | --- |
78
+ | `isResetAvailable()` | *Does the operator INTEND to offer reset?* — the `AILK_AUTH_PASSWORD_RESET` flag, alone | nothing user-visible |
79
+ | `resetSendConfigured()` | *Can this deployment actually COMPLETE a reset?* — that flag AND the password method AND a send seam AND `BETTER_AUTH_URL` AND `BETTER_AUTH_SECRET` | every user-visible reset affordance |
80
+
81
+ **The rule: every user-visible reset affordance keys on
82
+ `resetSendConfigured()`.** That is the `/forgot-password` and `/reset-password`
83
+ page branches, the "Forgot password?" link on `/sign-in`, and anything added
84
+ later that offers a visitor a reset step. The flag alone is a real deployment
85
+ state — intent set, transport missing — and a flag-keyed affordance in that
86
+ state is a link to a page that renders its own not-available card, or a form
87
+ whose only possible outcome is the error state. Whether an affordance is a link
88
+ or a whole page makes no difference to the rule.
89
+
90
+ **Server callbacks are not affordances, and one is deliberately outside the
91
+ predicate.** `sendResetPassword` IS gated — a defined callback under a page
92
+ that denies the flow exists would be a live anonymous mail-sending surface.
93
+ `auth.ts`'s `onPasswordReset` is NOT, and must stay that way: Better Auth
94
+ registers the `/reset-password` POST on `emailAndPassword.enabled`, so a token
95
+ minted before an operator removed a conjunct stays consumable for its whole
96
+ hour, and gating the callback would make exactly those resets the ones that
97
+ skip sibling-token invalidation and the password-changed notice. Offering a
98
+ step is predicated; cleaning up after one that already happened is not.
99
+
100
+ **Anything `onPasswordReset` calls must not be able to reject it.** It runs
101
+ AFTER the password has changed and the presented token has been consumed, so a
102
+ rejection is not a failed reset — it is a 500 shown to someone whose password
103
+ DID change, over a token they cannot retry with. Both halves of that callback
104
+ are guarded at the call site, and `lib/auth-email.ts` guards its own signing
105
+ and transport steps (#6068).
106
+
107
+ **Mocking `lib/auth-env` in a test?** The suites under `(auth)` use PARTIAL
108
+ module mocks, so a member the factory omits is `undefined` rather than the real
109
+ function — the page under test throws instead of taking a branch. Declare
110
+ every predicate the pages call, binding the two reset ones to the same
111
+ `jest.fn()` when the case does not need them to disagree.
112
+
69
113
  ## Conversational intake (contact page)
70
114
 
71
115
  The contact page (`apps/web/app/[locale]/contact/`) consumes
@@ -16,7 +16,10 @@
16
16
  * default) instead of a hardcoded vendor-brand literal.
17
17
  */
18
18
 
19
- import { getMcpToolCatalog, type McpToolCatalogEntry } from "@working-theory/metadata";
19
+ import {
20
+ getMcpToolCatalog,
21
+ type McpToolCatalogEntry,
22
+ } from "@working-theory/metadata";
20
23
 
21
24
  import { SITE_NAME } from "../../../lib/site-brand";
22
25
 
@@ -74,6 +77,17 @@ export interface BuildMcpManifestOptions {
74
77
  * Typically sourced from the MCP_PUBLIC_HTTP_URL env var in the web app.
75
78
  */
76
79
  mcpHttpUrl?: string;
80
+ /**
81
+ * Whether this deployment holds the `commerce` add-on licence claim
82
+ * (#5971). Add-on-gated tools — today only `start_purchase` — are advertised
83
+ * only when it does, so the discovery manifest and the running MCP server
84
+ * agree about what this site offers.
85
+ *
86
+ * Injected rather than resolved here: this builder is PURE (same input →
87
+ * same output), a property its own suite asserts. `route.ts` resolves the
88
+ * claim and passes it in. Omitted ⇒ false, the fail-closed default.
89
+ */
90
+ commerceClaimed?: boolean;
77
91
  }
78
92
 
79
93
  /** Path the MCP server is mounted at, relative to baseUrl. */
@@ -105,7 +119,9 @@ export function buildMcpManifest(opts: BuildMcpManifestOptions): McpManifest {
105
119
  description:
106
120
  "AX-first OSS template — ship software with API + MCP as canonical agent surfaces.",
107
121
  mcpEndpoint: `${opts.baseUrl}${MCP_ENDPOINT_PATH}`,
108
- tools: getMcpToolCatalog(),
122
+ tools: getMcpToolCatalog({
123
+ addons: { commerce: opts.commerceClaimed === true },
124
+ }),
109
125
  transport: {
110
126
  type: "http",
111
127
  version: MCP_HTTP_VERSION,
@@ -5,8 +5,14 @@
5
5
  * URL, and discover the MCP endpoint + tool catalog without out-of-band
6
6
  * coordination. Issue #209 (initial), #300 (moved to correct URL + CORS).
7
7
  *
8
- * Pure assembly — no fetch(). The catalog is read in-process from
9
- * @working-theory/metadata; the AILK version is read from this app's package.json.
8
+ * Assembly — no fetch(). The catalog is read in-process from
9
+ * @working-theory/metadata; the AILK version is read from this app's
10
+ * package.json; the `commerce` add-on claim is read locally through
11
+ * `checkLicense()` (#5971) so an add-on-gated tool is advertised on exactly
12
+ * the deployments whose MCP server registers it. Without that, a KEYED site
13
+ * would register `start_purchase` while hiding it from its own manifest —
14
+ * the surface disagreement `apps/mcp/__tests__/catalog-drift.test.ts` exists
15
+ * to make impossible.
10
16
  *
11
17
  * Mirrors /.well-known/ai-plugin.json conventions: Response.json with a
12
18
  * conservative cache-control header.
@@ -17,6 +23,7 @@
17
23
  */
18
24
 
19
25
  import packageJson from "../../../package.json";
26
+ import { resolveCommerceClaim } from "../../../src/lib/commerce-claim";
20
27
  import { resolveBaseUrl } from "../../../src/lib/routes";
21
28
 
22
29
  import { buildMcpManifest } from "./build-manifest";
@@ -34,7 +41,7 @@ export function OPTIONS(): Response {
34
41
  });
35
42
  }
36
43
 
37
- export function GET(): Response {
44
+ export async function GET(): Promise<Response> {
38
45
  // MCP_PUBLIC_HTTP_URL advertises the standalone HTTP transport endpoint
39
46
  // (apps/mcp running in HTTP mode). When set, the manifest includes it
40
47
  // as an additional transport descriptor for discovering agents.
@@ -43,6 +50,7 @@ export function GET(): Response {
43
50
  const manifest = buildMcpManifest({
44
51
  baseUrl: resolveBaseUrl(),
45
52
  ailkVersion: packageJson.version,
53
+ commerceClaimed: await resolveCommerceClaim(),
46
54
  // Spread conditional to satisfy exactOptionalPropertyTypes: omit the key
47
55
  // entirely when the env var is not set rather than passing undefined.
48
56
  ...(mcpHttpUrlEnv ? { mcpHttpUrl: mcpHttpUrlEnv } : {}),