create-ailk 0.4.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 (396) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +12248 -0
  3. package/component-catalog.md +193 -68
  4. package/dist/cli.js +29 -1
  5. package/dist/derive-architecture.js +5 -2
  6. package/dist/lib/api-boot-assertion.d.ts +46 -0
  7. package/dist/lib/api-boot-assertion.js +70 -0
  8. package/dist/lib/extract-module-bundle.d.ts +50 -2
  9. package/dist/lib/extract-module-bundle.js +223 -15
  10. package/dist/lib/fetch-guard.d.ts +28 -0
  11. package/dist/lib/fetch-guard.js +101 -0
  12. package/dist/lib/fetch-module.js +4 -0
  13. package/dist/lib/fetch-template.js +4 -0
  14. package/dist/lib/import-graph.d.ts +29 -0
  15. package/dist/lib/import-graph.js +140 -0
  16. package/dist/module-architecture.d.ts +60 -8
  17. package/dist/module-architecture.js +587 -242
  18. package/dist/parse-args.js +6 -2
  19. package/dist/programmatic.d.ts +10 -0
  20. package/dist/programmatic.js +134 -19
  21. package/dist/site-chrome.d.ts +80 -0
  22. package/dist/site-chrome.js +135 -0
  23. package/dist/surfaces.d.ts +21 -6
  24. package/dist/surfaces.js +209 -36
  25. package/dist/template-manifest.d.ts +88 -3
  26. package/dist/template-manifest.js +281 -29
  27. package/dist/template-names.d.ts +1 -1
  28. package/dist/template-names.js +2 -0
  29. package/package.json +10 -2
  30. package/site-templates/_base/ailk-template.json +6 -0
  31. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
  34. package/site-templates/marketing-site/ailk-template.json +11 -0
  35. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  36. package/site-templates/marketing-site/provenance.json +4 -0
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
  39. package/site-templates/marketing-site-free/ailk-template.json +2 -1
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
  41. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
  42. package/site-templates/minimalism-site/ailk-template.json +22 -0
  43. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  44. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  49. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  50. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  51. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  53. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  54. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  55. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  56. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  57. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
  58. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  60. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  65. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  68. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  69. package/site-templates/minimalism-site/provenance.json +4 -0
  70. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  71. package/site-templates/saas-product/ailk-template.json +16 -0
  72. package/site-templates/saas-product/provenance.json +10 -0
  73. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  74. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  75. package/site-templates/simple-service/ailk-template.json +10 -0
  76. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  77. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  78. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  79. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  80. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  81. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  82. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  83. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  84. package/site-templates/simple-service/provenance.json +4 -0
  85. package/templates/.claude/skills/README.md +16 -25
  86. package/templates/.env.example +102 -12
  87. package/templates/.github/workflows/ci.yml +48 -0
  88. package/templates/README.md +3 -1
  89. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  90. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  91. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  92. package/templates/apps/api/.env.example +13 -0
  93. package/templates/apps/api/CLAUDE.md +29 -2
  94. package/templates/apps/api/eslint.config.mjs +69 -0
  95. package/templates/apps/api/jest.config.cjs +3 -0
  96. package/templates/apps/api/package.json +7 -2
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  98. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
  99. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  100. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  101. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  102. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
  103. package/templates/apps/api/src/config/index.ts +62 -0
  104. package/templates/apps/api/src/config/modules.ts +7 -3
  105. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
  106. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  107. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  108. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  109. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  110. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  111. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  112. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  113. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  114. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  115. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  116. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  117. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  118. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  119. package/templates/apps/api/src/lib/site.ts +208 -9
  120. package/templates/apps/api/src/lib/stripe.ts +66 -0
  121. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
  122. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
  123. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  124. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  125. package/templates/apps/api/src/openapi/spec.ts +543 -2
  126. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  127. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  128. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  129. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  130. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  131. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  132. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  133. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  134. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  135. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  136. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  137. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  138. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  139. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  140. package/templates/apps/api/src/routes/content/index.ts +13 -73
  141. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  142. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  143. package/templates/apps/api/src/routes/content/search.ts +129 -0
  144. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  145. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  146. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  147. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  148. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  149. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  150. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  151. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  152. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  153. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  154. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  155. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  156. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  157. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  158. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  159. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  160. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  161. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  162. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  163. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  164. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  165. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  166. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  167. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  168. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  169. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  170. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  171. package/templates/apps/api/src/server.ts +521 -55
  172. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  173. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  174. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  175. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  176. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  177. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  178. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  179. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  180. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  181. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  182. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  183. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  184. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  185. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  186. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  187. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  188. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  189. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  190. package/templates/apps/api/src/services/audit-log.ts +668 -0
  191. package/templates/apps/api/src/services/consent.ts +18 -0
  192. package/templates/apps/api/src/services/default-tenant.ts +300 -0
  193. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  194. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  195. package/templates/apps/api/src/services/entitlements.ts +80 -0
  196. package/templates/apps/api/src/services/lead-promotion.ts +160 -0
  197. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  198. package/templates/apps/api/src/services/leads-inbox.ts +284 -0
  199. package/templates/apps/api/src/services/listing-config.ts +27 -9
  200. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  201. package/templates/apps/api/src/services/project-listings.ts +201 -6
  202. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  203. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  204. package/templates/apps/api/src/services/revalidation.ts +173 -0
  205. package/templates/apps/api/src/services/verified-email.ts +71 -0
  206. package/templates/apps/api/src/vercel-handler.ts +75 -35
  207. package/templates/apps/mcp/.env.example +17 -0
  208. package/templates/apps/mcp/CLAUDE.md +58 -25
  209. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  210. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  211. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  212. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  213. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  214. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  215. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  216. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  217. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  218. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  219. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  220. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  221. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  222. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  223. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  224. package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
  225. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  226. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  227. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  228. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  229. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  230. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  231. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  232. package/templates/apps/mcp/eslint.config.mjs +69 -0
  233. package/templates/apps/mcp/jest.config.cjs +13 -0
  234. package/templates/apps/mcp/package.json +8 -2
  235. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  236. package/templates/apps/mcp/src/config/modules.ts +10 -5
  237. package/templates/apps/mcp/src/http-transport.ts +102 -29
  238. package/templates/apps/mcp/src/index.ts +73 -11
  239. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  240. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  241. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  242. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  243. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  244. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  245. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  246. package/templates/apps/mcp/src/server.ts +61 -37
  247. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  248. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  249. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  250. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  251. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  252. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  253. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  254. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  255. package/templates/apps/mcp/src/tools/index.ts +9 -7
  256. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  257. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  258. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  259. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  260. package/templates/apps/web/.env.example +74 -8
  261. package/templates/apps/web/CLAUDE.md +46 -0
  262. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  263. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  264. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  265. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  266. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  267. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  268. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  269. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  270. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  271. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  272. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  273. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  274. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  275. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  276. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  277. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  278. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  279. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  280. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  281. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
  282. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
  283. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  284. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  285. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  286. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  287. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  288. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  289. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  290. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  291. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  292. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  293. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  294. package/templates/apps/web/app/[locale]/layout.tsx +308 -65
  295. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  296. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  297. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  298. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  299. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  300. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  301. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  302. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  303. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  304. package/templates/apps/web/auth.ts +407 -8
  305. package/templates/apps/web/components/google-analytics.tsx +18 -1
  306. package/templates/apps/web/eslint.config.mjs +69 -0
  307. package/templates/apps/web/jest.config.cjs +15 -19
  308. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  309. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  310. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  311. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
  312. package/templates/apps/web/lib/auth-email.ts +318 -0
  313. package/templates/apps/web/lib/auth-env.ts +85 -0
  314. package/templates/apps/web/lib/auth-routes.ts +55 -0
  315. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  316. package/templates/apps/web/lib/nav-config.ts +27 -7
  317. package/templates/apps/web/lib/server-api.ts +1 -1
  318. package/templates/apps/web/lib/site-chrome.ts +29 -0
  319. package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
  320. package/templates/apps/web/messages/en.json +216 -2
  321. package/templates/apps/web/next.config.mjs +20 -0
  322. package/templates/apps/web/package.json +8 -3
  323. package/templates/apps/web/sentry.client.config.ts +131 -0
  324. package/templates/apps/web/sentry.edge.config.ts +72 -0
  325. package/templates/apps/web/sentry.server.config.ts +72 -0
  326. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  327. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  328. package/templates/apps/web/src/lib/routes.ts +13 -18
  329. package/templates/brand_assets/README.md +24 -0
  330. package/templates/content/en/AboutPage/about.mdx +160 -15
  331. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  332. package/templates/content/en/ContactPage/contact.mdx +90 -13
  333. package/templates/content/en/FAQPage/faq.mdx +62 -22
  334. package/templates/content/en/HomePage/home.mdx +244 -20
  335. package/templates/content/en/LandingPage/blog.mdx +83 -0
  336. package/templates/content/en/LandingPage/features.mdx +103 -0
  337. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  338. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  339. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  340. package/templates/database/CHANGELOG.md +149 -0
  341. package/templates/database/README.md +10 -3
  342. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  343. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  344. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  345. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  346. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  347. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  348. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  349. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  350. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  351. package/templates/database/__tests__/seed.test.ts +1 -1
  352. package/templates/database/__tests__/tenancy.test.ts +141 -0
  353. package/templates/database/config/schema.prisma +79 -2
  354. package/templates/database/content/schema.prisma +87 -11
  355. package/templates/database/eslint.config.mjs +69 -0
  356. package/templates/database/inbox/schema.prisma +452 -9
  357. package/templates/database/jest.config.cjs +5 -3
  358. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  359. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  360. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  361. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  362. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  363. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  364. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  365. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  366. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  367. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  368. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  369. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  370. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  371. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  372. package/templates/database/package.json +7 -3
  373. package/templates/database/seed.ts +1 -1
  374. package/templates/database/tenancy.ts +86 -1
  375. package/templates/database/tsconfig.json +1 -0
  376. package/templates/package.json +1 -14
  377. package/versions.json +52 -0
  378. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
  379. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  380. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  381. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  382. package/templates/apps/api/src/routes/content/create.ts +0 -115
  383. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  384. package/templates/apps/api/src/routes/content/update.ts +0 -112
  385. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  386. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  387. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  388. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  389. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  390. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  391. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  392. /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
  393. /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
  394. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  395. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
  396. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -11,15 +11,23 @@
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";
30
+ import { defaultTenantPreHandlerPlugin } from "./middleware/default-tenant.js";
23
31
  import {
24
32
  rateLimitMiddleware,
25
33
  DEFAULT_PER_IP_PER_MINUTE_STRICT,
@@ -36,24 +44,87 @@ import {
36
44
  import type { RegisteredRoute } from "./openapi/surface-drift.js";
37
45
  import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
38
46
  import { tokenRoutes } from "./routes/auth/tokens.js";
47
+ import { transactionalEmailRoute } from "./routes/auth/transactional-email.js";
39
48
  import { userRoutes } from "./routes/auth/user.js";
40
49
  import { getPageRoute } from "./routes/content/get-page.js";
41
50
  import { listPagesRoute } from "./routes/content/list-pages.js";
42
51
  import { listTypesRoute } from "./routes/content/list-types.js";
43
52
  import {
53
+ CONTENT_RATE_LIMIT_AUTH,
44
54
  CONTENT_RATE_LIMIT_UNAUTH,
45
55
  CONTENT_RATE_LIMIT_WINDOW,
46
56
  } from "./routes/content/rate-limit.js";
57
+ import { searchContentRoute } from "./routes/content/search.js";
47
58
  import {
48
59
  leadCallbackRoute,
49
60
  leadSubmitRoute,
50
61
  leadSubscribeRoute,
51
62
  } from "./routes/leads/index.js";
63
+ import { leadListRoute } from "./routes/leads/list.js";
64
+ import { requestRoutes } from "./routes/requests/index.js";
52
65
  import {
53
66
  registerTestAuthRoute,
54
67
  shouldMountTestAuth,
55
68
  } from "./routes/test/auth/session.js";
56
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
+
57
128
  /**
58
129
  * Paid-module SLICE entry points, keyed by the export each branch needs
59
130
  * (#5182).
@@ -132,11 +203,21 @@ const WORKSPACES_SESSIONS = "./routes/workspaces/sessions.js";
132
203
  const WORKSPACES_CONFIG = "./routes/workspaces/config.js";
133
204
  const WORKSPACES_GENERAL = "./routes/workspaces/general.js";
134
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";
135
219
  const BILLING_INDEX = "./routes/billing/index.js";
136
220
  const WAITLIST_EXPERIMENTS_INDEX = "./routes/waitlist-experiments/index.js";
137
- const CONTENT_CREATE = "./routes/content/create.js";
138
- const CONTENT_UPDATE = "./routes/content/update.js";
139
- const CONTENT_DELETE = "./routes/content/delete.js";
140
221
  const SCHEDULE_INDEX = "./routes/schedule/index.js";
141
222
  const FLOWS_INDEX = "./routes/flows/index.js";
142
223
  const DELIVERABLES_INDEX = "./routes/deliverables/index.js";
@@ -146,7 +227,9 @@ const WAITLIST_SIGNUPS_INDEX = "./routes/waitlist-signups/index.js";
146
227
  const CHECKOUT_INDEX = "./routes/checkout/index.js";
147
228
  const WEBHOOKS_INDEX = "./routes/webhooks/index.js";
148
229
  const FLOW_CHECKOUTS_INDEX = "./routes/flow-checkouts/index.js";
230
+ const SUBSCRIPTION_INDEX = "./routes/subscription/index.js";
149
231
  const RECOMMENDER_INDEX = "./routes/flows/recommender.js";
232
+ const MAGNETS_INDEX = "./routes/magnets/index.js";
150
233
 
151
234
  /**
152
235
  * Paid-module SLICE entry points, keyed by the export each branch needs.
@@ -181,6 +264,33 @@ const SLICE_PLUGINS = {
181
264
  specifier: WORKSPACES_MEMBERS,
182
265
  load: () => import(WORKSPACES_MEMBERS),
183
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
+ },
184
294
  billingRoutes: {
185
295
  specifier: BILLING_INDEX,
186
296
  load: () => import(BILLING_INDEX),
@@ -189,18 +299,6 @@ const SLICE_PLUGINS = {
189
299
  specifier: WAITLIST_EXPERIMENTS_INDEX,
190
300
  load: () => import(WAITLIST_EXPERIMENTS_INDEX),
191
301
  },
192
- createPageRoute: {
193
- specifier: CONTENT_CREATE,
194
- load: () => import(CONTENT_CREATE),
195
- },
196
- updatePageRoute: {
197
- specifier: CONTENT_UPDATE,
198
- load: () => import(CONTENT_UPDATE),
199
- },
200
- deletePageRoute: {
201
- specifier: CONTENT_DELETE,
202
- load: () => import(CONTENT_DELETE),
203
- },
204
302
  listAvailabilityRoute: {
205
303
  specifier: SCHEDULE_INDEX,
206
304
  load: () => import(SCHEDULE_INDEX),
@@ -213,6 +311,20 @@ const SLICE_PLUGINS = {
213
311
  specifier: SCHEDULE_INDEX,
214
312
  load: () => import(SCHEDULE_INDEX),
215
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
+ },
216
328
  getFlowRoute: {
217
329
  specifier: FLOWS_INDEX,
218
330
  load: () => import(FLOWS_INDEX),
@@ -225,6 +337,12 @@ const SLICE_PLUGINS = {
225
337
  specifier: FLOWS_INDEX,
226
338
  load: () => import(FLOWS_INDEX),
227
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
+ },
228
346
  deliverableDownloadRoute: {
229
347
  specifier: DELIVERABLES_INDEX,
230
348
  load: () => import(DELIVERABLES_INDEX),
@@ -263,9 +381,13 @@ const SLICE_PLUGINS = {
263
381
  specifier: WAITLIST_SIGNUPS_INDEX,
264
382
  load: () => import(WAITLIST_SIGNUPS_INDEX),
265
383
  },
266
- // Commerce (D-47, #5364) — checkout, the Stripe webhook receiver, flow
267
- // checkouts and the recommender. Module-conditional (isModuleEnabled
268
- // "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.
269
391
  checkoutRoutes: {
270
392
  specifier: CHECKOUT_INDEX,
271
393
  load: () => import(CHECKOUT_INDEX),
@@ -278,6 +400,13 @@ const SLICE_PLUGINS = {
278
400
  specifier: FLOW_CHECKOUTS_INDEX,
279
401
  load: () => import(FLOW_CHECKOUTS_INDEX),
280
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
+ },
281
410
  saveRecommendationRoute: {
282
411
  specifier: RECOMMENDER_INDEX,
283
412
  load: () => import(RECOMMENDER_INDEX),
@@ -334,6 +463,27 @@ async function tenantPreHandlerSlicePlugin(): Promise<FastifyPluginCallback> {
334
463
  return plugin;
335
464
  }
336
465
 
466
+ /**
467
+ * The tenant guard THIS build runs (#5677). Exactly one of the two, never
468
+ * both: the multi-tenant module's `resolveActiveTenant` guard when the module
469
+ * is enabled, and the base-build default-workspace guard
470
+ * (`middleware/default-tenant.ts`) otherwise.
471
+ *
472
+ * Precedence, not parallelism. On a build WITH the module this returns the
473
+ * identical plugin every tenant scope registered before, so multi-tenant
474
+ * resolution and behavior are unchanged. On a build without it, the base guard
475
+ * resolves the seeded default Workspace (`database/tenancy.ts`) under the same
476
+ * default-deny posture — which is what makes the already-built review
477
+ * dashboard reachable at Pro instead of leaving its routes unregistered.
478
+ *
479
+ * The base guard is a STATIC import, not a slice load: it always ships.
480
+ */
481
+ async function tenantPreHandlerForBuild(): Promise<FastifyPluginCallback> {
482
+ return isModuleEnabled("multi-tenant")
483
+ ? await tenantPreHandlerSlicePlugin()
484
+ : defaultTenantPreHandlerPlugin;
485
+ }
486
+
337
487
  /**
338
488
  * The commerce boot-time sandbox guard — the one other slice entry that is
339
489
  * not a route plugin, mirroring `tenantPreHandlerSlicePlugin()` above (D3).
@@ -363,12 +513,22 @@ export async function build() {
363
513
  // Startup-time validation: fail fast if LEAD_CAPTURE_FILE points at a missing directory.
364
514
  await validateLeadCaptureDir();
365
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
+
366
525
  // Runtime sandbox-mode guard: reject live-mode Stripe keys before any
367
526
  // webhook or checkout call can exercise them. Idempotent — safe even if
368
- // assertSandboxKey is called again inside a route handler. Module-
369
- // conditional (commerce, D-47/D3): a build without commerce has no
370
- // commerce request path to guard, and lib/stripe.ts is stripped with it.
371
- 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) {
372
532
  await assertCommerceSandboxKey(config.STRIPE_SECRET_KEY);
373
533
  }
374
534
 
@@ -395,7 +555,27 @@ export async function build() {
395
555
  // (server-action-forwarded traffic), which was previously broken for
396
556
  // every caller.
397
557
  const server = Fastify({
398
- 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
+ },
399
579
  trustProxy: true,
400
580
  });
401
581
 
@@ -527,6 +707,18 @@ export async function build() {
527
707
  );
528
708
  }
529
709
 
710
+ // The leads INBOX read (#5678) — auth only, NOT tenant-guarded, for the
711
+ // same reason as the three reads below: the tenant scope is registered
712
+ // only when the multi-tenant module is present, and lead capture is a
713
+ // never-gate surface at EVERY level, so reading a lead back cannot be
714
+ // gated on a module either. Registered unconditionally — it is the one
715
+ // route in this scope with no `isModuleEnabled()` gate. Identity is
716
+ // `request.userId`; the scope is resolved from the caller's own
717
+ // memberships by `services/leads-inbox.ts` through
718
+ // `services/owner-workspaces.ts`'s `resolveOwnerWorkspaces`, never from a
719
+ // request parameter.
720
+ await authScope.register(leadListRoute);
721
+
530
722
  // Respondent signup read (#4731) — auth only, NOT tenant-guarded: a
531
723
  // respondent has no workspace membership and the tenant guard is
532
724
  // default-deny. Identity is request.userId; the service re-checks
@@ -546,6 +738,17 @@ export async function build() {
546
738
  await authScope.register(await sliceRoutePlugin("flowCheckoutRoutes"));
547
739
  }
548
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
+
549
752
  // The founder's OWN listings read (#5295) — the same posture again: auth
550
753
  // only, NOT tenant-guarded, because a founder is not a workspace member
551
754
  // and the tenant guard is default-deny. Identity is request.userId; the
@@ -580,6 +783,35 @@ export async function build() {
580
783
  await tenantScope.register(await sliceRoutePlugin("sessionRoutes"));
581
784
  // Org-billing routes (#3701) — org-scoped read + Stripe customer-portal.
582
785
  await tenantScope.register(await sliceRoutePlugin("billingRoutes"));
786
+ });
787
+ }
788
+
789
+ // REVIEW-SURFACE routes — a sibling tenant scope, gated on EITHER module
790
+ // (#5677). The waitlist operator dashboard and the project-listings
791
+ // dashboard were registered inside the multi-tenant scope above, which was
792
+ // too narrow: `signal-collectors` (Pro) is the module that SELLS the
793
+ // instruments these routes read back — flows, deliverables, project
794
+ // listings, waitlist experiments — so a Pro customer could collect
795
+ // submissions and not read them. That was an accident of registration
796
+ // scope, not a pricing decision (#5677). Widening the gate is the
797
+ // correction.
798
+ //
799
+ // This is a REGISTRATION change, not a tenant-isolation change. The scope
800
+ // registers `tenantPreHandlerForBuild()`, which on a multi-tenant build is
801
+ // byte-for-byte the guard these two route plugins already ran behind, and
802
+ // on a build without the module is the base-build guard with the same
803
+ // default-deny posture over the seeded default Workspace. Each plugin
804
+ // registers exactly once — they live here and nowhere else, so the scopes
805
+ // cannot collide on a path — and every OTHER route in the scope above
806
+ // (workspaceSessionRoutes, sessionRoutes, billingRoutes) stays
807
+ // multi-tenant-only: those are Organization/Workspace administration, not
808
+ // a site's own submissions.
809
+ if (
810
+ isModuleEnabled("multi-tenant") ||
811
+ isModuleEnabled("signal-collectors")
812
+ ) {
813
+ await authScope.register(async (tenantScope) => {
814
+ await tenantScope.register(await tenantPreHandlerForBuild());
583
815
  // Waitlist operator-dashboard read API (#4665 D7, B0) — tenant-scoped
584
816
  // list/comparison/signups reads over A1's waitlist-experiment rows.
585
817
  await tenantScope.register(
@@ -593,6 +825,51 @@ export async function build() {
593
825
  });
594
826
  }
595
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
+
596
873
  // Workspace SETTINGS routes — a sibling tenant scope, gated on EITHER
597
874
  // module (#5371 D2). The site-config routes were registered inside the
598
875
  // multi-tenant scope above, which was too narrow: nothing in their logic
@@ -603,18 +880,30 @@ export async function build() {
603
880
  // original mis-scoping twice.
604
881
  //
605
882
  // This is a REGISTRATION change, not a tenant-isolation change: the scope
606
- // registers its own `tenantPreHandlerSlicePlugin()`, the same default-deny
607
- // guard the scope above uses, with the same `resolveActiveTenant` behind
608
- // it. Every other route in that scope (workspaceSessionRoutes,
609
- // sessionRoutes, billingRoutes, waitlistExperimentRoutes, the
610
- // project-listings tenant routes) stays multi-tenant-only.
883
+ // registers its own `tenantPreHandlerForBuild()`, the same default-deny
884
+ // guard the scopes above use. The remaining routes in the multi-tenant
885
+ // scope above (workspaceSessionRoutes, sessionRoutes, billingRoutes) stay
886
+ // multi-tenant-only.
887
+ //
888
+ // #5677's selector fixes ONE of this scope's two stripped-scaffold
889
+ // problems and not the other, so do not read it as making a
890
+ // `--modules workspace` scaffold work. It removes the guard's dependency
891
+ // on `./middleware/tenant.js` (a multi-tenant slice file, which threw
892
+ // ERR_MODULE_NOT_FOUND during construction). The three route plugins
893
+ // below are still reached through slice loads, and
894
+ // `routes/workspaces/config.ts` remains a multi-tenant `ownedFiles` entry
895
+ // — `general.ts` and `members.ts` are workspace-owned, `config.ts` is not
896
+ // — so that scaffold still has no `routes/workspaces/` directory and
897
+ // still throws here. Fixing it is the same one-line class of change #5677
898
+ // made for the review surface: co-own `config.ts` under `workspace` in
899
+ // MODULE_SLICES. Out of scope for #5677, which owns the review surface.
611
900
  //
612
901
  // A build with BOTH modules enabled registers each of the three route
613
902
  // plugins exactly once: they live here and nowhere else, so the two
614
903
  // scopes cannot collide on a path.
615
904
  if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
616
905
  await authScope.register(async (tenantScope) => {
617
- await tenantScope.register(await tenantPreHandlerSlicePlugin());
906
+ await tenantScope.register(await tenantPreHandlerForBuild());
618
907
  await tenantScope.register(
619
908
  await sliceRoutePlugin("workspaceConfigRoutes"),
620
909
  );
@@ -626,28 +915,157 @@ export async function build() {
626
915
  await tenantScope.register(
627
916
  await sliceRoutePlugin("workspaceMembersRoutes"),
628
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
+ );
629
995
  });
630
996
  }
631
997
  });
632
998
 
633
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
+ }
634
1009
  await server.register(listTypesRoute);
1010
+ // Before `listPagesRoute`: `search` must never be read as a `:type`.
1011
+ await server.register(searchContentRoute);
635
1012
  await server.register(listPagesRoute);
636
1013
  await server.register(getPageRoute);
637
- // Content-write routes (create/update/delete) — already Pro-tier gated at
638
- // CALL time via requirePaidTier (apps/api/src/lib/require-paid-tier.ts);
639
- // module-conditional at REGISTRATION too (S1, #5088) so an OSS build that
640
- // excludes the content-write slice never wires the handler in at all. Not
641
- // a MODULE_SLICES entry — see config/modules.ts.
642
- if (isModuleEnabled("content-write")) {
643
- await server.register(await sliceRoutePlugin("createPageRoute"));
644
- await server.register(await sliceRoutePlugin("updatePageRoute"));
645
- await server.register(await sliceRoutePlugin("deletePageRoute"));
646
- }
647
- // Module-conditional (commerce, D-47): checkout moved to its own module.
648
- if (isModuleEnabled("commerce")) {
649
- await server.register(await sliceRoutePlugin("checkoutRoutes"));
650
- }
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
+ });
651
1069
  // Deliverable download — a capability-token URL (#3961 D5); the token
652
1070
  // itself is the sole gate, so no auth preHandler applies. Module-conditional
653
1071
  // (signal-collectors, D-48): gated PDFs and reports.
@@ -675,12 +1093,15 @@ export async function build() {
675
1093
  done(null, body);
676
1094
  },
677
1095
  );
678
- // Stripe webhook receiver — module-conditional (commerce, D-47). The
679
- // scope wrapper and both raw-body parsers above stay unconditional: the
680
- // Slack parser below needs them too (D2).
681
- if (isModuleEnabled("commerce")) {
682
- await webhooksScope.register(await sliceRoutePlugin("webhooksRoutes"));
683
- }
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"));
684
1105
  // Project-listing operator decision (Slack Approve/Reject) — the "hand an
685
1106
  // operator a decision" half of D-48's grouping sentence. Module-conditional
686
1107
  // (signal-collectors).
@@ -689,6 +1110,35 @@ export async function build() {
689
1110
  }
690
1111
  });
691
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
+
692
1142
  // Leads routes — rate-limit middleware scoped to this sub-router. Own
693
1143
  // default (10/min, pre-#4730) rather than inheriting the wider
694
1144
  // flows-sized default (#5111, DEF-004 — #4730's shared-default bump was
@@ -726,11 +1176,15 @@ export async function build() {
726
1176
  // every recipient — the residual the first one left, and the one that
727
1177
  // decides whether a flood can cost the site its sending domain.
728
1178
  if (isModuleEnabled("signal-collectors")) {
729
- await leadsScope.register(await sliceRoutePlugin("projectListingStartRoute"));
1179
+ await leadsScope.register(
1180
+ await sliceRoutePlugin("projectListingStartRoute"),
1181
+ );
730
1182
  await leadsScope.register(
731
1183
  await sliceRoutePlugin("projectListingDraftPatchRoute"),
732
1184
  );
733
- await leadsScope.register(await sliceRoutePlugin("projectListingSubmitRoute"));
1185
+ await leadsScope.register(
1186
+ await sliceRoutePlugin("projectListingSubmitRoute"),
1187
+ );
734
1188
  // #5407 — the resume link's verification write. Anonymous, and gated
735
1189
  // on the resume token AND the nonce that only the resume email
736
1190
  // carries, so it belongs in the same rate-limited scope as the other
@@ -738,7 +1192,9 @@ export async function build() {
738
1192
  await leadsScope.register(
739
1193
  await sliceRoutePlugin("projectListingVerifyEmailRoute"),
740
1194
  );
741
- await leadsScope.register(await sliceRoutePlugin("projectListingPublicRoute"));
1195
+ await leadsScope.register(
1196
+ await sliceRoutePlugin("projectListingPublicRoute"),
1197
+ );
742
1198
  }
743
1199
  });
744
1200
 
@@ -756,13 +1212,17 @@ export async function build() {
756
1212
  if (isModuleEnabled("signal-collectors")) {
757
1213
  await flowsScope.register(await sliceRoutePlugin("getFlowRoute"));
758
1214
  await flowsScope.register(await sliceRoutePlugin("submitFlowStepRoute"));
759
- await flowsScope.register(await sliceRoutePlugin("getFlowAggregateRoute"));
1215
+ await flowsScope.register(
1216
+ await sliceRoutePlugin("getFlowAggregateRoute"),
1217
+ );
760
1218
  }
761
1219
  // The recommender's email-moment routes (#4841) — same anonymous,
762
1220
  // rate-limited posture as the three above. Module-conditional (commerce,
763
1221
  // D-47): the recommender leaves Pro flows and moves to commerce whole.
764
1222
  if (isModuleEnabled("commerce")) {
765
- await flowsScope.register(await sliceRoutePlugin("saveRecommendationRoute"));
1223
+ await flowsScope.register(
1224
+ await sliceRoutePlugin("saveRecommendationRoute"),
1225
+ );
766
1226
  await flowsScope.register(
767
1227
  await sliceRoutePlugin("checkoutRecommendationRoute"),
768
1228
  );
@@ -803,6 +1263,12 @@ export async function build() {
803
1263
  await scheduleScope.register(
804
1264
  await sliceRoutePlugin("createBookingRoute"),
805
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
+ );
806
1272
  });
807
1273
  }
808
1274