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
@@ -11,13 +11,20 @@
11
11
  */
12
12
  import cors from "@fastify/cors";
13
13
  import rateLimit from "@fastify/rate-limit";
14
+ import { idRegex } from "@working-theory/ids";
14
15
  import Fastify from "fastify";
15
- import type { FastifyPluginAsync, FastifyPluginCallback } from "fastify";
16
+ import type {
17
+ FastifyPluginAsync,
18
+ FastifyPluginCallback,
19
+ FastifyRequest,
20
+ } from "fastify";
16
21
 
17
22
  import { config } from "./config/index.js";
18
23
  import { isModuleEnabled } from "./config/modules.js";
19
24
  import { registerErrorSerializer } from "./lib/error-serializer.js";
20
25
  import { validateLeadCaptureDir } from "./lib/lead-capture-store.js";
26
+ import { requireCommerceClaim } from "./lib/require-commerce-claim.js";
27
+ import { assertSiteAliasResolves } from "./lib/site.js";
21
28
  import { loadSlice } from "./lib/slice-load.js";
22
29
  import { authPreHandlerPlugin } from "./middleware/auth.js";
23
30
  import { defaultTenantPreHandlerPlugin } from "./middleware/default-tenant.js";
@@ -37,25 +44,87 @@ import {
37
44
  import type { RegisteredRoute } from "./openapi/surface-drift.js";
38
45
  import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
39
46
  import { tokenRoutes } from "./routes/auth/tokens.js";
47
+ import { transactionalEmailRoute } from "./routes/auth/transactional-email.js";
40
48
  import { userRoutes } from "./routes/auth/user.js";
41
49
  import { getPageRoute } from "./routes/content/get-page.js";
42
50
  import { listPagesRoute } from "./routes/content/list-pages.js";
43
51
  import { listTypesRoute } from "./routes/content/list-types.js";
44
52
  import {
53
+ CONTENT_RATE_LIMIT_AUTH,
45
54
  CONTENT_RATE_LIMIT_UNAUTH,
46
55
  CONTENT_RATE_LIMIT_WINDOW,
47
56
  } from "./routes/content/rate-limit.js";
57
+ import { searchContentRoute } from "./routes/content/search.js";
48
58
  import {
49
59
  leadCallbackRoute,
50
60
  leadSubmitRoute,
51
61
  leadSubscribeRoute,
52
62
  } from "./routes/leads/index.js";
53
63
  import { leadListRoute } from "./routes/leads/list.js";
64
+ import { requestRoutes } from "./routes/requests/index.js";
54
65
  import {
55
66
  registerTestAuthRoute,
56
67
  shouldMountTestAuth,
57
68
  } from "./routes/test/auth/session.js";
58
69
 
70
+ /**
71
+ * What a request looks like in a log line — Fastify's own default shape, minus
72
+ * the QUERY STRING (#5970 security review).
73
+ *
74
+ * Exported so the property can be asserted directly rather than inferred from
75
+ * a captured line, because the thing it protects is easy to regress silently:
76
+ * pino's stock `req` serializer logs `req.raw.url` verbatim, and a route may
77
+ * carry its credential there. `GET /v1/requests/:id/confirm?token=…` does,
78
+ * since a link in an email has nowhere else to put one, and that token is live
79
+ * for seven days and mints a chargeable Stripe artifact.
80
+ *
81
+ * Nothing that identifies a request is lost: a route parameter such as `:id`
82
+ * lives in the PATH and survives. Only the secret after `?` is dropped.
83
+ *
84
+ * #6084 D14 — `GET /v1/checkout/sessions/:id` puts its credential in the
85
+ * PATH, not the query string, so the strip above does not cover it: every
86
+ * request would otherwise write a live `cs_…` Checkout Session id into the
87
+ * log — contradicting D3 and `sessions.ts`'s own stated invariant that only
88
+ * the first 8 characters of a session id ever reach a log line. A path
89
+ * SEGMENT rule (never a substring replace) truncates any `cs_…`-shaped
90
+ * segment to its first 8 characters plus `…`, so it cannot corrupt an
91
+ * unrelated URL.
92
+ *
93
+ * #6216 D11/§ 9 A-3 — the SAME rule now carries a second arm for
94
+ * `lead_…`-shaped segments. Fastify's automatic "incoming request" line is
95
+ * emitted by its internal hook BEFORE any route hook can intervene, so
96
+ * every `GET`/`PATCH .../workspaces/:workspaceId/messages/:leadId` would
97
+ * otherwise write the lead id into the log beside `remoteAddress` —
98
+ * completing exactly the `leadId` ↔ `siteId`/`ip`/`user-agent` correlation
99
+ * `routes/leads/index.ts` and this file's own PII posture refuse. No
100
+ * route-level discipline can fix it; this is the only place that can.
101
+ */
102
+ const SESSION_ID_LOG_SEGMENT = /^cs_[A-Za-z0-9_]+$/;
103
+ const LEAD_ID_LOG_SEGMENT = idRegex("lead");
104
+
105
+ function redactSensitiveIdSegments(url: string): string {
106
+ return url
107
+ .split("/")
108
+ .map((segment) =>
109
+ SESSION_ID_LOG_SEGMENT.test(segment) || LEAD_ID_LOG_SEGMENT.test(segment)
110
+ ? `${segment.slice(0, 8)}…`
111
+ : segment,
112
+ )
113
+ .join("/");
114
+ }
115
+
116
+ export function requestLogSerializer(request: FastifyRequest) {
117
+ const raw = request.url;
118
+ const queryAt = raw.indexOf("?");
119
+ const path = queryAt === -1 ? raw : raw.slice(0, queryAt);
120
+ return {
121
+ method: request.method,
122
+ url: redactSensitiveIdSegments(path),
123
+ host: request.hostname,
124
+ remoteAddress: request.ip,
125
+ };
126
+ }
127
+
59
128
  /**
60
129
  * Paid-module SLICE entry points, keyed by the export each branch needs
61
130
  * (#5182).
@@ -134,11 +203,21 @@ const WORKSPACES_SESSIONS = "./routes/workspaces/sessions.js";
134
203
  const WORKSPACES_CONFIG = "./routes/workspaces/config.js";
135
204
  const WORKSPACES_GENERAL = "./routes/workspaces/general.js";
136
205
  const WORKSPACES_MEMBERS = "./routes/workspaces/members.js";
206
+ // #6216 — a SIBLING specifier registered in the SAME workspace/multi-tenant
207
+ // scope as WORKSPACES_GENERAL/WORKSPACES_MEMBERS below, not the
208
+ // multi-tenant-only family WORKSPACES_MEETINGS/WORKSPACES_CONFIG sit in.
209
+ const WORKSPACES_MESSAGES = "./routes/workspaces/messages.js";
210
+ // #6183 S9b — a SEPARATE specifier from WORKSPACES_CONFIG: this route is
211
+ // registered under isModuleEnabled("scheduling") alone, never the
212
+ // workspace/multi-tenant scope config.js shares with general.js/members.js
213
+ // (see config.ts's own header for why).
214
+ const WORKSPACES_MEETINGS = "./routes/workspaces/meetings.js";
215
+ const WORKSPACES_CONTENT = "./routes/workspaces/content.js";
216
+ const ASSETS_UPLOAD = "./routes/assets/upload.js";
217
+ const ASSETS_READ = "./routes/assets/read.js";
218
+ const ASSETS_MANAGE = "./routes/assets/manage.js";
137
219
  const BILLING_INDEX = "./routes/billing/index.js";
138
220
  const WAITLIST_EXPERIMENTS_INDEX = "./routes/waitlist-experiments/index.js";
139
- const CONTENT_CREATE = "./routes/content/create.js";
140
- const CONTENT_UPDATE = "./routes/content/update.js";
141
- const CONTENT_DELETE = "./routes/content/delete.js";
142
221
  const SCHEDULE_INDEX = "./routes/schedule/index.js";
143
222
  const FLOWS_INDEX = "./routes/flows/index.js";
144
223
  const DELIVERABLES_INDEX = "./routes/deliverables/index.js";
@@ -148,7 +227,9 @@ const WAITLIST_SIGNUPS_INDEX = "./routes/waitlist-signups/index.js";
148
227
  const CHECKOUT_INDEX = "./routes/checkout/index.js";
149
228
  const WEBHOOKS_INDEX = "./routes/webhooks/index.js";
150
229
  const FLOW_CHECKOUTS_INDEX = "./routes/flow-checkouts/index.js";
230
+ const SUBSCRIPTION_INDEX = "./routes/subscription/index.js";
151
231
  const RECOMMENDER_INDEX = "./routes/flows/recommender.js";
232
+ const MAGNETS_INDEX = "./routes/magnets/index.js";
152
233
 
153
234
  /**
154
235
  * Paid-module SLICE entry points, keyed by the export each branch needs.
@@ -183,6 +264,33 @@ const SLICE_PLUGINS = {
183
264
  specifier: WORKSPACES_MEMBERS,
184
265
  load: () => import(WORKSPACES_MEMBERS),
185
266
  },
267
+ // #6216 — the Inbox message API, the SAME scope as the two above.
268
+ workspaceMessageRoutes: {
269
+ specifier: WORKSPACES_MESSAGES,
270
+ load: () => import(WORKSPACES_MESSAGES),
271
+ },
272
+ // #6183 S9b — registered in the scheduling scope, not the
273
+ // workspace/multi-tenant scope its siblings above share.
274
+ workspaceMeetingsRoutes: {
275
+ specifier: WORKSPACES_MEETINGS,
276
+ load: () => import(WORKSPACES_MEETINGS),
277
+ },
278
+ workspaceContentRoutes: {
279
+ specifier: WORKSPACES_CONTENT,
280
+ load: () => import(WORKSPACES_CONTENT),
281
+ },
282
+ workspaceAssetUploadRoute: {
283
+ specifier: ASSETS_UPLOAD,
284
+ load: () => import(ASSETS_UPLOAD),
285
+ },
286
+ publicAssetReadRoute: {
287
+ specifier: ASSETS_READ,
288
+ load: () => import(ASSETS_READ),
289
+ },
290
+ workspaceAssetManageRoutes: {
291
+ specifier: ASSETS_MANAGE,
292
+ load: () => import(ASSETS_MANAGE),
293
+ },
186
294
  billingRoutes: {
187
295
  specifier: BILLING_INDEX,
188
296
  load: () => import(BILLING_INDEX),
@@ -191,18 +299,6 @@ const SLICE_PLUGINS = {
191
299
  specifier: WAITLIST_EXPERIMENTS_INDEX,
192
300
  load: () => import(WAITLIST_EXPERIMENTS_INDEX),
193
301
  },
194
- createPageRoute: {
195
- specifier: CONTENT_CREATE,
196
- load: () => import(CONTENT_CREATE),
197
- },
198
- updatePageRoute: {
199
- specifier: CONTENT_UPDATE,
200
- load: () => import(CONTENT_UPDATE),
201
- },
202
- deletePageRoute: {
203
- specifier: CONTENT_DELETE,
204
- load: () => import(CONTENT_DELETE),
205
- },
206
302
  listAvailabilityRoute: {
207
303
  specifier: SCHEDULE_INDEX,
208
304
  load: () => import(SCHEDULE_INDEX),
@@ -215,6 +311,20 @@ const SLICE_PLUGINS = {
215
311
  specifier: SCHEDULE_INDEX,
216
312
  load: () => import(SCHEDULE_INDEX),
217
313
  },
314
+ // #6183 S9b D2/D12 — the enabled-meeting-slugs public read; same
315
+ // specifier as the three anonymous schedule routes above (§ D6's
316
+ // precedent, no new SLICE_PLUGINS specifier constant needed).
317
+ meetingTypesRoute: {
318
+ specifier: SCHEDULE_INDEX,
319
+ load: () => import(SCHEDULE_INDEX),
320
+ },
321
+ // #6085 — the tenant-guarded appointments read; same specifier as the
322
+ // three anonymous schedule routes above (§ D6, no new SLICE_PLUGINS
323
+ // specifier constant needed).
324
+ listAppointmentsRoute: {
325
+ specifier: SCHEDULE_INDEX,
326
+ load: () => import(SCHEDULE_INDEX),
327
+ },
218
328
  getFlowRoute: {
219
329
  specifier: FLOWS_INDEX,
220
330
  load: () => import(FLOWS_INDEX),
@@ -227,6 +337,12 @@ const SLICE_PLUGINS = {
227
337
  specifier: FLOWS_INDEX,
228
338
  load: () => import(FLOWS_INDEX),
229
339
  },
340
+ // #6157 — the tenant-scoped magnet Admin authoring surface. ONE plugin,
341
+ // ONE specifier (D2): all four routes register through this one entry.
342
+ magnetRoutes: {
343
+ specifier: MAGNETS_INDEX,
344
+ load: () => import(MAGNETS_INDEX),
345
+ },
230
346
  deliverableDownloadRoute: {
231
347
  specifier: DELIVERABLES_INDEX,
232
348
  load: () => import(DELIVERABLES_INDEX),
@@ -265,9 +381,13 @@ const SLICE_PLUGINS = {
265
381
  specifier: WAITLIST_SIGNUPS_INDEX,
266
382
  load: () => import(WAITLIST_SIGNUPS_INDEX),
267
383
  },
268
- // Commerce (D-47, #5364) — checkout, the Stripe webhook receiver, flow
269
- // checkouts and the recommender. Module-conditional (isModuleEnabled
270
- // "commerce") like every other paid slice above.
384
+ // Commerce (D-47, #5364), re-cut by D-64 (#5796). `checkoutRoutes` and
385
+ // `webhooksRoutes` are the base-tree PRIMITIVES now: they register
386
+ // UNCONDITIONALLY and gate at call time on the `commerce` license claim
387
+ // (lib/require-commerce-claim.ts), so they are no longer slice-gated even
388
+ // though they still load through this table. `flowCheckoutRoutes` and the
389
+ // two recommender entries below stay module-conditional (isModuleEnabled
390
+ // "commerce") — they are commerce OVERLAY, not primitives.
271
391
  checkoutRoutes: {
272
392
  specifier: CHECKOUT_INDEX,
273
393
  load: () => import(CHECKOUT_INDEX),
@@ -280,6 +400,13 @@ const SLICE_PLUGINS = {
280
400
  specifier: FLOW_CHECKOUTS_INDEX,
281
401
  load: () => import(FLOW_CHECKOUTS_INDEX),
282
402
  },
403
+ // #6038 — the buyer-scoped subscription-status route pair (the
404
+ // update-your-card banner's read + portal handoff). Commerce OVERLAY,
405
+ // same posture as flowCheckoutRoutes immediately above.
406
+ subscriptionRoutes: {
407
+ specifier: SUBSCRIPTION_INDEX,
408
+ load: () => import(SUBSCRIPTION_INDEX),
409
+ },
283
410
  saveRecommendationRoute: {
284
411
  specifier: RECOMMENDER_INDEX,
285
412
  load: () => import(RECOMMENDER_INDEX),
@@ -386,12 +513,22 @@ export async function build() {
386
513
  // Startup-time validation: fail fast if LEAD_CAPTURE_FILE points at a missing directory.
387
514
  await validateLeadCaptureDir();
388
515
 
516
+ // #6155 D6 — the site-alias workspace assertion (ownership brief § Layer
517
+ // 2, AC-5): resolves + asserts once at startup that AILK_SITE_ID (or its
518
+ // "default" fallback) names a real workspace, throwing when it does not —
519
+ // the same catch-log-exit(1) path index.ts already runs on this throws
520
+ // loud rather than serving empty lists. Carve-out: with no DATABASE_URL
521
+ // configured, it logs a warning and returns instead of throwing (a
522
+ // database-less dev boot).
523
+ await assertSiteAliasResolves();
524
+
389
525
  // Runtime sandbox-mode guard: reject live-mode Stripe keys before any
390
526
  // webhook or checkout call can exercise them. Idempotent — safe even if
391
- // assertSandboxKey is called again inside a route handler. Module-
392
- // conditional (commerce, D-47/D3): a build without commerce has no
393
- // commerce request path to guard, and lib/stripe.ts is stripped with it.
394
- if (isModuleEnabled("commerce") && config.STRIPE_SECRET_KEY) {
527
+ // assertSandboxKey is called again inside a route handler. UNCONDITIONAL
528
+ // since D-64 (#5796): the commerce primitives and lib/stripe.ts are base
529
+ // tree now, so the key's presence — not a module flag — is the whole
530
+ // question. A build with no STRIPE_SECRET_KEY has nothing to assert.
531
+ if (config.STRIPE_SECRET_KEY) {
395
532
  await assertCommerceSandboxKey(config.STRIPE_SECRET_KEY);
396
533
  }
397
534
 
@@ -418,7 +555,27 @@ export async function build() {
418
555
  // (server-action-forwarded traffic), which was previously broken for
419
556
  // every caller.
420
557
  const server = Fastify({
421
- logger: true,
558
+ logger: {
559
+ // The request serializer drops the QUERY STRING (#5970 security review,
560
+ // finding 1). Fastify's default `req` serializer logs `req.raw.url`
561
+ // verbatim, so any route whose credential rides in the query string
562
+ // writes that credential into the API's own log on every single request
563
+ // — including the automatic "incoming request" line, which is emitted by
564
+ // Fastify's internal hook before any route-level hook can intervene.
565
+ //
566
+ // `GET /v1/requests/:id/confirm?token=…` is exactly such a route: the
567
+ // token IS the authorization, it is live for seven days, and holding it
568
+ // is enough to read a requester's name and phone and to mint a Payment
569
+ // Link on the site's own Stripe account. `Referrer-Policy: no-referrer`
570
+ // on that route exists to keep the token out of a THIRD PARTY's logs;
571
+ // this keeps it out of ours, which is the larger hole of the two.
572
+ //
573
+ // The shape is otherwise Fastify's own default, minus the query: a log
574
+ // consumer reading `method` + `url` + `remoteAddress` sees what it
575
+ // always saw. Nothing that identifies a REQUEST is lost — `:id` is in
576
+ // the path and survives; only the secret after `?` is dropped.
577
+ serializers: { req: requestLogSerializer },
578
+ },
422
579
  trustProxy: true,
423
580
  });
424
581
 
@@ -581,6 +738,17 @@ export async function build() {
581
738
  await authScope.register(await sliceRoutePlugin("flowCheckoutRoutes"));
582
739
  }
583
740
 
741
+ // The update-your-card banner's read + portal handoff (#6038) — the
742
+ // same posture as flow-checkouts immediately above: auth only, NOT
743
+ // tenant-guarded (a subscriber has no workspace membership). Identity is
744
+ // request.userId; GET is cookie-or-bearer, POST refuses bearer callers
745
+ // (D3.1). Module-conditional (commerce, D8): the account page's
746
+ // subscription-status read already degrades to "no banner" with no code
747
+ // change when this is absent (SKIPPED_STATUSES covers 404 — spec §4.2).
748
+ if (isModuleEnabled("commerce")) {
749
+ await authScope.register(await sliceRoutePlugin("subscriptionRoutes"));
750
+ }
751
+
584
752
  // The founder's OWN listings read (#5295) — the same posture again: auth
585
753
  // only, NOT tenant-guarded, because a founder is not a workspace member
586
754
  // and the tenant guard is default-deny. Identity is request.userId; the
@@ -657,6 +825,51 @@ export async function build() {
657
825
  });
658
826
  }
659
827
 
828
+ // The magnet Admin authoring surface (#6157 S5c) — its OWN sibling
829
+ // tenant scope (D3), not a line inside the review-surface scope above:
830
+ // its module-exclusion row stays its own, and the scope can be read,
831
+ // moved or gated without disturbing `waitlistExperimentRoutes` /
832
+ // `projectListingTenantRoutes`. Gated `multi-tenant || signal-collectors`
833
+ // (D4), VERBATIM the review-surface expression above — not narrowed to
834
+ // `(multi-tenant || signal-collectors) && <tenancy module>`, which would
835
+ // put the magnet surface out of reach of `marketing-site` (the Pro
836
+ // flagship template, which takes `signal-collectors` WITHOUT
837
+ // `multi-tenant`). The delivery is brought up to the gate instead
838
+ // (`packages/create-ailk/src/module-architecture.ts` D14/D15/D16), the
839
+ // same correction #5677 already made once for this exact scope.
840
+ if (
841
+ isModuleEnabled("multi-tenant") ||
842
+ isModuleEnabled("signal-collectors")
843
+ ) {
844
+ await authScope.register(async (tenantScope) => {
845
+ await tenantScope.register(await tenantPreHandlerForBuild());
846
+ await tenantScope.register(await sliceRoutePlugin("magnetRoutes"));
847
+ });
848
+ }
849
+
850
+ // The appointments read (#6085, brief H7) — a sibling tenant scope,
851
+ // gated on `scheduling` alone (§ D6). NOT the anonymous schedule scope
852
+ // below (`isModuleEnabled("scheduling")`'s OTHER site, § 1.9/§ D6): a
853
+ // prospect booking a call has no account, so that scope carries no
854
+ // auth preHandler, and mounting this read there would publish every
855
+ // attendee's name and email to the internet. `tenantPreHandlerForBuild()`
856
+ // is the SAME default-deny guard the review-surface scope above uses —
857
+ // on a build with no `multi-tenant` module it fails closed unless
858
+ // `AILK_OPERATOR_EMAIL` is pinned (`middleware/default-tenant.ts`).
859
+ //
860
+ // The meetings config route (#6183 S9b) shares this exact scope and
861
+ // gate, for the same reason: its GET reaches
862
+ // apps/api/src/services/scheduling/ (D24's listEventTypes join), which
863
+ // is undeliverable to a scaffold without this module — see
864
+ // routes/workspaces/meetings.ts's own header.
865
+ if (isModuleEnabled("scheduling")) {
866
+ await authScope.register(async (tenantScope) => {
867
+ await tenantScope.register(await tenantPreHandlerForBuild());
868
+ await tenantScope.register(await sliceRoutePlugin("listAppointmentsRoute"));
869
+ await tenantScope.register(await sliceRoutePlugin("workspaceMeetingsRoutes"));
870
+ });
871
+ }
872
+
660
873
  // Workspace SETTINGS routes — a sibling tenant scope, gated on EITHER
661
874
  // module (#5371 D2). The site-config routes were registered inside the
662
875
  // multi-tenant scope above, which was too narrow: nothing in their logic
@@ -702,28 +915,157 @@ export async function build() {
702
915
  await tenantScope.register(
703
916
  await sliceRoutePlugin("workspaceMembersRoutes"),
704
917
  );
918
+ // The Inbox message API (#6216 S7e) — the SAME scope, the SAME
919
+ // default-deny guard: list, item read, and the handled stamp.
920
+ await tenantScope.register(
921
+ await sliceRoutePlugin("workspaceMessageRoutes"),
922
+ );
923
+ // The asset store's MANAGE routes — list, PATCH, DELETE (#6093 D16).
924
+ // Registered HERE, in the plain tenant scope, and NOT inside the
925
+ // upload's own nested strict-rate-limit scope below: a JSON
926
+ // list/patch/delete has no 20 MB body to bound, and inheriting that
927
+ // per-IP ceiling would throttle the Admin's own list against its
928
+ // siblings.
929
+ await tenantScope.register(
930
+ await sliceRoutePlugin("workspaceAssetManageRoutes"),
931
+ );
932
+ // The workspace asset store's upload half (#5854). It belongs in THIS
933
+ // scope and not beside the public routes below, because the two
934
+ // refusals its AC names — no granted tenant, and a `:workspaceId` that
935
+ // is not the session's — are `tenantPreHandlerForBuild()`'s to make,
936
+ // before any handler runs and so before any write. Its raw-body parser
937
+ // and its `onRequest` size/type hook are declared INSIDE that plugin,
938
+ // so they encapsulate to it and reach neither the public read below nor
939
+ // any sibling here.
940
+ //
941
+ // Its own rate-limit scope, at the strict per-IP ceiling the leads and
942
+ // scheduling scopes use (#5854 security review round 1, MEDIUM). Auth
943
+ // and tenant are `preHandler`s, so an UNAUTHENTICATED caller still
944
+ // reaches the parser and can make the server buffer up to 20 MB before
945
+ // the 401. `rateLimitMiddleware` is an `onRequest` hook, so scoping it
946
+ // here is what bounds how often that can be done — the cap bounds one
947
+ // body, this bounds the rate. Nested so the ceiling applies to the
948
+ // upload alone and not to the config/general/members siblings above.
949
+ await tenantScope.register(async (assetUploadScope) => {
950
+ assetUploadScope.register(rateLimitMiddleware, {
951
+ perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
952
+ });
953
+ await assetUploadScope.register(
954
+ await sliceRoutePlugin("workspaceAssetUploadRoute"),
955
+ );
956
+ });
957
+ });
958
+ }
959
+
960
+ // The editor's tenant-guarded content-write surface (#5853, roadmap D4).
961
+ // Its OWN sibling scope (D9) — not nested inside the workspace/multi-tenant
962
+ // scope above, so its module-exclusion row stays its own. The gate is
963
+ // `content-write` AND a tenancy module: the route file imports
964
+ // `lib/tenant-db.ts`, which only the `workspace` / `multi-tenant` slices
965
+ // deliver, so a scaffold enabling `content-write` without either would
966
+ // throw ERR_MODULE_NOT_FOUND at build() — the exact defect
967
+ // `tools/lint-slice-registration.ts` (required in CI) refuses. D9's
968
+ // "content-write alone" premise was falsified by that lint on PR #5921;
969
+ // the path itself is workspace-scoped (`/v1/workspaces/:workspaceId/…`),
970
+ // so requiring a tenancy module is coherent, not a narrowing.
971
+ // `tenantPreHandlerForBuild()` still selects the base-build
972
+ // default-workspace guard on a `workspace`-only build. The plugin is
973
+ // registered here and nowhere else, so no scope can collide on a path.
974
+ //
975
+ // The module flags are read ONCE, here, at build time (D10): on a build
976
+ // that excludes them the routes are never registered and Fastify's own 404
977
+ // answers — no auth, no tenant resolution, no handler-level
978
+ // `isModuleEnabled`.
979
+ if (
980
+ isModuleEnabled("content-write") &&
981
+ (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant"))
982
+ ) {
983
+ await authScope.register(async (tenantScope) => {
984
+ // D12: the authenticated ceiling (600/min), scope-local. The global
985
+ // limiter above is the unauth 60/min, which an autosaving editor would
986
+ // trip against its own user.
987
+ await tenantScope.register(rateLimit, {
988
+ max: CONTENT_RATE_LIMIT_AUTH,
989
+ timeWindow: CONTENT_RATE_LIMIT_WINDOW,
990
+ });
991
+ await tenantScope.register(await tenantPreHandlerForBuild());
992
+ await tenantScope.register(
993
+ await sliceRoutePlugin("workspaceContentRoutes"),
994
+ );
705
995
  });
706
996
  }
707
997
  });
708
998
 
709
999
  // Public routes — no auth required.
1000
+ //
1001
+ // The workspace asset store's READ half (#5854), under the SAME predicate as
1002
+ // its upload above rather than unconditionally: with no upload path in the
1003
+ // build it would be a permanent 404 advertised on both self-description
1004
+ // surfaces. It cannot join the block above — that block is nested in
1005
+ // `authScope`, and an `<img src>` carries no session.
1006
+ if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
1007
+ await server.register(await sliceRoutePlugin("publicAssetReadRoute"));
1008
+ }
710
1009
  await server.register(listTypesRoute);
1010
+ // Before `listPagesRoute`: `search` must never be read as a `:type`.
1011
+ await server.register(searchContentRoute);
711
1012
  await server.register(listPagesRoute);
712
1013
  await server.register(getPageRoute);
713
- // Content-write routes (create/update/delete) — already Pro-tier gated at
714
- // CALL time via requirePaidTier (apps/api/src/lib/require-paid-tier.ts);
715
- // module-conditional at REGISTRATION too (S1, #5088) so an OSS build that
716
- // excludes the content-write slice never wires the handler in at all. Not
717
- // a MODULE_SLICES entry — see config/modules.ts.
718
- if (isModuleEnabled("content-write")) {
719
- await server.register(await sliceRoutePlugin("createPageRoute"));
720
- await server.register(await sliceRoutePlugin("updatePageRoute"));
721
- await server.register(await sliceRoutePlugin("deletePageRoute"));
722
- }
723
- // Module-conditional (commerce, D-47): checkout moved to its own module.
724
- if (isModuleEnabled("commerce")) {
725
- await server.register(await sliceRoutePlugin("checkoutRoutes"));
726
- }
1014
+ // Checkout launcher + reader — commerce PRIMITIVES (D-64, #5796; the GET
1015
+ // added by #6084). Registered UNCONDITIONALLY and gated at CALL time on
1016
+ // the JWT `commerce` claim, the same posture content-write has
1017
+ // (requirePaidTier above), so adding commerce to a live site is a licence
1018
+ // change and lands no files on its repo. Without the claim the gate
1019
+ // answers the module-disabled 404 — byte-identical to the build that
1020
+ // never registered the route. `checkoutRoutes` (`routes/checkout/index.ts`)
1021
+ // registers BOTH `POST /v1/checkout/sessions` and
1022
+ // `GET /v1/checkout/sessions/:id` — the GET in its own nested,
1023
+ // rate-limited sub-scope (D16), so this preHandler applies to both while
1024
+ // the GET's own limiter applies to it alone.
1025
+ await server.register(async (checkoutScope) => {
1026
+ checkoutScope.addHook("preHandler", requireCommerceClaim);
1027
+ await checkoutScope.register(await sliceRoutePlugin("checkoutRoutes"));
1028
+ });
1029
+ // Request-and-confirm (#5970, D-67 / SUB-100) — the tenant's confirm form
1030
+ // and the Payment Link it mints. A money path, so it sits INSIDE the
1031
+ // commerce claim (DECISION-LOG D-63; D-67 assigns request-and-confirm to
1032
+ // the Commerce rung rather than exempting it), and the gate is applied by
1033
+ // COMPOSITION here — the checkout scope above, line for line. That is what
1034
+ // lets the route files themselves read no licence claim at all (#5970 AC-4).
1035
+ //
1036
+ // Rate-limited at the leads scope's own strict value: the confirm link is
1037
+ // unauthenticated (the token IS the credential), so it belongs on the same
1038
+ // footing as the anonymous surface that mails it. Note the same caveat
1039
+ // `server.ts` records elsewhere — `trustProxy` is on, so an IP limiter is
1040
+ // evadable; the bound that actually holds is per ROW, one live credential
1041
+ // per request, spent on use.
1042
+ await server.register(async (requestsScope) => {
1043
+ requestsScope.register(rateLimitMiddleware, {
1044
+ perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
1045
+ });
1046
+ // `requireCommerceClaim` short-circuits with `reply.send`, so a wrapper is
1047
+ // the only place the unclaimed case can be OBSERVED: the preHandler ends
1048
+ // the request before any handler runs, and the gate itself logs only on
1049
+ // `undetermined` (`lib/require-commerce-claim.ts`).
1050
+ //
1051
+ // Spec D3 names this line as one of two bounds on the residual it
1052
+ // discloses — a site configured for request-and-confirm but carrying no
1053
+ // `commerce` claim emails a tenant a confirm link that answers 404. The
1054
+ // spec puts the log in the route, where it cannot be emitted; this is the
1055
+ // registration, which is where it can (#5970 acceptance review, advisory
1056
+ // 4). Without it the misconfiguration is visible only in the tenant's
1057
+ // inbox, which is the one place the operator is not looking.
1058
+ requestsScope.addHook("preHandler", async (request, reply) => {
1059
+ await requireCommerceClaim(request, reply);
1060
+ if (reply.sent) {
1061
+ request.log.warn(
1062
+ { outcome: "confirm_link_unclaimed", path: request.routeOptions.url },
1063
+ "request-confirm: a confirm link was opened on a site without the commerce claim",
1064
+ );
1065
+ }
1066
+ });
1067
+ await requestsScope.register(requestRoutes);
1068
+ });
727
1069
  // Deliverable download — a capability-token URL (#3961 D5); the token
728
1070
  // itself is the sole gate, so no auth preHandler applies. Module-conditional
729
1071
  // (signal-collectors, D-48): gated PDFs and reports.
@@ -751,12 +1093,15 @@ export async function build() {
751
1093
  done(null, body);
752
1094
  },
753
1095
  );
754
- // Stripe webhook receiver — module-conditional (commerce, D-47). The
755
- // scope wrapper and both raw-body parsers above stay unconditional: the
756
- // Slack parser below needs them too (D2).
757
- if (isModuleEnabled("commerce")) {
758
- await webhooksScope.register(await sliceRoutePlugin("webhooksRoutes"));
759
- }
1096
+ // Stripe webhook receiver + customer mapping — commerce PRIMITIVES
1097
+ // (D-64, #5796), registered UNCONDITIONALLY alongside checkout above.
1098
+ // NO `requireCommerceClaim` preHandler here, deliberately: D3 puts
1099
+ // signature verification BEFORE the claim check, so the handler itself
1100
+ // consults `hasCommerceClaim()` once the signature is settled — a signed
1101
+ // event on an unclaimed site is acked 200 and dropped rather than
1102
+ // retried forever, while an unsigned body gets the module-disabled 404.
1103
+ // See routes/webhooks/README.md § Commerce claim.
1104
+ await webhooksScope.register(await sliceRoutePlugin("webhooksRoutes"));
760
1105
  // Project-listing operator decision (Slack Approve/Reject) — the "hand an
761
1106
  // operator a decision" half of D-48's grouping sentence. Module-conditional
762
1107
  // (signal-collectors).
@@ -765,6 +1110,35 @@ export async function build() {
765
1110
  }
766
1111
  });
767
1112
 
1113
+ // Auth transactional-email relay (#5962 § D3) — its OWN scope, deliberately
1114
+ // OUTSIDE `authScope`: the caller is `apps/web`'s Better Auth callbacks
1115
+ // acting for an anonymous visitor who is by definition not signed in, so an
1116
+ // auth preHandler in front of it would reject every legitimate call. The
1117
+ // gate is the HMAC signature over the raw body plus the 300 s replay window,
1118
+ // both verified inside the handler.
1119
+ //
1120
+ // Raw-body parser scoped to this sub-router only, for the same reason
1121
+ // `webhooksScope` carries one: the signature must be checked over the EXACT
1122
+ // bytes the caller signed, never a re-serialized parse.
1123
+ //
1124
+ // `DEFAULT_PER_IP_PER_MINUTE_STRICT` (10/min) — the constant `leadsScope`
1125
+ // below already uses for its anonymous surface. It is defence in depth, not
1126
+ // the control: an unsigned body never reaches a send, and per-victim volume
1127
+ // is bounded by `lib/auth-email-budget.ts`.
1128
+ await server.register(async (authEmailScope) => {
1129
+ authEmailScope.register(rateLimitMiddleware, {
1130
+ perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
1131
+ });
1132
+ authEmailScope.addContentTypeParser(
1133
+ "application/json",
1134
+ { parseAs: "buffer" },
1135
+ (_req, body, done) => {
1136
+ done(null, body);
1137
+ },
1138
+ );
1139
+ await authEmailScope.register(transactionalEmailRoute);
1140
+ });
1141
+
768
1142
  // Leads routes — rate-limit middleware scoped to this sub-router. Own
769
1143
  // default (10/min, pre-#4730) rather than inheriting the wider
770
1144
  // flows-sized default (#5111, DEF-004 — #4730's shared-default bump was
@@ -802,11 +1176,15 @@ export async function build() {
802
1176
  // every recipient — the residual the first one left, and the one that
803
1177
  // decides whether a flood can cost the site its sending domain.
804
1178
  if (isModuleEnabled("signal-collectors")) {
805
- await leadsScope.register(await sliceRoutePlugin("projectListingStartRoute"));
1179
+ await leadsScope.register(
1180
+ await sliceRoutePlugin("projectListingStartRoute"),
1181
+ );
806
1182
  await leadsScope.register(
807
1183
  await sliceRoutePlugin("projectListingDraftPatchRoute"),
808
1184
  );
809
- await leadsScope.register(await sliceRoutePlugin("projectListingSubmitRoute"));
1185
+ await leadsScope.register(
1186
+ await sliceRoutePlugin("projectListingSubmitRoute"),
1187
+ );
810
1188
  // #5407 — the resume link's verification write. Anonymous, and gated
811
1189
  // on the resume token AND the nonce that only the resume email
812
1190
  // carries, so it belongs in the same rate-limited scope as the other
@@ -814,7 +1192,9 @@ export async function build() {
814
1192
  await leadsScope.register(
815
1193
  await sliceRoutePlugin("projectListingVerifyEmailRoute"),
816
1194
  );
817
- await leadsScope.register(await sliceRoutePlugin("projectListingPublicRoute"));
1195
+ await leadsScope.register(
1196
+ await sliceRoutePlugin("projectListingPublicRoute"),
1197
+ );
818
1198
  }
819
1199
  });
820
1200
 
@@ -832,13 +1212,17 @@ export async function build() {
832
1212
  if (isModuleEnabled("signal-collectors")) {
833
1213
  await flowsScope.register(await sliceRoutePlugin("getFlowRoute"));
834
1214
  await flowsScope.register(await sliceRoutePlugin("submitFlowStepRoute"));
835
- await flowsScope.register(await sliceRoutePlugin("getFlowAggregateRoute"));
1215
+ await flowsScope.register(
1216
+ await sliceRoutePlugin("getFlowAggregateRoute"),
1217
+ );
836
1218
  }
837
1219
  // The recommender's email-moment routes (#4841) — same anonymous,
838
1220
  // rate-limited posture as the three above. Module-conditional (commerce,
839
1221
  // D-47): the recommender leaves Pro flows and moves to commerce whole.
840
1222
  if (isModuleEnabled("commerce")) {
841
- await flowsScope.register(await sliceRoutePlugin("saveRecommendationRoute"));
1223
+ await flowsScope.register(
1224
+ await sliceRoutePlugin("saveRecommendationRoute"),
1225
+ );
842
1226
  await flowsScope.register(
843
1227
  await sliceRoutePlugin("checkoutRecommendationRoute"),
844
1228
  );
@@ -879,6 +1263,12 @@ export async function build() {
879
1263
  await scheduleScope.register(
880
1264
  await sliceRoutePlugin("createBookingRoute"),
881
1265
  );
1266
+ // #6183 S9b D2/D12 — the enabled-meeting-slugs public read. Same scope
1267
+ // as the three routes above: it inherits the module gate and the
1268
+ // strict per-IP rate limit (§9 SEC-4).
1269
+ await scheduleScope.register(
1270
+ await sliceRoutePlugin("meetingTypesRoute"),
1271
+ );
882
1272
  });
883
1273
  }
884
1274