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
@@ -12,11 +12,19 @@
12
12
  import Script from "next/script";
13
13
  import type { ReactElement } from "react";
14
14
 
15
+ import { TOKEN_QUERY_PARAMS } from "../lib/telemetry-scrub";
16
+
15
17
  export function GoogleAnalytics(): ReactElement | null {
16
18
  const tagId = process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
17
19
  if (!tagId) {
18
20
  return null;
19
21
  }
22
+ // The init script below runs in the browser as raw inline JS, outside any
23
+ // module graph, so it can't `import` telemetry-scrub.ts — it gets its own
24
+ // copy of scrubTelemetryUrl, driven by the shared param list, so the
25
+ // report-grant token (?g=<id>, #5562) never reaches GA's page_location
26
+ // default (document.location.href) (#5613).
27
+ const scrubParamsJson = JSON.stringify(TOKEN_QUERY_PARAMS);
20
28
  return (
21
29
  <>
22
30
  <Script
@@ -27,7 +35,16 @@ export function GoogleAnalytics(): ReactElement | null {
27
35
  {`window.dataLayer = window.dataLayer || [];
28
36
  function gtag(){dataLayer.push(arguments);}
29
37
  gtag('js', new Date());
30
- gtag('config', '${tagId}');`}
38
+ function scrubTelemetryUrl(url) {
39
+ try {
40
+ var parsed = new URL(url);
41
+ ${scrubParamsJson}.forEach(function (param) { parsed.searchParams.delete(param); });
42
+ return parsed.toString();
43
+ } catch (e) {
44
+ return url;
45
+ }
46
+ }
47
+ gtag('config', '${tagId}', { page_location: scrubTelemetryUrl(window.location.href) });`}
31
48
  </Script>
32
49
  </>
33
50
  );
@@ -0,0 +1,69 @@
1
+ // Self-contained ESLint flat config for this scaffolded package.
2
+ //
3
+ // It deliberately does NOT depend on @working-theory/eslint-config — that
4
+ // package is internal to the AI Launch Kit monorepo and is not published, so
5
+ // it cannot be a dependency of a scaffolded starter. Everything below comes
6
+ // from public npm packages already pinned as devDependencies (see
7
+ // package.json): @eslint/js, globals and typescript-eslint.
8
+ //
9
+ // Source file name (#5556): this file is checked in as `_eslint.config.mjs`,
10
+ // not `eslint.config.mjs` — build-templates.mjs renames it at snapshot time
11
+ // (renameScaffoldLintConfigs()). A file literally named `eslint.config.mjs`
12
+ // anywhere under this monorepo (even one this repo's own root eslint.config.ts
13
+ // lists in its `ignores`) is auto-discovered by ESLint's flat-config nested
14
+ // resolution as a competing config root, which breaks the monorepo's own
15
+ // lint runs (verified live: typescript-eslint's "multiple candidate
16
+ // TSConfigRootDirs" parser error across unrelated packages/create-ailk
17
+ // files). The underscore prefix keeps it invisible to that discovery while
18
+ // it lives in this checkout; only the snapshot in templates/ — which ships
19
+ // to the customer, not this repo — gets the real filename.
20
+ import js from "@eslint/js";
21
+ import globals from "globals";
22
+ import tseslint from "typescript-eslint";
23
+
24
+ export default tseslint.config(
25
+ {
26
+ ignores: [
27
+ "**/.next/**",
28
+ "**/dist/**",
29
+ "**/generated/**",
30
+ "**/node_modules/**",
31
+ ],
32
+ },
33
+ js.configs.recommended,
34
+ ...tseslint.configs.recommended,
35
+ {
36
+ languageOptions: {
37
+ globals: {
38
+ ...globals.node,
39
+ ...globals.browser,
40
+ ...globals.jest,
41
+ },
42
+ // Explicit, self-contained root — this scaffold config is never meant
43
+ // to combine with any sibling scaffold package's config (each ships
44
+ // standalone), and leaving it unset makes typescript-eslint's
45
+ // candidate-root disambiguation ambiguous the moment more than one
46
+ // scaffold package's config resolves in the same ESLint process (e.g.
47
+ // this monorepo's own `templates/` build, where all four exist
48
+ // side by side; a real customer scaffold never has that problem,
49
+ // since it only ever ships and lints ONE of these four at a time).
50
+ parserOptions: {
51
+ tsconfigRootDir: import.meta.dirname,
52
+ },
53
+ },
54
+ rules: {
55
+ // Downgraded rather than off, so both stay visible without failing a
56
+ // fresh scaffold's lint:
57
+ // - unused-vars: most of the source follows the underscore-prefix
58
+ // convention for intentionally-unused bindings, which this rule's
59
+ // default options don't recognize; a handful of test fixtures also
60
+ // carry genuinely-unused locals this minimal config doesn't try to
61
+ // triage file-by-file.
62
+ // - require-imports: CommonJS require() is the correct form in Jest
63
+ // config/mock files and in a few test-only dynamic re-require
64
+ // patterns.
65
+ "@typescript-eslint/no-unused-vars": "warn",
66
+ "@typescript-eslint/no-require-imports": "warn",
67
+ },
68
+ },
69
+ );
@@ -58,9 +58,15 @@ module.exports = {
58
58
  "<rootDir>/node_modules/@working-theory/schema-validator/dist/index.js",
59
59
  "^@working-theory/sections-registry$":
60
60
  "<rootDir>/node_modules/@working-theory/sections-registry/dist/index.js",
61
-
62
61
  "^@working-theory/templates/shells$":
63
62
  "<rootDir>/node_modules/@working-theory/templates/dist/shells/index.js",
63
+ // Pure-data routes-map subpath (#5572 U2) — mapped directly to source so
64
+ // a consumer of the lightweight `./routes` export (routes.ts,
65
+ // next.config.mjs, this suite) never pulls in the templates package's
66
+ // shells (React/lucide-react) chain, mirroring the same entry in the
67
+ // root and create-ailk jest configs.
68
+ "^@working-theory/templates/routes$":
69
+ "<rootDir>/node_modules/@working-theory/templates/dist/routes-map.js",
64
70
  "^@working-theory/templates$": "<rootDir>/node_modules/@working-theory/templates/dist/index.js",
65
71
  "^@working-theory/ui$": "<rootDir>/node_modules/@working-theory/ui/dist/index.js",
66
72
  "^@working-theory/ui/blocks$": "<rootDir>/node_modules/@working-theory/ui/dist/blocks/index.js",
@@ -69,20 +75,6 @@ module.exports = {
69
75
  "<rootDir>/node_modules/@working-theory/ui/dist/primitives/index.js",
70
76
  "^@working-theory/ui/panels$": "<rootDir>/node_modules/@working-theory/ui/dist/panels/index.js",
71
77
  "^@working-theory/ui/sections$": "<rootDir>/node_modules/@working-theory/ui/dist/sections/index.js",
72
- // #5214: the scheduling slice's renderer-registration subpath. Mapped to
73
- // source like every sibling above — packages/ui/dist isn't built in this
74
- // workspace pre-`pnpm build`. The target strips with the slice on the public
75
- // tree, where nothing imports it, so the unused mapping is inert there.
76
- "^@working-theory/ui/slices/scheduling$":
77
- "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/scheduling.js",
78
- // #5366: the signal-collectors slice's re-export subpath. Same reasoning
79
- // as the scheduling mapping above.
80
- "^@working-theory/ui/slices/signal-collectors$":
81
- "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/signal-collectors.js",
82
- // #5364: the commerce slice's renderer-registration + CheckoutPriceCard
83
- // re-export subpath. Same reasoning as the scheduling mapping above.
84
- "^@working-theory/ui/slices/commerce$":
85
- "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/commerce.js",
86
78
  "^@working-theory/theme$": "<rootDir>/node_modules/@working-theory/theme/dist/index.js",
87
79
  // site-config publishes ESM in dist/; map to source so ts-jest compiles it
88
80
  // through the same transformer as every sibling above. Only the
@@ -92,11 +84,15 @@ module.exports = {
92
84
  "<rootDir>/node_modules/@working-theory/site-config/dist/schema/index.js",
93
85
  "^@working-theory/validation$": "<rootDir>/node_modules/@working-theory/validation/dist/index.js",
94
86
  "^@working-theory/web-i18n$": "<rootDir>/node_modules/@working-theory/web-i18n/dist/index.js",
95
- // The dependency-free tenancy-constants subpath (database/tenancy.ts —
96
- // it imports only a type from @working-theory/ids). Mapped to source like
97
- // every sibling above; `database/dist` is not built in this workspace
98
- // pre-`pnpm build`.
87
+ // The dependency-free tenancy-constants subpath (database/tenancy.ts).
88
+ // Mapped to source like every sibling above; `database/dist` is not
89
+ // built in this workspace pre-`pnpm build`.
99
90
  "^@working-theory/database/tenancy$": "<rootDir>/node_modules/@working-theory/database/dist/tenancy.js",
91
+ // #5876: database/tenancy.ts imports isSentinel as a VALUE (not just the
92
+ // Id type) from @working-theory/ids, so ts-jest needs it resolvable at
93
+ // runtime here too — mirrors the same mapping added to
94
+ // database/jest.config.cjs and apps/mcp/jest.config.cjs.
95
+ "^@working-theory/ids$": "<rootDir>/node_modules/@working-theory/ids/dist/index.js",
100
96
  // #5025: pnpm keys next-intl's virtual-store path on its resolved peers,
101
97
  // so apps/web and packages/page-renderer can link two PHYSICALLY DISTINCT
102
98
  // copies of the same version — and a `jest.mock("next-intl/server")` in
@@ -0,0 +1,214 @@
1
+ /**
2
+ * @file auth-email.test.ts — Better Auth's three mail callbacks (#5962 § D3).
3
+ *
4
+ * Written before the implementation. The transport is INJECTED, so nothing
5
+ * here mocks an internal module and nothing here sends mail.
6
+ *
7
+ * What the suite pins, in the order the design depends on it:
8
+ * - ONE serialization: the bytes that are signed are the bytes that are
9
+ * sent. A key-order or whitespace drift between signer and sender is the
10
+ * whole failure mode the raw-string seam exists to make impossible.
11
+ * - the signature is HMAC-SHA256 over `<timestamp>.<body>`, so the
12
+ * timestamp cannot be swapped independently of the body it authenticates.
13
+ * - the credential-account check (§ D14): a user with no `credential`
14
+ * account gets NO reset mail, and the suppression is silent — same return,
15
+ * no throw, log-only. An OAuth-only user who completes a reset would
16
+ * otherwise silently gain a password path around their IdP.
17
+ * - the § D12 PII boundary on every log line: a userId, a kind, a reason and
18
+ * an error KIND. Never the address, never the token, never the link.
19
+ * - nothing throws. A failed notification must never fail the password
20
+ * change it reports.
21
+ */
22
+
23
+ import type { AuthEmailLog } from "../auth-email";
24
+ import {
25
+ buildAuthEmailRequest,
26
+ makeSendResetPassword,
27
+ makeSendVerificationEmail,
28
+ makeSendPasswordChangedNotice,
29
+ } from "../auth-email";
30
+
31
+ const ORIGINAL_ENV = process.env;
32
+
33
+ type LogLine = { level: string; fields: Record<string, unknown> };
34
+
35
+ function makeLog(): { lines: LogLine[]; log: AuthEmailLog } {
36
+ const lines: LogLine[] = [];
37
+ const record =
38
+ (level: string) =>
39
+ (fields: Record<string, unknown>): void => {
40
+ lines.push({ level, fields });
41
+ };
42
+ return { lines, log: { warn: record("warn"), info: record("info") } };
43
+ }
44
+
45
+ /** A prisma double exposing only what these callbacks touch. */
46
+ function makePrisma(hasCredential: boolean) {
47
+ return {
48
+ account: {
49
+ findFirst: jest.fn().mockResolvedValue(
50
+ hasCredential ? { id: "acct_1" } : null,
51
+ ),
52
+ },
53
+ verification: { deleteMany: jest.fn().mockResolvedValue({ count: 2 }) },
54
+ };
55
+ }
56
+
57
+ beforeEach(() => {
58
+ process.env = { ...ORIGINAL_ENV };
59
+ process.env.AUTH_EMAIL_WEBHOOK_SECRET = "webhook-secret";
60
+ process.env.AUTH_EMAIL_FROM = "noreply@site.example";
61
+ process.env.BETTER_AUTH_URL = "https://site.example";
62
+ });
63
+
64
+ afterAll(() => {
65
+ process.env = ORIGINAL_ENV;
66
+ });
67
+
68
+ describe("buildAuthEmailRequest()", () => {
69
+ it("signs the EXACT string it returns as the body", () => {
70
+ const req = buildAuthEmailRequest(
71
+ { kind: "password-reset", userId: "user_1", to: "a@b.test", url: "https://site.example/r" },
72
+ "webhook-secret",
73
+ );
74
+
75
+ const { createHmac } = require("node:crypto") as typeof import("node:crypto");
76
+ const timestamp = req.headers["x-ailk-auth-email-timestamp"];
77
+ const expected = `sha256=${createHmac("sha256", "webhook-secret")
78
+ .update(`${timestamp}.${req.rawBody}`, "utf8")
79
+ .digest("hex")}`;
80
+
81
+ expect(req.headers["x-ailk-auth-email-signature"]).toBe(expected);
82
+ // And the body really is a single string, not an object re-serialized later.
83
+ expect(typeof req.rawBody).toBe("string");
84
+ expect(JSON.parse(req.rawBody)).toMatchObject({
85
+ kind: "password-reset",
86
+ userId: "user_1",
87
+ to: "a@b.test",
88
+ });
89
+ });
90
+
91
+ it("puts a current unix-seconds timestamp inside the signed material", () => {
92
+ const req = buildAuthEmailRequest(
93
+ { kind: "password-changed", userId: "user_1", to: "a@b.test" },
94
+ "webhook-secret",
95
+ );
96
+ const ts = Number(req.headers["x-ailk-auth-email-timestamp"]);
97
+ expect(Math.abs(Math.floor(Date.now() / 1000) - ts)).toBeLessThan(5);
98
+ });
99
+ });
100
+
101
+ describe("sendResetPassword (§ D14 — the credential-account check)", () => {
102
+ it("sends for a user who HAS a credential account", async () => {
103
+ const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
104
+ const { lines, log } = makeLog();
105
+ const prisma = makePrisma(true);
106
+
107
+ const cb = makeSendResetPassword({ prisma, send, log });
108
+ await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/api/auth/reset-password/tok" });
109
+
110
+ expect(send).toHaveBeenCalledTimes(1);
111
+ expect(lines.some((l) => l.fields.reason === "sent")).toBe(true);
112
+ });
113
+
114
+ it("sends NOTHING for a user with no credential account, and does not throw", async () => {
115
+ const send = jest.fn();
116
+ const { lines, log } = makeLog();
117
+ const prisma = makePrisma(false);
118
+
119
+ const cb = makeSendResetPassword({ prisma, send, log });
120
+ await expect(
121
+ cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" }),
122
+ ).resolves.toBeUndefined();
123
+
124
+ expect(send).not.toHaveBeenCalled();
125
+ expect(lines.some((l) => l.fields.reason === "no_credential_account")).toBe(
126
+ true,
127
+ );
128
+ });
129
+
130
+ it("never throws when the transport fails", async () => {
131
+ const send = jest.fn().mockRejectedValue(new Error("network"));
132
+ const { lines, log } = makeLog();
133
+ const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
134
+
135
+ await expect(
136
+ cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" }),
137
+ ).resolves.toBeUndefined();
138
+ expect(lines.some((l) => l.fields.reason === "send_failed")).toBe(true);
139
+ });
140
+
141
+ it("logs nothing when the relay secret is absent — it does not send blind", async () => {
142
+ delete process.env.AUTH_EMAIL_WEBHOOK_SECRET;
143
+ const send = jest.fn();
144
+ const { lines, log } = makeLog();
145
+ const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
146
+
147
+ await cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" });
148
+ expect(send).not.toHaveBeenCalled();
149
+ expect(lines.some((l) => l.fields.reason === "not_configured")).toBe(true);
150
+ });
151
+ });
152
+
153
+ describe("the § D12 PII boundary", () => {
154
+ it("never logs the address, the token or the assembled link", async () => {
155
+ const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
156
+ const { lines, log } = makeLog();
157
+ const url =
158
+ "https://site.example/api/auth/reset-password/SECRETTOKEN?callbackURL=%2Freset-password";
159
+
160
+ const reset = makeSendResetPassword({ prisma: makePrisma(true), send, log });
161
+ await reset({ user: { id: "user_1", email: "person@example.com" }, url });
162
+
163
+ const verify = makeSendVerificationEmail({ send, log });
164
+ await verify({ user: { id: "user_1", email: "person@example.com" }, url });
165
+
166
+ const notice = makeSendPasswordChangedNotice({ send, log });
167
+ await notice({ id: "user_1", email: "person@example.com" });
168
+
169
+ const serialized = JSON.stringify(lines);
170
+ expect(serialized).not.toContain("person@example.com");
171
+ expect(serialized).not.toContain("SECRETTOKEN");
172
+ expect(serialized).not.toContain("site.example");
173
+ // What it DOES carry.
174
+ expect(serialized).toContain("user_1");
175
+ expect(serialized).toContain("password-reset");
176
+ });
177
+
178
+ it("carries only the tagged error KIND, never a message that can quote an address", async () => {
179
+ const send = jest.fn().mockResolvedValue({
180
+ ok: false,
181
+ status: 400,
182
+ body: { error: "validation_failed for person@example.com" },
183
+ });
184
+ const { lines, log } = makeLog();
185
+ const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
186
+
187
+ await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" });
188
+ expect(JSON.stringify(lines)).not.toContain("person@example.com");
189
+ });
190
+ });
191
+
192
+ describe("makeSendPasswordChangedNotice (§ D17)", () => {
193
+ it("sends a password-changed body carrying no url", async () => {
194
+ const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
195
+ const { log } = makeLog();
196
+ const notice = makeSendPasswordChangedNotice({ send, log });
197
+
198
+ await notice({ id: "user_1", email: "person@example.com" });
199
+
200
+ const [rawBody] = send.mock.calls[0];
201
+ const parsed = JSON.parse(rawBody);
202
+ expect(parsed.kind).toBe("password-changed");
203
+ expect(parsed).not.toHaveProperty("url");
204
+ });
205
+
206
+ it("never throws — a failed notice must not fail the password change", async () => {
207
+ const send = jest.fn().mockRejectedValue(new Error("down"));
208
+ const { log } = makeLog();
209
+ const notice = makeSendPasswordChangedNotice({ send, log });
210
+ await expect(
211
+ notice({ id: "user_1", email: "person@example.com" }),
212
+ ).resolves.toBeUndefined();
213
+ });
214
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Tests for the auth-route predicate (#5963).
3
+ *
4
+ * `isAuthPath()` is the ONE list the locale layout branches on and the five
5
+ * `(auth)` page segments are named from, so the layout and the routes cannot
6
+ * drift. Covers bare paths, locale-prefixed paths, near misses, nested paths,
7
+ * and the absent-header fail-safe.
8
+ */
9
+
10
+ import { AUTH_SEGMENTS, isAuthPath } from "../auth-routes";
11
+
12
+ const LOCALES = ["en", "fr", "de"];
13
+
14
+ describe("AUTH_SEGMENTS", () => {
15
+ it("names exactly the five auth routes", () => {
16
+ expect([...AUTH_SEGMENTS]).toEqual([
17
+ "sign-in",
18
+ "sign-up",
19
+ "forgot-password",
20
+ "reset-password",
21
+ "verify-email",
22
+ ]);
23
+ });
24
+ });
25
+
26
+ describe("isAuthPath", () => {
27
+ it.each(AUTH_SEGMENTS)("matches the bare /%s path", (segment) => {
28
+ expect(isAuthPath(`/${segment}`, LOCALES)).toBe(true);
29
+ });
30
+
31
+ it.each(AUTH_SEGMENTS)("matches the locale-prefixed /fr/%s path", (segment) => {
32
+ expect(isAuthPath(`/fr/${segment}`, LOCALES)).toBe(true);
33
+ });
34
+
35
+ it("matches a trailing-slash auth path", () => {
36
+ expect(isAuthPath("/sign-in/", LOCALES)).toBe(true);
37
+ });
38
+
39
+ it("matches an auth path carrying a query string", () => {
40
+ expect(isAuthPath("/sign-in?next=/account", LOCALES)).toBe(true);
41
+ });
42
+
43
+ it("matches a nested path under an auth segment", () => {
44
+ expect(isAuthPath("/reset-password/confirm", LOCALES)).toBe(true);
45
+ });
46
+
47
+ it("does not match a near miss", () => {
48
+ expect(isAuthPath("/sign-in-x", LOCALES)).toBe(false);
49
+ expect(isAuthPath("/sign", LOCALES)).toBe(false);
50
+ });
51
+
52
+ it("does not match a marketing path", () => {
53
+ expect(isAuthPath("/pricing", LOCALES)).toBe(false);
54
+ expect(isAuthPath("/fr/pricing", LOCALES)).toBe(false);
55
+ });
56
+
57
+ it("does not match the locale root or the site root", () => {
58
+ expect(isAuthPath("/", LOCALES)).toBe(false);
59
+ expect(isAuthPath("/fr", LOCALES)).toBe(false);
60
+ });
61
+
62
+ it("does not strip a prefix that is not an enabled locale", () => {
63
+ // `/es` is not enabled here, so `es` is the first segment, not a prefix.
64
+ expect(isAuthPath("/es/sign-in", LOCALES)).toBe(false);
65
+ });
66
+
67
+ it("fails to NOT-an-auth-path for an empty or absent pathname", () => {
68
+ // The fail-safe direction (§ D1): an absent `x-pathname` must never strip
69
+ // a marketing page's chrome. The worst case is an auth page WITH chrome,
70
+ // which the (auth) group's own tests catch.
71
+ expect(isAuthPath("", LOCALES)).toBe(false);
72
+ expect(isAuthPath(undefined, LOCALES)).toBe(false);
73
+ });
74
+ });
@@ -0,0 +1,72 @@
1
+ // oss: false
2
+ /**
3
+ * Tests for enabledMeetingSlugs (#6183 S9b D5) — the D4-resolving read both
4
+ * /meet pages share.
5
+ */
6
+ const mockMeetingTypes = jest.fn();
7
+ jest.mock("@working-theory/api-client", () => ({
8
+ __esModule: true,
9
+ schedule: { meetingTypes: (...args: unknown[]) => mockMeetingTypes(...args) },
10
+ }));
11
+
12
+ import { enabledMeetingSlugs } from "../meeting-slugs";
13
+
14
+ describe("enabledMeetingSlugs", () => {
15
+ const originalEnv = process.env["SCHEDULING_CAL_LINK"];
16
+
17
+ beforeEach(() => {
18
+ mockMeetingTypes.mockReset();
19
+ });
20
+
21
+ afterEach(() => {
22
+ if (originalEnv === undefined) delete process.env["SCHEDULING_CAL_LINK"];
23
+ else process.env["SCHEDULING_CAL_LINK"] = originalEnv;
24
+ });
25
+
26
+ it("passes a real, populated array through unchanged", async () => {
27
+ mockMeetingTypes.mockResolvedValue({
28
+ ok: true,
29
+ body: { slugs: ["intro", "demo"] },
30
+ });
31
+
32
+ await expect(enabledMeetingSlugs()).resolves.toEqual(["intro", "demo"]);
33
+ });
34
+
35
+ it("passes a real, intentionally empty array through unchanged (D4 row 2 — never the fallback)", async () => {
36
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
37
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: [] } });
38
+
39
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
40
+ });
41
+
42
+ it("null + SCHEDULING_CAL_LINK set → the derived singleton (D4 row 1)", async () => {
43
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
44
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
45
+
46
+ await expect(enabledMeetingSlugs()).resolves.toEqual(["intro-call"]);
47
+ });
48
+
49
+ it("null + SCHEDULING_CAL_LINK unset → [] (no shipped page to protect)", async () => {
50
+ delete process.env["SCHEDULING_CAL_LINK"];
51
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
52
+
53
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
54
+ });
55
+
56
+ it("a non-ok API result → [], fail closed — an outage must not open the gate", async () => {
57
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
58
+ mockMeetingTypes.mockResolvedValue({ ok: false, status: 500, body: null });
59
+
60
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
61
+ });
62
+
63
+ it("null + a MALFORMED SCHEDULING_CAL_LINK slug → [], never a corrupt fallback (D14 parity, security review 2026-09-19)", async () => {
64
+ // A comma in the derived slug would corrupt the batched event-meta
65
+ // `calLinks` join downstream (it splits on comma), misattributing the
66
+ // rest of the list to a foreign owner — MeetingSlugSchema rejects it.
67
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro,evil";
68
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
69
+
70
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
71
+ });
72
+ });
@@ -310,8 +310,25 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
310
310
  reason: "interactive flow engine, API-backed",
311
311
  subtree: true,
312
312
  },
313
- { route: "schedule", reason: "booking surface, API-backed", subtree: true },
314
- { route: "sign-in", reason: "auth surface, not a content page" },
313
+ { route: "meet", reason: "booking surface, API-backed", subtree: true },
314
+ // #6084 — both purchase return pages read a Checkout Session (or read
315
+ // nothing, for cancel) and render via SectionRenderer directly; neither
316
+ // is a content-adapter page, and the paid render's own schema.org node
317
+ // (`PaymentComplete`, D6) is asserted directly, not through this walk.
318
+ {
319
+ route: "purchase",
320
+ reason: "checkout return pages, API-backed / read-nothing",
321
+ subtree: true,
322
+ },
323
+ // The whole (auth) route group (#5963). The walk keys on the DIRECTORY
324
+ // path, and a route group's parens are part of it, so this one subtree
325
+ // entry covers all five auth pages — including /sign-in, whose own entry it
326
+ // replaces now that the page sits inside the group.
327
+ {
328
+ route: "(auth)",
329
+ reason: "auth surfaces, not content pages",
330
+ subtree: true,
331
+ },
315
332
  ];
316
333
 
317
334
  interface RouteEntry {
@@ -345,14 +362,36 @@ function exemption(route: string): string | undefined {
345
362
  )?.reason;
346
363
  }
347
364
 
348
- // The page identity a route renders — the two `renderPage({ type, slug })`
349
- // shapes tools/lint-check-content-renders.ts recognises (a literal slug, or a
350
- // slug taken from params). A route that reads its identity another way
351
- // (e.g. a page-type manifest) is not recognised and must be declared.
365
+ // The page identity a route renders — the three shapes
366
+ // tools/lint-check-content-renders.ts recognises: `renderPage({ type, slug })`
367
+ // with a literal slug, the same with a slug taken from params, and a direct
368
+ // `getMdxSource("<Type>", slug, …)` for a route that composes its own body.
369
+ // This file previously mirrored only the first two and so drifted from the
370
+ // tool its comment names (whose own `GET_MDX_SOURCE_RE` has carried the third
371
+ // arm all along); #5562 restored the parity. A route that reads its identity
372
+ // some other way (e.g. a page-type manifest) is still unrecognised and must
373
+ // be declared.
352
374
  const RENDER_PAGE_EXACT_RE =
353
375
  /renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*slug\s*:\s*["']([^"']+)["']/s;
354
376
  const RENDER_PAGE_DYNAMIC_RE =
355
377
  /renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*\bslug\s*(?!:\s*["'])/s;
378
+ // A route that composes its own body instead of calling `renderPage` — it
379
+ // resolves content with `getMdxSource(<Type>, slug, locale)` and emits the
380
+ // graph itself via `pageToJsonLd` + `<JsonLdScript>`. That is not a route
381
+ // hiding from this check; it is the SAME declaration one level down, since
382
+ // `renderPage` resolves its own content with exactly this call. `/reports/
383
+ // [slug]` (#5562) is the first: it owns a full-bleed `<main>` and a header
384
+ // section that already renders the title, so `renderPage`'s own `<h1>`/`<p>`
385
+ // would duplicate it.
386
+ //
387
+ // Recognising the shape is deliberately better than exempting the route:
388
+ // an exemption declares "nothing is known to emit JSON-LD here" — false for
389
+ // these routes — and would silence the check for a page that DOES emit a
390
+ // graph. Recognised, the route is rendered and validated like any other.
391
+ // Matched only after both `renderPage` shapes above, so a route that uses
392
+ // `renderPage` is still read through that (richer, slug-bearing) form.
393
+ // Mirrors `GET_MDX_SOURCE_RE` in tools/lint-check-content-renders.ts.
394
+ const GET_MDX_SOURCE_RE = /getMdxSource\s*\(\s*["']([A-Za-z]+)["']/s;
356
395
 
357
396
  function routeIdentity(
358
397
  file: string,
@@ -362,6 +401,11 @@ function routeIdentity(
362
401
  if (exact) return { type: exact[1] as string, slug: exact[2] as string };
363
402
  const dynamic = RENDER_PAGE_DYNAMIC_RE.exec(src);
364
403
  if (dynamic) return { type: dynamic[1] as string, slug: null };
404
+ // Self-composing route (see GET_MDX_SOURCE_RE). Its slug always comes from
405
+ // the route params — a self-composing FIXED route would name its slug as a
406
+ // literal to `getMdxSource`, and none exists — so the dynamic path applies.
407
+ const ownBody = GET_MDX_SOURCE_RE.exec(src);
408
+ if (ownBody) return { type: ownBody[1] as string, slug: null };
365
409
  return null;
366
410
  }
367
411