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
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Better Auth's outbound-mail callbacks (#5962 § D3).
3
+ *
4
+ * WHY THIS FILE EXISTS AT ALL. `architecture.yaml`'s `apps/web` node denies
5
+ * `packages/email` in as many words ("email send is server-side only via
6
+ * apps/api"), and `tools/lint-architecture.ts` evaluates deny BEFORE allow, so
7
+ * an added allow entry would not clear it. These callbacks therefore serialize
8
+ * one body, sign it, and POST it to `apps/api`'s
9
+ * `/v1/auth/transactional-email` through the canonical client. `apps/web`
10
+ * sends no mail and imports no mail package.
11
+ *
12
+ * WHY IT IS NOT INLINE IN `auth.ts`. Three callbacks doing a credential-account
13
+ * lookup, a one-shot serialization, an HMAC and a bounded log line turn a
14
+ * declarative config file into a service module, and make themselves
15
+ * untestable without constructing a Better Auth instance. Each factory below
16
+ * takes its deps, so the suite next door exercises them with an injected
17
+ * transport and a prisma double.
18
+ *
19
+ * ONE SERIALIZATION. `buildAuthEmailRequest` builds the body string ONCE,
20
+ * signs THAT string, and hands the same string to the transport — the
21
+ * invariant `apps/api/src/services/revalidation.ts` states for itself ("a
22
+ * key-order or whitespace drift between signer and sender is impossible by
23
+ * construction"). This is why the call cannot go through the client's
24
+ * `post()`, which stringifies its own payload argument.
25
+ *
26
+ * PII BOUNDARY (§ D12). Every line carries `userId`, `kind`, `reason` and — on
27
+ * a failure — an error KIND. Never the recipient address, never the token,
28
+ * never the assembled link. The link IS the credential: one log line quoting
29
+ * it hands anyone with log access a live reset for an arbitrary account.
30
+ *
31
+ * NOTHING HERE THROWS. A reset send that fails must not fail the endpoint that
32
+ * already answered 200, and a failed § D17 notice must not fail the password
33
+ * change it reports.
34
+ */
35
+
36
+ import { createHmac } from "node:crypto";
37
+
38
+ import { authEmail } from "@working-theory/api-client";
39
+
40
+ /** The three fixed kinds the relay will send. A caller picks no copy. */
41
+ export type AuthEmailKind =
42
+ | "password-reset"
43
+ | "email-verification"
44
+ | "password-changed";
45
+
46
+ export interface AuthEmailPayload {
47
+ kind: AuthEmailKind;
48
+ userId: string;
49
+ to: string;
50
+ url?: string;
51
+ expiresInMinutes?: number;
52
+ }
53
+
54
+ export interface AuthEmailRequest {
55
+ rawBody: string;
56
+ headers: Record<string, string>;
57
+ }
58
+
59
+ /** The transport seam — `authEmail.send`'s shape, injectable for tests. */
60
+ export type AuthEmailTransport = (
61
+ rawBody: string,
62
+ headers: Record<string, string>,
63
+ ) => Promise<unknown>;
64
+
65
+ /** The log seam — the two levels § D12's table uses. */
66
+ export interface AuthEmailLog {
67
+ warn: (fields: Record<string, unknown>) => void;
68
+ info: (fields: Record<string, unknown>) => void;
69
+ }
70
+
71
+ /**
72
+ * The narrow slice of the Prisma client these callbacks touch.
73
+ *
74
+ * Declared with METHOD SHORTHAND rather than arrow properties, deliberately
75
+ * (code review). Under `strictFunctionTypes` a property-typed
76
+ * `(args: unknown) => …` is checked contravariantly, so the generated
77
+ * `PrismaClient` — whose delegates take their own narrow arg types — is not
78
+ * assignable to it, and every caller needs an `as unknown as` double
79
+ * assertion to get past that. Method shorthand is checked bivariantly, which
80
+ * is the correct reading here: this interface exists to NARROW the surface
81
+ * these callbacks may touch, not to accept a wider argument than Prisma does.
82
+ * The real client now satisfies it structurally and `auth.ts` passes it with
83
+ * no cast at all.
84
+ */
85
+ export interface AuthEmailPrisma {
86
+ account: {
87
+ findFirst(args: unknown): Promise<{ id: string } | null>;
88
+ };
89
+ verification?: {
90
+ deleteMany(args: unknown): Promise<unknown>;
91
+ };
92
+ }
93
+
94
+ const defaultLog: AuthEmailLog = {
95
+ warn: (fields) => console.warn(`[auth-email] ${JSON.stringify(fields)}`),
96
+ info: (fields) => console.info(`[auth-email] ${JSON.stringify(fields)}`),
97
+ };
98
+
99
+ /**
100
+ * Builds the signed request for one message. The timestamp is inside the
101
+ * signed material, so it cannot be swapped independently of the body it
102
+ * authenticates — the replay window #5851's revalidate pair deliberately does
103
+ * without, closed here because a replayed EMAIL is not a no-op.
104
+ */
105
+ export function buildAuthEmailRequest(
106
+ payload: AuthEmailPayload,
107
+ secret: string,
108
+ ): AuthEmailRequest {
109
+ // ONE serialization. This exact string is both signed and sent.
110
+ const rawBody = JSON.stringify({
111
+ kind: payload.kind,
112
+ userId: payload.userId,
113
+ to: payload.to,
114
+ ...(payload.url !== undefined ? { url: payload.url } : {}),
115
+ ...(payload.expiresInMinutes !== undefined
116
+ ? { expiresInMinutes: payload.expiresInMinutes }
117
+ : {}),
118
+ });
119
+ const timestamp = String(Math.floor(Date.now() / 1000));
120
+ const signature = `sha256=${createHmac("sha256", secret)
121
+ .update(`${timestamp}.${rawBody}`, "utf8")
122
+ .digest("hex")}`;
123
+
124
+ return {
125
+ rawBody,
126
+ headers: {
127
+ "x-ailk-auth-email-signature": signature,
128
+ "x-ailk-auth-email-timestamp": timestamp,
129
+ },
130
+ };
131
+ }
132
+
133
+ interface DispatchDeps {
134
+ send: AuthEmailTransport;
135
+ /** Defaults to a console logger bound to the § D12 field set. */
136
+ log?: AuthEmailLog;
137
+ }
138
+
139
+ /**
140
+ * Signs and posts one message, and swallows every failure into one log line.
141
+ * The relay answers 202 for an accepted send AND for one its per-recipient
142
+ * budget suppressed, so a caller here cannot tell the two apart — which is the
143
+ * point: an observable suppression is an account-enumeration oracle.
144
+ */
145
+ async function dispatch(
146
+ payload: AuthEmailPayload,
147
+ { send, log }: { send: AuthEmailTransport; log: AuthEmailLog },
148
+ ): Promise<void> {
149
+ const secret = process.env.AUTH_EMAIL_WEBHOOK_SECRET;
150
+ const base = { userId: payload.userId, kind: payload.kind };
151
+
152
+ if (!secret) {
153
+ // Never sign with an empty key: the relay would reject every delivery and
154
+ // the deployment would have no local signal at all.
155
+ log.warn({ ...base, reason: "not_configured" });
156
+ return;
157
+ }
158
+
159
+ /**
160
+ * SIGNING IS GUARDED TOO (#6068). This file's header promises "NOTHING HERE
161
+ * THROWS", and `auth.ts`'s `onPasswordReset` relies on that promise in as
162
+ * many words — but the build-and-sign step used to sit OUTSIDE the try
163
+ * below, so anything `buildAuthEmailRequest` threw propagated straight out
164
+ * of a callback that runs AFTER the password has already changed and the
165
+ * presented token has already been consumed. #6061 made that callback
166
+ * unconditional, so it now runs in degraded configurations, which is exactly
167
+ * where a signer is least likely to be healthy.
168
+ *
169
+ * Its own `try` rather than a widened one, so the log line still names the
170
+ * STAGE that failed: a signer that cannot run is an operator's problem with
171
+ * this deployment, not the relay's, and `transport_error` would send whoever
172
+ * reads the line looking in the wrong place.
173
+ */
174
+ let request: AuthEmailRequest;
175
+ try {
176
+ request = buildAuthEmailRequest(payload, secret);
177
+ } catch {
178
+ // The KIND only — see this file's PII boundary note.
179
+ log.warn({ ...base, reason: "send_failed", errorKind: "signing_failed" });
180
+ return;
181
+ }
182
+
183
+ try {
184
+ const result = (await send(request.rawBody, request.headers)) as
185
+ | { ok: boolean; status?: number }
186
+ | undefined;
187
+ if (result && result.ok === false) {
188
+ // The STATUS, never the response body — the relay's own error text is
189
+ // not guaranteed free of the address it was given.
190
+ log.warn({ ...base, reason: "send_failed", errorKind: "relay_rejected" });
191
+ return;
192
+ }
193
+ log.info({ ...base, reason: "sent" });
194
+ } catch {
195
+ // The error's NAME would be safe; its message is not guaranteed to be.
196
+ log.warn({ ...base, reason: "send_failed", errorKind: "transport_error" });
197
+ }
198
+ }
199
+
200
+ export interface ResetPasswordDeps extends DispatchDeps {
201
+ prisma: AuthEmailPrisma;
202
+ }
203
+
204
+ /**
205
+ * Better Auth's `sendResetPassword` (§ D14).
206
+ *
207
+ * The credential-account check is the load-bearing part. `password.mjs`
208
+ * CREATES a `credential` account on reset if the user has none — so combined
209
+ * with § D2, which enables the password method on `AUTH_EMAIL_SERVER` +
210
+ * `AUTH_EMAIL_FROM`, an existing Google-or-GitHub-only user who completed a
211
+ * reset would silently gain a password path around their identity provider
212
+ * and whatever MFA it enforces. No credential account means no mail.
213
+ *
214
+ * The skip is SILENT — same 200, same body, log only. The visitor-facing
215
+ * answer must not distinguish "this address has no password" from "this
216
+ * address does not exist": the second is the enumeration AC-1 forbids and the
217
+ * first is nearly as useful to an attacker, since it maps a site's users to
218
+ * their identity provider.
219
+ */
220
+ export function makeSendResetPassword(deps: ResetPasswordDeps) {
221
+ const { prisma, send } = deps;
222
+ const log = deps.log ?? defaultLog;
223
+ return async function sendResetPassword({
224
+ user,
225
+ url,
226
+ }: {
227
+ user: { id: string; email: string };
228
+ url: string;
229
+ }): Promise<void> {
230
+ let hasPassword: { id: string } | null;
231
+ try {
232
+ hasPassword = await prisma.account.findFirst({
233
+ where: { userId: user.id, providerId: "credential" },
234
+ select: { id: true },
235
+ });
236
+ } catch {
237
+ log.warn({
238
+ userId: user.id,
239
+ kind: "password-reset",
240
+ reason: "send_failed",
241
+ errorKind: "lookup_failed",
242
+ });
243
+ return;
244
+ }
245
+
246
+ if (!hasPassword) {
247
+ log.warn({
248
+ userId: user.id,
249
+ kind: "password-reset",
250
+ reason: "no_credential_account",
251
+ });
252
+ return; // no send; the endpoint's 200 body is unchanged
253
+ }
254
+
255
+ await dispatch(
256
+ {
257
+ kind: "password-reset",
258
+ userId: user.id,
259
+ to: user.email,
260
+ url,
261
+ expiresInMinutes: 60,
262
+ },
263
+ { send, log },
264
+ );
265
+ };
266
+ }
267
+
268
+ /** Better Auth's `sendVerificationEmail` (§ D9). */
269
+ export function makeSendVerificationEmail(deps: DispatchDeps) {
270
+ const { send } = deps;
271
+ const log = deps.log ?? defaultLog;
272
+ return async function sendVerificationEmail({
273
+ user,
274
+ url,
275
+ }: {
276
+ user: { id: string; email: string };
277
+ url: string;
278
+ }): Promise<void> {
279
+ await dispatch(
280
+ {
281
+ kind: "email-verification",
282
+ userId: user.id,
283
+ to: user.email,
284
+ url,
285
+ expiresInMinutes: 24 * 60,
286
+ },
287
+ { send, log },
288
+ );
289
+ };
290
+ }
291
+
292
+ /**
293
+ * The completed-reset notice (§ D17).
294
+ *
295
+ * § D2 leaves `requireEmailVerification: false` and § D7 revokes every session
296
+ * on reset, so a takeover through a compromised mailbox is otherwise silent
297
+ * and total: the owner's sessions vanish and no message ever reaches them.
298
+ * This notice is the only signal the owner gets. It carries no link and no
299
+ * token — the payload has no `url` — so the message reporting a takeover
300
+ * cannot hand the attacker a second credential.
301
+ */
302
+ export function makeSendPasswordChangedNotice(deps: DispatchDeps) {
303
+ const { send } = deps;
304
+ const log = deps.log ?? defaultLog;
305
+ return async function sendPasswordChangedNotice(user: {
306
+ id: string;
307
+ email: string;
308
+ }): Promise<void> {
309
+ await dispatch(
310
+ { kind: "password-changed", userId: user.id, to: user.email },
311
+ { send, log },
312
+ );
313
+ };
314
+ }
315
+
316
+ /** The production transport — the canonical client, never a bare fetch. */
317
+ export const authEmailTransport: AuthEmailTransport = (rawBody, headers) =>
318
+ authEmail.send(rawBody, headers);
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Single source of truth for:
5
5
  * - AILK_AUTH_REQUIRED flag (whether protected routes redirect unauthenticated users)
6
+ * - AILK_AUTH_SIGNUP_OPEN / AILK_AUTH_PASSWORD_RESET flags (#5963) — whether
7
+ * the /sign-up and /forgot-password|/reset-password surfaces offer a flow
6
8
  * - Active provider list (which Better Auth providers are activated by env vars)
7
9
  *
8
10
  * No better-auth import — this module is safe to require in tests without the
@@ -24,6 +26,32 @@ export function isAuthRequired(): boolean {
24
26
  return val === "1" || val === "true";
25
27
  }
26
28
 
29
+ /**
30
+ * Returns true when AILK_AUTH_SIGNUP_OPEN is set to "1" or "true" (#5963).
31
+ * Default (unset or "0"/"false") = sign-up is CLOSED, which is what a
32
+ * scaffold actually is: `apps/web/auth.ts` does not enable
33
+ * `emailAndPassword`, so there is no public sign-up flow to open. The
34
+ * `/sign-up` page renders the `invitation` intent while this is false.
35
+ *
36
+ * This is the seam #5962 flips once the server-side flow is real.
37
+ */
38
+ export function isSignUpOpen(): boolean {
39
+ const val = process.env.AILK_AUTH_SIGNUP_OPEN;
40
+ return val === "1" || val === "true";
41
+ }
42
+
43
+ /**
44
+ * Returns true when AILK_AUTH_PASSWORD_RESET is set to "1" or "true" (#5963).
45
+ * Default (unset or "0"/"false") = password reset is NOT available — again
46
+ * the honest reading of a scaffold, where `emailAndPassword` is unconfigured.
47
+ * `/forgot-password` and `/reset-password` render their not-available state
48
+ * while this is false; #5962 lands the flow behind it.
49
+ */
50
+ export function isResetAvailable(): boolean {
51
+ const val = process.env.AILK_AUTH_PASSWORD_RESET;
52
+ return val === "1" || val === "true";
53
+ }
54
+
27
55
  /**
28
56
  * Returns the list of provider IDs whose required env vars are all present.
29
57
  * A provider is only included when every one of its required vars is non-empty.
@@ -64,3 +92,60 @@ export function getActiveProviderIds(): string[] {
64
92
  export function hasAnyProvider(): boolean {
65
93
  return getActiveProviderIds().length > 0;
66
94
  }
95
+
96
+ /**
97
+ * True when this deployment can actually complete a password reset end to end,
98
+ * SAFELY (#5962 § D10). Five conjuncts, each of which independently makes the
99
+ * flow impossible or unsafe:
100
+ *
101
+ * AILK_AUTH_PASSWORD_RESET the operator's own opt-in (#5963 D5, read here
102
+ * through isResetAvailable()). Default off.
103
+ * the password METHOD `emailAndPassword` is enabled on exactly this
104
+ * predicate (#5962 D2); with only OAuth
105
+ * configured there is no password to reset.
106
+ * a send SEAM AUTH_EMAIL_WEBHOOK_SECRET + AUTH_EMAIL_FROM —
107
+ * without the secret the apps/api relay answers
108
+ * 503 and a visitor would be told "check your
109
+ * email" for mail that will never be sent.
110
+ * BETTER_AUTH_URL unset means Better Auth derives the emitted
111
+ * link's host AND the redirectTo allow-list from
112
+ * the incoming REQUEST (§ D13). A reset token
113
+ * minted under a request-derived origin is a
114
+ * token an attacker can aim at itself.
115
+ * BETTER_AUTH_SECRET unset means auth.ts's dev fallback — a string
116
+ * published in this repository — signs the
117
+ * verification JWTs and the session cookies.
118
+ *
119
+ * WHY THIS IS A SECOND NAME RATHER THAN A WIDENED `isResetAvailable()`.
120
+ * #5962's spec § D10 widens `isResetAvailable()` itself and makes this an
121
+ * alias of it. That is not takeable as written: #5963 landed
122
+ * `apps/web/__tests__/auth/env-helpers.test.ts`, which asserts
123
+ * `isResetAvailable()` is true on the flag ALONE, and #5962's AC-3 requires
124
+ * every existing auth test to pass UNEDITED. So the conjunction lives here,
125
+ * under its own name, and `isResetAvailable()` keeps the contract #5963
126
+ * shipped — one conjunct of this one.
127
+ *
128
+ * The consequence, stated rather than hidden: on a deployment that sets the
129
+ * flag but not the transport, `/forgot-password` renders the form rather than
130
+ * the not-available card, and a submit lands on the error state instead —
131
+ * because the server refuses to send. That is a degraded message, not a false
132
+ * promise: the card only swaps to "check your email" on a successful
133
+ * response, so no visitor is ever told mail is coming when none can be sent.
134
+ *
135
+ * `RESEND_API_KEY` is deliberately NOT a conjunct: it is `apps/api`'s
136
+ * variable, not `apps/web`'s, and `apps/web` cannot read it. The relay answers
137
+ * 503 when it is absent, which lands as a `send_failed` log line rather than a
138
+ * page state.
139
+ *
140
+ * No better-auth import: this module stays require-safe in tests, exactly as
141
+ * its own header promises.
142
+ */
143
+ export function resetSendConfigured(): boolean {
144
+ if (!isResetAvailable()) return false;
145
+ if (!getActiveProviderIds().includes("email")) return false;
146
+ if (!process.env.AUTH_EMAIL_WEBHOOK_SECRET || !process.env.AUTH_EMAIL_FROM) {
147
+ return false;
148
+ }
149
+ if (!process.env.BETTER_AUTH_URL) return false;
150
+ return Boolean(process.env.BETTER_AUTH_SECRET);
151
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The auth route group's segment list and its path predicate (#5963).
3
+ *
4
+ * ONE module, so the list `app/[locale]/layout.tsx` branches on to drop the
5
+ * marketing chrome and the list the `(auth)` group's page segments exist at
6
+ * cannot drift. A route group contributes no URL segment, so these are the
7
+ * real paths: `/sign-in`, `/sign-up`, `/forgot-password`, `/reset-password`
8
+ * and `/verify-email`, each under next-intl's `as-needed` prefixing (bare for
9
+ * the default locale, `/<locale>/…` otherwise).
10
+ *
11
+ * No Next.js import — a pure function, asserted directly by its own suite.
12
+ */
13
+
14
+ /** The five segments the `(auth)` route group serves. */
15
+ export const AUTH_SEGMENTS = [
16
+ "sign-in",
17
+ "sign-up",
18
+ "forgot-password",
19
+ "reset-password",
20
+ "verify-email",
21
+ ] as const;
22
+
23
+ export type AuthSegment = (typeof AUTH_SEGMENTS)[number];
24
+
25
+ /**
26
+ * Whether `pathname` addresses one of the five auth routes.
27
+ *
28
+ * Locale-prefix-aware the same way `resolveLocalePath()` is: strip a leading
29
+ * `/<locale>` when it matches an ENABLED locale, then compare the first
30
+ * remaining segment against the list. A prefix that is not an enabled locale
31
+ * is a first segment, not a prefix.
32
+ *
33
+ * Fail-safe direction: an absent or empty pathname answers `false`. The
34
+ * layout's branch reads a header that can legitimately be missing (a direct
35
+ * render in a unit test, a path the middleware matcher excludes), and a
36
+ * missing header must never strip a marketing page's chrome. The worst case
37
+ * is an auth page that renders WITH chrome, which the group's own tests catch.
38
+ */
39
+ export function isAuthPath(
40
+ pathname: string | undefined | null,
41
+ locales: readonly string[],
42
+ ): boolean {
43
+ if (!pathname) return false;
44
+
45
+ // Drop a query string / fragment — `x-pathname` carries the path, but a
46
+ // caller passing a full request path should get the same verdict.
47
+ const path = pathname.split(/[?#]/)[0] ?? "";
48
+ const bare =
49
+ locales.length > 0
50
+ ? path.replace(new RegExp(`^/(${locales.join("|")})(?=/|$)`), "")
51
+ : path;
52
+
53
+ const first = bare.split("/")[1];
54
+ return AUTH_SEGMENTS.some((segment) => segment === first);
55
+ }
@@ -0,0 +1,53 @@
1
+ // oss: false
2
+ /**
3
+ * enabledMeetingSlugs — the enabled-meeting-slugs read both `/meet` pages
4
+ * share (#6183 S9b D5).
5
+ *
6
+ * Resolves `GET /v1/schedule/meeting-types`'s three D4 outcomes into ONE
7
+ * array both pages consume identically:
8
+ *
9
+ * - an array is returned as-is (a real, operator-written enabled list —
10
+ * D4 rows 2 and 3, INCLUDING an intentionally empty `[]`);
11
+ * - `null` becomes the D4 row-1 singleton derived from
12
+ * `SCHEDULING_CAL_LINK` — the ONE slug an unconfigured deployment has
13
+ * ever advertised — or `[]` when that env var is unset;
14
+ * - any non-`ok` API result becomes `[]`, fail closed: an API outage
15
+ * must not open the gate.
16
+ *
17
+ * A duplicated inline call in both pages is how the two would drift, and
18
+ * it is also how one of them would forget the fail-closed arm — this is
19
+ * the ONE place that reasoning lives (D5).
20
+ */
21
+ import { schedule } from "@working-theory/api-client";
22
+ import { MeetingSlugSchema } from "@working-theory/site-config/schema";
23
+
24
+ /**
25
+ * The D4 row-1 fallback: the slug half of the env-configured booker's own
26
+ * calLink (`"<owner>/<slug>"`), or `[]` when the env var is unset. This is
27
+ * deliberately the ONLY reader of `SCHEDULING_CAL_LINK`'s SECOND segment —
28
+ * `meet/[slug]/page.tsx`'s own `composedCalLink` reads the FIRST segment
29
+ * (the owner) for an unrelated purpose and is not touched by this file.
30
+ *
31
+ * Re-validated against `MeetingSlugSchema` (D14 parity with the operator-
32
+ * written path — `meeting-types.ts`'s own per-entry `safeParse` filter):
33
+ * `SCHEDULING_CAL_LINK` is operator-set env config, not a schema-validated
34
+ * write, so a malformed value (a stray comma, an empty second segment) could
35
+ * otherwise yield a slug that corrupts the batched `event-meta` `calLinks`
36
+ * join downstream — which splits on comma and would misattribute the rest
37
+ * of the list to a foreign owner (security review, 2026-09-19).
38
+ */
39
+ function derivedFallbackSlugs(): string[] {
40
+ const configured = process.env["SCHEDULING_CAL_LINK"];
41
+ if (!configured) return [];
42
+ const slug = configured.split("/")[1];
43
+ if (!slug) return [];
44
+ const parsed = MeetingSlugSchema.safeParse(slug);
45
+ return parsed.success ? [parsed.data] : [];
46
+ }
47
+
48
+ /** The enabled meeting-type slug set for this deployment, D4-resolved. */
49
+ export async function enabledMeetingSlugs(): Promise<string[]> {
50
+ const res = await schedule.meetingTypes();
51
+ if (!res.ok) return [];
52
+ return res.body.slugs ?? derivedFallbackSlugs();
53
+ }
@@ -1,13 +1,33 @@
1
- // nav-config.ts — Nav item registry for apps/web OSS template (#1451)
1
+ // nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469).
2
2
  //
3
- // The OSS template ships a focused navigation inventory — home, docs, and blog.
4
- // Marketing-site items (pricing, services, about) belong in the per-site fork.
5
- // Paths are locale-agnostic; Nav prefixes them with the active locale at render.
3
+ // The Free marketing-site template's nav (the approved chrome, #5469 D5):
4
+ // a Product flyout (How it works · Features · FAQ), then How it works,
5
+ // Pricing and About as top-level items. Paths are locale-agnostic; Nav
6
+ // prefixes them with the active locale at render.
7
+ //
8
+ // In a SCAFFOLD this file is regenerated by create-ailk from the declared
9
+ // route surfaces (`renderNavConfig` in packages/create-ailk/src/surfaces.ts)
10
+ // so the nav never links a page the scaffold does not carry — keep the two in
11
+ // step when the base item set changes.
6
12
 
7
13
  import type { NavItem } from "@working-theory/ui";
14
+ import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
8
15
 
9
16
  export const navItems: NavItem[] = [
10
- { labelKey: "home", path: "/" },
11
- { labelKey: "docs", path: "/docs" },
12
- { labelKey: "blog", path: "/blog" },
17
+ {
18
+ labelKey: "product",
19
+ flyout: {
20
+ presentation: "list",
21
+ width: "trigger",
22
+ features: [
23
+ { labelKey: "howItWorks", descKey: "howItWorksDesc", icon: Layers, path: "/how-it-works" },
24
+ { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/features" },
25
+ { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },
26
+ ],
27
+ actions: [{ labelKey: "docs", icon: BookOpen, path: "/how-it-works" }],
28
+ },
29
+ },
30
+ { labelKey: "howItWorks", path: "/how-it-works" },
31
+ { labelKey: "pricing", path: "/pricing" },
32
+ { labelKey: "about", path: "/about" },
13
33
  ];
@@ -17,7 +17,7 @@
17
17
  * hand-rolled `fetch`. This module supplies exactly the two things that
18
18
  * namespace doesn't own: the cookie/origin-forwarding `init` (B0's additive
19
19
  * `init?: { headers }` on `http.get`) and the envelope→`ApiError` unwrap
20
- * (mirrors `apps-paid/workspace/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
20
+ * (mirrors `apps-paid/admin/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
21
21
  * the `@working-theory/api-client` `ApiResult<T>` shape B0 already returns
22
22
  * rather than re-parsing the wire envelope by hand).
23
23
  */
@@ -0,0 +1,29 @@
1
+ // site-chrome.ts — resolved template layout knobs.
2
+ //
3
+ // GENERATED by create-ailk from the declared template's `layout` block
4
+ // (#5612). A scaffold with no template, or a template with no `layout`
5
+ // block, gets these defaults — byte-identical to every scaffold before this
6
+ // file existed. `apps/web/app/[locale]/layout.tsx` reads this to decide:
7
+ // - nav: "mark-only" → Nav renders the brand mark + right-hand controls
8
+ // only (no nav links).
9
+ // - footer: "centered" → Footer renders its existing centered variant.
10
+ // - announcementBanner: false → the home-only announcement Banner is not
11
+ // mounted (the cookie-consent Banner is unaffected — it is not an
12
+ // announcement).
13
+ // - home: "deck" → the locale-root route mounts MarketingShell with
14
+ // snap="mandatory" + deckHero (spec 5426 D7); every other route is
15
+ // unaffected.
16
+
17
+ export interface SiteChrome {
18
+ nav: "full" | "mark-only";
19
+ footer: "columns" | "centered";
20
+ announcementBanner: boolean;
21
+ home: "default" | "deck";
22
+ }
23
+
24
+ export const siteChrome: SiteChrome = {
25
+ nav: "full",
26
+ footer: "columns",
27
+ announcementBanner: true,
28
+ home: "default",
29
+ };
@@ -0,0 +1,50 @@
1
+ // Strips token-bearing query params from a URL before it's reported to
2
+ // third-party telemetry (Google Analytics, Sentry). #5562 found the report
3
+ // grant token (`/reports/<slug>?g=<grantId>`) — the bearer credential for a
4
+ // private customer deliverable — riding `document.location.href` into GA's
5
+ // `page_location` default and into Sentry events, transactions, and session
6
+ // replays. This is the single source of the stripped-parameter list.
7
+ //
8
+ // `session_id` joined by #6084 (D15): the Checkout Session id riding
9
+ // `/purchase/success?session_id=cs_…` is the SAME class of bearer capability
10
+ // — the exact defect #5562 fixed for `?g=`, recurring on a second surface.
11
+ //
12
+ // Mirrored (duplicated, not shared) in packages/analytics/src/scrub.ts,
13
+ // since apps/* must not import @working-theory/* packages — keep both lists
14
+ // in sync (#5613).
15
+ export const TOKEN_QUERY_PARAMS = ["g", "session_id"];
16
+
17
+ // Sentry navigation breadcrumbs (`data.to`/`data.from`) are relative
18
+ // (`path?search`, no origin) rather than the absolute URLs GA and
19
+ // `request.url` deal in — resolve against a throwaway base so relative
20
+ // values get scrubbed too, then strip the base back off (security review,
21
+ // PR #5639).
22
+ const RELATIVE_URL_BASE = "https://scrub.invalid";
23
+
24
+ function hasScheme(url: string): boolean {
25
+ return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url) || url.startsWith("//");
26
+ }
27
+
28
+ export function scrubTelemetryUrl(url: string): string {
29
+ const relative = !hasScheme(url);
30
+ let parsed: URL;
31
+ try {
32
+ parsed = relative ? new URL(url, RELATIVE_URL_BASE) : new URL(url);
33
+ } catch {
34
+ return url;
35
+ }
36
+
37
+ let changed = false;
38
+ for (const param of TOKEN_QUERY_PARAMS) {
39
+ if (parsed.searchParams.has(param)) {
40
+ parsed.searchParams.delete(param);
41
+ changed = true;
42
+ }
43
+ }
44
+
45
+ if (!changed) {
46
+ return url;
47
+ }
48
+ const result = parsed.toString();
49
+ return relative ? result.slice(RELATIVE_URL_BASE.length) : result;
50
+ }