create-ailk 0.7.0 → 0.9.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 (310) hide show
  1. package/README.md +112 -10
  2. package/component-catalog.json +798 -49
  3. package/component-catalog.md +9 -6
  4. package/dist/add-module.d.ts +20 -0
  5. package/dist/add-module.js +43 -7
  6. package/dist/add-route.d.ts +56 -0
  7. package/dist/add-route.js +286 -0
  8. package/dist/apply-design-md.d.ts +7 -0
  9. package/dist/apply-design-md.js +50 -0
  10. package/dist/apply-design.d.ts +64 -0
  11. package/dist/apply-design.js +273 -0
  12. package/dist/claude-md.d.ts +111 -0
  13. package/dist/claude-md.js +334 -0
  14. package/dist/cli.js +114 -28
  15. package/dist/derive-architecture.js +41 -36
  16. package/dist/design-compile.d.ts +71 -0
  17. package/dist/design-compile.js +98 -0
  18. package/dist/design-package.d.ts +83 -0
  19. package/dist/design-package.js +468 -0
  20. package/dist/design-presets.d.ts +86 -0
  21. package/dist/design-presets.js +642 -0
  22. package/dist/design-scaffold.d.ts +76 -0
  23. package/dist/design-scaffold.js +220 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +4 -1
  26. package/dist/legacy-nav-renderers.d.ts +45 -0
  27. package/dist/legacy-nav-renderers.js +545 -0
  28. package/dist/lib/auth-secret-file.d.ts +19 -0
  29. package/dist/lib/auth-secret-file.js +112 -0
  30. package/dist/lib/design-checks.d.ts +78 -0
  31. package/dist/lib/design-checks.js +174 -0
  32. package/dist/lib/extract-module-bundle.js +5 -1
  33. package/dist/lib/license-key-files.d.ts +9 -0
  34. package/dist/lib/license-key-files.js +2 -2
  35. package/dist/lib/module-license-gate.d.ts +89 -0
  36. package/dist/lib/module-license-gate.js +171 -12
  37. package/dist/lib/module-tier.d.ts +45 -16
  38. package/dist/lib/module-tier.js +76 -0
  39. package/dist/lib/pro-callout.js +10 -2
  40. package/dist/lib/render-verdict.d.ts +52 -0
  41. package/dist/lib/render-verdict.js +77 -0
  42. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  43. package/dist/lib/scaffold-render-sweep.js +236 -0
  44. package/dist/module-architecture.d.ts +19 -16
  45. package/dist/module-architecture.js +64 -21
  46. package/dist/parse-args.d.ts +88 -10
  47. package/dist/parse-args.js +272 -21
  48. package/dist/programmatic.d.ts +66 -12
  49. package/dist/programmatic.js +361 -34
  50. package/dist/site-chrome.d.ts +19 -9
  51. package/dist/site-chrome.js +23 -13
  52. package/dist/site-manifest.d.ts +25 -0
  53. package/dist/site-manifest.js +254 -0
  54. package/dist/site-roster.d.ts +41 -0
  55. package/dist/site-roster.js +149 -11
  56. package/dist/starter-grid.d.ts +232 -0
  57. package/dist/starter-grid.js +545 -0
  58. package/dist/surfaces.d.ts +41 -9
  59. package/dist/surfaces.js +268 -92
  60. package/dist/template-manifest.d.ts +65 -29
  61. package/dist/template-manifest.js +161 -382
  62. package/dist/template-names.d.ts +14 -12
  63. package/dist/template-names.js +13 -17
  64. package/package.json +5 -4
  65. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  66. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  67. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  68. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  69. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  70. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  71. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  72. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  73. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  74. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  75. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  76. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  77. package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
  78. package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
  79. package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
  80. package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
  81. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  82. package/site-templates/designs/minimalism/sample/README.md +48 -0
  83. package/site-templates/designs/standard/DESIGN.md +103 -6
  84. package/site-templates/marketing-site/DESIGN.md +103 -6
  85. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
  86. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
  87. package/site-templates/marketing-site-free/DESIGN.md +103 -6
  88. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
  89. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
  90. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  91. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  92. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  93. package/site-templates/saas-product/DESIGN.md +103 -6
  94. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  95. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
  96. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
  97. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  98. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  99. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  100. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  101. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  102. package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  103. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  104. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  105. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  106. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  107. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  108. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
  109. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
  110. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  111. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  112. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  113. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  114. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  115. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  116. package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
  117. package/site-templates/shapes/organization-service/provenance.json +4 -0
  118. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
  119. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
  120. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  121. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  122. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  123. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  124. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  125. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
  126. package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
  127. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  128. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  129. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  130. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  131. package/site-templates/shapes/person-service/provenance.json +4 -0
  132. package/templates/.claude/rules/architecture.md +7 -0
  133. package/templates/.claude/skills/README.md +5 -3
  134. package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
  135. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
  136. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  137. package/templates/.claude/skills/add-page/references/page-types.md +85 -72
  138. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  139. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  140. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  141. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  142. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  143. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  144. package/templates/.claude/skills/check-site/SKILL.md +23 -0
  145. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  146. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  147. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  148. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  149. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  150. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
  151. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  152. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  153. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  154. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  155. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  156. package/templates/.github/workflows/ci.yml +4 -0
  157. package/templates/.prettierignore +1 -0
  158. package/templates/CLAUDE.md +146 -103
  159. package/templates/DESIGN.md +103 -6
  160. package/templates/README.md +63 -43
  161. package/templates/apps/api/.env.example +2 -1
  162. package/templates/apps/api/CLAUDE.md +3 -2
  163. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  164. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  165. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  166. package/templates/apps/api/src/config/index.ts +35 -16
  167. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  168. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  169. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  170. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  171. package/templates/apps/api/src/lib/stripe.ts +9 -0
  172. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  173. package/templates/apps/api/src/middleware/auth.ts +54 -0
  174. package/templates/apps/api/src/openapi/spec.ts +63 -12
  175. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  176. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  177. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  178. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  179. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  180. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  181. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  182. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  183. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  184. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  185. package/templates/apps/api/src/server.ts +35 -0
  186. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  187. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  188. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  189. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  190. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  191. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  192. package/templates/apps/api/src/services/revalidation.ts +60 -15
  193. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  194. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  195. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  196. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  197. package/templates/apps/web/.env.example +13 -0
  198. package/templates/apps/web/CLAUDE.md +22 -28
  199. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  200. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  201. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  202. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  203. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  204. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  205. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  206. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  207. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  208. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  209. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  210. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  211. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  212. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  213. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  214. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  215. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  216. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  217. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  218. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
  219. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  220. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  221. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  222. package/templates/apps/web/app/[locale]/layout.tsx +65 -17
  223. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  224. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  225. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  226. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  227. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  228. package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
  229. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  230. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  232. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  233. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  234. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  235. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  236. package/templates/apps/web/lib/locale-guard.ts +18 -0
  237. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  238. package/templates/apps/web/lib/nav-config.ts +50 -37
  239. package/templates/apps/web/lib/site-brand.tsx +13 -9
  240. package/templates/apps/web/lib/site-identity.ts +14 -0
  241. package/templates/apps/web/lib/site-logo.tsx +36 -0
  242. package/templates/apps/web/lib/site-theme.ts +46 -26
  243. package/templates/apps/web/messages/en.json +45 -5
  244. package/templates/apps/web/middleware.ts +28 -4
  245. package/templates/content/_site.mdx +4 -2
  246. package/templates/content/en/BlogList/blog.mdx +37 -16
  247. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  248. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  249. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  250. package/templates/content/en/BookCall/start-project.mdx +56 -0
  251. package/templates/content/en/ContactPage/contact.mdx +43 -34
  252. package/templates/content/en/HomePage/home.mdx +10 -22
  253. package/templates/content/en/LandingPage/meet.mdx +59 -0
  254. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  255. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  256. package/templates/database/CHANGELOG.md +14 -0
  257. package/templates/database/package.json +1 -1
  258. package/templates/package.json +2 -1
  259. package/templates/tools/check-claude-md.ts +128 -0
  260. package/versions.json +33 -33
  261. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
  262. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  263. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  264. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  265. package/site-templates/minimalism-site/DESIGN.md +0 -162
  266. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
  267. package/site-templates/minimalism-site/ailk-template.json +0 -23
  268. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  269. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
  270. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  271. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  272. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  273. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  274. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  275. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  276. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  277. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  278. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  279. package/site-templates/minimalism-site/provenance.json +0 -4
  280. package/site-templates/simple-service/DESIGN.md +0 -162
  281. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  282. package/site-templates/simple-service/ailk-template.json +0 -11
  283. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  284. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  285. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  286. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  287. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  288. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  289. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  290. package/site-templates/simple-service/provenance.json +0 -4
  291. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  292. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  293. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  294. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  295. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
  296. /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
  297. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  298. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  299. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  300. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  301. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  302. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  303. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  304. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  305. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  306. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  307. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
  308. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
  309. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
  310. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
@@ -3,30 +3,48 @@
3
3
  * @description The signed webhook `apps/api` fires at the site's
4
4
  * `/api/revalidate` on publish and unpublish (#5851, roadmap D6). One POST of
5
5
  * `{ workspaceId, type, slug, locale }` to `CONTENT_REVALIDATE_URL`, signed
6
- * HMAC-SHA256 over the exact body bytes with `CONTENT_REVALIDATE_SECRET`.
6
+ * HMAC-SHA256 with `CONTENT_REVALIDATE_SECRET` over the base string
7
+ * `v0:{timestamp}:{event}:{body}` (#5902) — the Slack shape
8
+ * (`routes/slack/actions.ts:56`) with the event folded in, so neither a replay
9
+ * outside the receiver's 5-minute window nor an in-flight event flip verifies.
10
+ *
11
+ * `type`, `slug` and `locale` are validated against bounded Zod shapes FIRST,
12
+ * before the config check and before any outbound call; an invalid value
13
+ * throws. That is the one way this function rejects, and it is a programmer
14
+ * error rather than a delivery failure: the publish routes already hold these
15
+ * values to the same shapes.
7
16
  *
8
17
  * Either var absent means "revalidation off": one `warn` and return. Both set
9
18
  * means three bounded attempts (500 ms then 2000 ms backoff, 10 s per-attempt
10
19
  * timeout — 32.5 s worst case), every attempt logged, and a final failure
11
- * LOGGED rather than thrown. `notifyRevalidate` never rejects, which is what
12
- * lets the publish path `await` it and still always complete.
20
+ * LOGGED rather than thrown. Delivery never rejects, which is what lets the
21
+ * publish path `await` it and still always complete.
13
22
  *
14
23
  * PII boundary: log lines carry only
15
24
  * `{ event, workspaceId, type, slug, locale, attempt/attempts, status/lastStatus, errName }`.
16
25
  * Never the secret, never the signature, never a response body, never page
17
26
  * content.
18
27
  *
19
- * Nothing calls this yet — the publish/unpublish handlers are a sibling child
20
- * of Epic #5323, and the receiver is Epic #5324. This module fixes the wire
21
- * contract a receiver implements: method POST; the four-key JSON body;
22
- * headers `content-type: application/json`, `x-ailk-revalidate-signature:
23
- * sha256=<lowercase hex>` and `x-ailk-revalidate-event: publish | unpublish`.
24
- * A receiver verifies by recomputing the HMAC over the RAW request body and
25
- * comparing in constant time.
28
+ * The publish and unpublish handlers in `routes/workspaces/content.ts` call
29
+ * this; the receiver is `apps/web/app/api/revalidate/route.ts`. This module
30
+ * fixes the wire contract that receiver implements: method POST; the four-key
31
+ * JSON body; headers `content-type: application/json`,
32
+ * `x-ailk-revalidate-timestamp: <unix seconds>`,
33
+ * `x-ailk-revalidate-event: publish | unpublish` and
34
+ * `x-ailk-revalidate-signature: sha256=<lowercase hex>`. A receiver rejects a
35
+ * timestamp more than 5 minutes from its own clock, then recomputes the HMAC
36
+ * over `v0:{timestamp}:{event}:` + the RAW request body and compares in
37
+ * constant time.
26
38
  */
27
39
  import { createHmac } from "node:crypto";
28
40
 
41
+ import {
42
+ SCHEMA_ORG_TYPES,
43
+ SLUG_PATTERN,
44
+ } from "@working-theory/content-adapters";
29
45
  import { createLogger } from "@working-theory/observability";
46
+ import { localeSchema } from "@working-theory/validation";
47
+ import { z } from "zod";
30
48
 
31
49
  import { config } from "../config/index.js";
32
50
  import { outboundFetch } from "../lib/outbound-http.js";
@@ -35,6 +53,22 @@ const log = createLogger({ service: "revalidation" });
35
53
 
36
54
  const SIGNATURE_HEADER = "x-ailk-revalidate-signature";
37
55
  const EVENT_HEADER = "x-ailk-revalidate-event";
56
+ const TIMESTAMP_HEADER = "x-ailk-revalidate-timestamp";
57
+
58
+ /**
59
+ * The bounds on the three fields that become the site's cache tags
60
+ * (`content:{workspace}:{type}:{slug}:{locale}`). `type` and `slug` are exactly
61
+ * the content routes' `addressParamsSchema` shapes and `locale` is the BCP-47
62
+ * subset `resolveLocale` produces, so the publish path can never trip this
63
+ * after its write has committed. None admits a `:`, which is what stops a
64
+ * crafted `type` colliding with another page's tag (#6005 M3); the receiver
65
+ * applies the same shapes.
66
+ */
67
+ const targetFieldsSchema = z.object({
68
+ type: z.enum(SCHEMA_ORG_TYPES),
69
+ slug: z.string().regex(SLUG_PATTERN),
70
+ locale: localeSchema,
71
+ });
38
72
 
39
73
  /**
40
74
  * Backoff before attempt 2 and attempt 3. Three attempts total, no fourth:
@@ -69,14 +103,21 @@ function isRetryableStatus(status: number): boolean {
69
103
  }
70
104
 
71
105
  /**
72
- * Tells the site to revalidate one piece of content. Resolves in every case —
73
- * success, exhausted retries, or unconfigured — so an `await` in the publish
74
- * path can never fail the publish it follows.
106
+ * Tells the site to revalidate one piece of content. Resolves on success,
107
+ * exhausted retries, or unconfigured, so an `await` in the publish path cannot
108
+ * fail the publish it follows. Rejects only when `type`, `slug` or `locale` is
109
+ * out of bounds, before anything is signed or sent.
75
110
  */
76
111
  export async function notifyRevalidate(
77
112
  target: RevalidateTarget,
78
113
  options: RevalidateOptions,
79
114
  ): Promise<void> {
115
+ targetFieldsSchema.parse({
116
+ type: target.type,
117
+ slug: target.slug,
118
+ locale: target.locale,
119
+ });
120
+
80
121
  const url = config.CONTENT_REVALIDATE_URL;
81
122
  const secret = config.CONTENT_REVALIDATE_SECRET;
82
123
 
@@ -100,8 +141,11 @@ export async function notifyRevalidate(
100
141
  slug: target.slug,
101
142
  locale: target.locale,
102
143
  });
144
+ // Taken once per call and shared by every attempt: the retry schedule's
145
+ // 32.5 s worst case sits well inside the receiver's 5-minute tolerance.
146
+ const timestamp = String(Math.floor(Date.now() / 1000));
103
147
  const signature = createHmac("sha256", secret)
104
- .update(body, "utf8")
148
+ .update(`v0:${timestamp}:${options.event}:${body}`, "utf8")
105
149
  .digest("hex");
106
150
 
107
151
  let attempt = 0;
@@ -117,8 +161,9 @@ export async function notifyRevalidate(
117
161
  method: "POST",
118
162
  headers: {
119
163
  "content-type": "application/json",
120
- [SIGNATURE_HEADER]: `sha256=${signature}`,
164
+ [TIMESTAMP_HEADER]: timestamp,
121
165
  [EVENT_HEADER]: options.event,
166
+ [SIGNATURE_HEADER]: `sha256=${signature}`,
122
167
  },
123
168
  body,
124
169
  // The SSRF gate is waived deliberately and narrowly: this URL comes
@@ -103,7 +103,6 @@ const MCP_ONLY_TOOLS = new Set([
103
103
  "ailkGetPricing", // get_pricing — pure read, no action equivalent (I4)
104
104
  "ailkGetFaq", // get_faq — pure read, no action equivalent (I4)
105
105
  "ailkAudit", // audit — AEO scoring tool, no web action equivalent (I5; ADR 0013)
106
- "ailkSubscribeNewsletter", // subscribe_newsletter — anonymous write via MCP; no Server Action (B7)
107
106
  "ailkRequestCallback", // request_callback — anonymous write via MCP; no Server Action (B7)
108
107
  "ailkCreateProject", // create_project — agent-only project creator; no Server Action equivalent (#299)
109
108
  "ailkGetFlow", // get_flow — agent-callable flow-engine read; no web Server Action (#3956 D7)
@@ -228,6 +227,8 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
228
227
  // ailkSubmitFlowStep): twenty tools + three paired Server Actions.
229
228
  // - ailkPurchase: paired with purchaseAction (apps/web/app/dev/purchase/).
230
229
  // - ailkCaptureLead: paired with captureLeadAction (apps/web/app/contact/).
230
+ // - ailkSubscribeNewsletter: paired with subscribeNewsletterAction
231
+ // (apps/web/app/[locale]/newsletter/newsletter.actions.ts, #6730 D9).
231
232
  // - ailkSubmitFlowStep: paired with submitFlowStepAction
232
233
  // (apps/web/app/[locale]/flows/[slug]/flow.actions.ts, #3957).
233
234
  // - startCheckoutAction, saveRecommendationAction: WEB-ONLY (#4842 D7,
@@ -241,8 +242,8 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
241
242
  // ailkGetLocations, ailkGetOfferings, ailkGetPricing, ailkGetFaq:
242
243
  // MCP-only (pure read; I4; in MCP_ONLY_TOOLS allow-list).
243
244
  // - ailkAudit: MCP-only (AEO scoring; I5; in MCP_ONLY_TOOLS allow-list).
244
- // - ailkSubscribeNewsletter, ailkRequestCallback:
245
- // MCP-only anonymous-write tools (B7; in MCP_ONLY_TOOLS allow-list).
245
+ // - ailkRequestCallback: MCP-only anonymous-write tool (B7; in
246
+ // MCP_ONLY_TOOLS allow-list).
246
247
  // - ailkCreateProject: MCP-only agent project creator (#299; in MCP_ONLY_TOOLS).
247
248
  // - ailkCreatePage, ailkUpdatePage, ailkDeletePage: RETIRED by #5853
248
249
  // (D7). Their HTTP twins — the untenanted /v1/content writes — were
@@ -264,7 +265,7 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
264
265
  // - ailkGetEventMeta: MCP-only meta-only scheduling read, twin of
265
266
  // GET /v1/schedule/event-meta (#4633) — same posture as the other
266
267
  // scheduling tools.
267
- expect(actions.size).toBe(5);
268
+ expect(actions.size).toBe(6);
268
269
  expect(tools.size).toBe(20);
269
270
  expect(r.missingTools).toEqual([]);
270
271
  expect(r.orphanTools).toEqual([]);
@@ -102,6 +102,30 @@ describe("scaffoldOutputSchema", () => {
102
102
  // __tests__/tools → __tests__ → apps/mcp → apps → repo root
103
103
  const REPO_ROOT = resolve(__dirname, "../../../..");
104
104
  const CLI_PATH = join(REPO_ROOT, "packages/create-ailk/dist/cli.js");
105
+ // NO_HAND_IMPLEMENT_OK: trunk fix for a red main (CI run 36386534464), operator-attended session.
106
+ // The CLI substitutes `workspace:*` versions from create-ailk's gitignored
107
+ // `versions.json`, which only `build-templates` writes ("empty before the first
108
+ // build"); with it absent every scaffold throws "no version found for …".
109
+ // Turbo gives this package's test no dependency on create-ailk's build-templates
110
+ // step, so whether the file exists here depends on shard packing. Every case
111
+ // that runs the CLI therefore skips — with a warning, never a failure — until
112
+ // both the CLI and its version map are on disk.
113
+ const VERSIONS_PATH = join(REPO_ROOT, "packages/create-ailk/versions.json");
114
+
115
+ /** True (after warning) when the CLI cannot scaffold in this checkout yet. */
116
+ function skipUnlessCliReady(): boolean {
117
+ if (!existsSync(CLI_PATH)) {
118
+ console.warn("[create_project.test] skipping integration test — CLI not built");
119
+ return true;
120
+ }
121
+ if (!existsSync(VERSIONS_PATH)) {
122
+ console.warn(
123
+ "[create_project.test] skipping integration test — run `pnpm --filter create-ailk run build-templates` first",
124
+ );
125
+ return true;
126
+ }
127
+ return false;
128
+ }
105
129
 
106
130
  describe("ailkCreateProject handler", () => {
107
131
  let scratch: string;
@@ -124,10 +148,7 @@ describe("ailkCreateProject handler", () => {
124
148
  });
125
149
 
126
150
  it("scaffolds at least one file into a fresh tmp dir and reports targetDir", async () => {
127
- if (!existsSync(CLI_PATH)) {
128
- console.warn("[create_project.test] skipping integration test — CLI not built");
129
- return;
130
- }
151
+ if (skipUnlessCliReady()) return;
131
152
  const target = join(scratch, "my-site");
132
153
  const output = await ailkCreateProject(
133
154
  { targetDir: target, projectName: "my-site" },
@@ -138,10 +159,7 @@ describe("ailkCreateProject handler", () => {
138
159
  });
139
160
 
140
161
  it("scaffolds package.json when full templates are built (build-templates required)", async () => {
141
- if (!existsSync(CLI_PATH)) {
142
- console.warn("[create_project.test] skipping integration test — CLI not built");
143
- return;
144
- }
162
+ if (skipUnlessCliReady()) return;
145
163
  const TEMPLATES_DIR = join(REPO_ROOT, "packages/create-ailk/templates");
146
164
  const PKG_JSON_IN_TEMPLATES = join(TEMPLATES_DIR, "package.json");
147
165
  if (!existsSync(PKG_JSON_IN_TEMPLATES)) {
@@ -156,10 +174,7 @@ describe("ailkCreateProject handler", () => {
156
174
  });
157
175
 
158
176
  it("scaffolds app dir when full templates are built (build-templates required)", async () => {
159
- if (!existsSync(CLI_PATH)) {
160
- console.warn("[create_project.test] skipping integration test — CLI not built");
161
- return;
162
- }
177
+ if (skipUnlessCliReady()) return;
163
178
  const TEMPLATES_DIR = join(REPO_ROOT, "packages/create-ailk/templates");
164
179
  const PKG_JSON_IN_TEMPLATES = join(TEMPLATES_DIR, "package.json");
165
180
  if (!existsSync(PKG_JSON_IN_TEMPLATES)) {
@@ -178,10 +193,7 @@ describe("ailkCreateProject handler", () => {
178
193
  });
179
194
 
180
195
  it("returns at least one file (schema-typed page seed or README.md)", async () => {
181
- if (!existsSync(CLI_PATH)) {
182
- console.warn("[create_project.test] skipping integration test — CLI not built");
183
- return;
184
- }
196
+ if (skipUnlessCliReady()) return;
185
197
  const target = join(scratch, "my-site");
186
198
  await ailkCreateProject({ targetDir: target, projectName: "my-site" }, CLI_PATH);
187
199
  // At minimum README.md is always emitted by the template.
@@ -189,10 +201,7 @@ describe("ailkCreateProject handler", () => {
189
201
  });
190
202
 
191
203
  it("returns output conforming to scaffoldOutputSchema", async () => {
192
- if (!existsSync(CLI_PATH)) {
193
- console.warn("[create_project.test] skipping integration test — CLI not built");
194
- return;
195
- }
204
+ if (skipUnlessCliReady()) return;
196
205
  const target = join(scratch, "my-site");
197
206
  const output = await ailkCreateProject(
198
207
  { targetDir: target, projectName: "my-site" },
@@ -203,10 +212,7 @@ describe("ailkCreateProject handler", () => {
203
212
  });
204
213
 
205
214
  it("rejects with a clear error when targetDir is non-empty", async () => {
206
- if (!existsSync(CLI_PATH)) {
207
- console.warn("[create_project.test] skipping integration test — CLI not built");
208
- return;
209
- }
215
+ if (skipUnlessCliReady()) return;
210
216
  const target = join(scratch, "populated");
211
217
  mkdirSync(target);
212
218
  writeFileSync(join(target, "existing.txt"), "data");
@@ -16,6 +16,12 @@
16
16
  * correctly in production and in integration tests.
17
17
  *
18
18
  * Input: { targetDir, projectName, preset? }
19
+ * The tool builds the Free organization-product starter (`--shape
20
+ * organization-product`, #6754 D9); `preset` stays reserved. Always builds
21
+ * the Free module set (`--modules marketing`, #6759 D6, SEC-7): a licence
22
+ * key sitting in the MCP server's own environment never widens what an
23
+ * anonymous caller receives — the explicit keep list takes precedence over
24
+ * any key-derived level probe before the child process reads its env.
19
25
  * targetDir — absolute or relative path to scaffold into; must not exist
20
26
  * or must be empty (enforced by create-ailk ValidationError)
21
27
  * projectName — project name used in README and package.json
@@ -94,12 +100,21 @@ export async function ailkCreateProject(
94
100
  const targetAbs = resolve(input.targetDir);
95
101
 
96
102
  // Spawn the CLI synchronously — no network, no async I/O needed.
97
- // Hard-coded node invocation; only targetDir flows through user input.
98
- const result = spawnSync("node", [bin, targetAbs], {
99
- stdio: ["ignore", "pipe", "pipe"],
100
- shell: false,
101
- encoding: "utf8",
102
- });
103
+ // Hard-coded node invocation; only targetDir flows through user input. The
104
+ // CLI refuses a run naming no shape and no template, so the tool names one.
105
+ // `--modules marketing` is pinned explicitly (#6759 D6, SEC-7): an explicit
106
+ // keep list takes the keep branch and resolves before the child ever reads
107
+ // AILK_LICENSE_KEY, so a licence key in the server's own environment can
108
+ // never turn this anonymous call into the keyed level probe.
109
+ const result = spawnSync(
110
+ "node",
111
+ [bin, targetAbs, "--shape", "organization-product", "--modules", "marketing"],
112
+ {
113
+ stdio: ["ignore", "pipe", "pipe"],
114
+ shell: false,
115
+ encoding: "utf8",
116
+ },
117
+ );
103
118
 
104
119
  if (result.status !== 0) {
105
120
  // Extract the error message from stderr (CLI writes "[create-ailk] <msg>").
@@ -41,8 +41,10 @@
41
41
  * simply not part of that shape. Either path is safe: the payload built
42
42
  * below can never carry a caller-supplied redirect. The shared
43
43
  * `purchaseInputSchema` (and the Server Action / API route that use it) is
44
- * unaffected: those are not anonymous surfaces and keep accepting a
45
- * caller-supplied redirect.
44
+ * unaffected: both keep accepting a caller-supplied redirect. The route's own
45
+ * anonymous exposure is gated only when `OSS_API_INTERNAL_SECRET` is set
46
+ * (`passesInternalAuthGate()` in `apps/api/src/routes/checkout/sessions.ts`);
47
+ * unset — the OSS standalone default — the route accepts anonymous callers.
46
48
  *
47
49
  * The file stays in the BASE tree and is never added to
48
50
  * `MODULE_SLICES.commerce.ownedFiles`: `../server.js` statically imports
@@ -52,6 +52,19 @@ CONTENT_ADAPTER=mdx
52
52
  # browser never sees.
53
53
  # NEXT_PUBLIC_AILK_API_URL=http://localhost:3002
54
54
 
55
+ # =============================================================================
56
+ # apps/api internal-auth secret (server-side only) — #6876
57
+ # =============================================================================
58
+ # SERVER-SIDE ONLY — never rename it with a NEXT_PUBLIC_ prefix. The /dev/purchase
59
+ # Server Action sends it as X-Internal-Auth on POST /v1/checkout/sessions,
60
+ # alongside the buyer's session cookie, so the route can stamp a signed-in
61
+ # buyer's Checkout. Must equal OSS_API_INTERNAL_SECRET in apps/api's
62
+ # environment. Unset on both sides for OSS standalone deployments: the API
63
+ # applies no gate and this app sends no header. Set on the API but not here,
64
+ # and the purchase fails with the route's 401.
65
+ # Generate with: openssl rand -hex 32 (see apps/api/.env.example)
66
+ # OSS_API_INTERNAL_SECRET=your-shared-secret-here
67
+
55
68
  # =============================================================================
56
69
  # Better Auth (apps/web)
57
70
  # =============================================================================
@@ -43,7 +43,7 @@ Raw HTML is allowed only when no `@working-theory/ui` component fits. Flag it in
43
43
  - Animations: only `transform` and `opacity`. Never `transition-all`.
44
44
  - Every interactive element needs hover, focus-visible, and active states.
45
45
 
46
- For the full responsive contract (path-scoped to UI files), see `.claude/rules/responsive.md`. For brand-asset discovery (logos, colors, photos), see `.claude/rules/brand.md`.
46
+ The four widths above are the responsive contract. Brand reference files (logos, photos, colour and type notes) live in `brand_assets/` at the repo root; the site's own brand colours and fonts are set in `content/_site.mdx` under `brand.colors` and `brand.fonts`.
47
47
 
48
48
  ## Local dev
49
49
 
@@ -110,33 +110,27 @@ function — the page under test throws instead of taking a branch. Declare
110
110
  every predicate the pages call, binding the two reset ones to the same
111
111
  `jest.fn()` when the case does not need them to disagree.
112
112
 
113
- ## Conversational intake (contact page)
114
-
115
- The contact page (`apps/web/app/[locale]/contact/`) consumes
116
- `ConversationalIntake` — a chat-style intake block + deterministic engine
117
- from `@working-theory/ui`, unmodified. The block/engine contract (six-behavior
118
- mapping, `NextTurn`/`IntakeScript` shapes, the v1/v2 seam) is documented in
119
- `packages/ui/src/blocks/conversational-intake/README.md`; read that first.
120
-
121
- This app's specific wiring: `ConversationalIntakeWrapper.tsx` hosts the
122
- block and owns submission + fallback; `intake-script.ts` supplies the script
123
- via `buildContactIntakeScript(copy)`, whose question ids (`name` / `message`
124
- / `email`) double as the field-mapping contract into `submitLeadInputSchema`;
125
- `intake-analytics.ts` fires the `"intake-turn-abandonment"` event once per
126
- turn (turn index + status only, never answer text) from the block's
127
- `getThinkingDelayMs` override seam. Completion submits through the existing
128
- `captureLeadAction`; Skip or a failed submit falls back to the existing
129
- `<ContactForm/>`, prefilled with whatever was already captured — a lead is
130
- never silently lost.
131
-
132
- **Note:** the contact page currently mounts the wrapper alongside
133
- `renderPage`'s output (`page.tsx`), not as a page-level swap. A downstream
134
- Positionary consumer pattern that DID swap the whole page was corrected
135
- under #4577 (decision D71 — the intake belongs behind the hero CTA's
136
- lightbox gate; landed in PR #4583 via `packages/ui/src/sections/FormLightbox.tsx`
137
- and `HeroIntakeContext.tsx`, spec `docs/specs/4577-intake-lightbox-correction-spec.md`).
138
- Don't take a whole-page mount as the pattern to replicate; the lightbox-gate
139
- seam is the canonical embedding shape.
113
+ ## Contact page
114
+
115
+ The contact page (`apps/web/app/[locale]/contact/page.tsx`) renders the
116
+ page's sections through `renderPage` and places ONE contact surface: into the
117
+ page's first `contact-form` section, else into a leading split hero's right
118
+ column, else after the page. That surface is the shared `LeadCaptureForm`
119
+ block from `@working-theory/ui`, bound to `captureLeadAction` — unless the
120
+ API reports this deployment accepts no contact message (production with
121
+ neither a database nor an email key), in which case it is `ContactFallback`:
122
+ the site's contact email, never a form that would report a message as sent
123
+ when it reached nobody. A failed status read counts as accepting, so the
124
+ form renders and its submit reports honestly.
125
+
126
+ The chat-style intake is not mounted by default.
127
+ `ConversationalIntakeWrapper.tsx` remains for a site that prefers it: it hosts
128
+ `@working-theory/ui`'s `ConversationalIntake` block (contract in
129
+ `packages/ui/src/blocks/conversational-intake/README.md`), submits through the
130
+ same `captureLeadAction`, and on Skip or a failed submit swaps to
131
+ `LeadCaptureForm`, prefilled with what was already captured. The canonical
132
+ embedding for it is behind the hero CTA's lightbox gate
133
+ (`packages/ui/src/sections/FormLightbox.tsx`, #4577), not a whole-page mount.
140
134
 
141
135
  ## AEO & performance
142
136
 
@@ -24,7 +24,7 @@ import { headers } from "next/headers";
24
24
  import { getTranslations } from "next-intl/server";
25
25
  import type { ReactElement, ReactNode } from "react";
26
26
 
27
- import { siteLogo } from "../../../lib/site-brand";
27
+ import { siteLogo } from "../../../lib/site-logo";
28
28
 
29
29
  interface Props {
30
30
  children: ReactNode;
@@ -10,6 +10,10 @@
10
10
  * 4. Renders an EmptyState — not a headerless table — for an empty page.
11
11
  * 5. Renders ForbiddenState on a 401/403; falls back to the first page
12
12
  * when a supplied `?cursor=` 400s; rethrows every other error.
13
+ * 6. With no session, renders ForbiddenState and makes no apps/api request
14
+ * — the (authed) layout's gate renders concurrently, so the page must
15
+ * not spend (or fail) an API call on a visitor it will never serve
16
+ * (#6865).
13
17
  */
14
18
  import { render, screen } from "@testing-library/react";
15
19
 
@@ -26,6 +30,13 @@ jest.mock("next/headers", () => ({
26
30
  cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
27
31
  }));
28
32
 
33
+ const mockGetSession = jest.fn();
34
+ // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts's lazy Better-Auth singleton has no HTTP/DB boundary a unit test can mock without standing up Prisma.
35
+ jest.mock("../../../../../auth", () => ({
36
+ __esModule: true,
37
+ auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
38
+ }));
39
+
29
40
  const mockList = jest.fn();
30
41
  jest.mock("@working-theory/api-client", () => ({
31
42
  __esModule: true,
@@ -64,6 +75,19 @@ function renderPage(cursor?: string) {
64
75
  describe("LeadsInboxPage", () => {
65
76
  beforeEach(() => {
66
77
  mockList.mockReset();
78
+ mockGetSession.mockReset();
79
+ mockGetSession.mockResolvedValue({ user: { id: "user_1" } });
80
+ });
81
+
82
+ // #6865: the (authed) layout renders SignInGate to an anonymous visitor,
83
+ // but Next renders this page concurrently — it must not call apps/api.
84
+ it("makes no apps/api request and renders ForbiddenState with no session", async () => {
85
+ mockGetSession.mockResolvedValue(null);
86
+ render(await renderPage("lead_9"));
87
+
88
+ expect(mockList).not.toHaveBeenCalled();
89
+ expect(screen.getByTestId("waitlist-forbidden")).toBeInTheDocument();
90
+ expect(screen.queryByTestId("leads-inbox-page")).not.toBeInTheDocument();
67
91
  });
68
92
 
69
93
  it("renders one row per lead, with email and source", async () => {
@@ -12,15 +12,24 @@
12
12
  * waitlist comparison page uses); every other error (a 400 on a malformed
13
13
  * cursor, a 500) rethrows to the route's error boundary rather than being
14
14
  * swallowed.
15
+ *
16
+ * The page reads the session itself before any API call (#6865): Next renders
17
+ * it concurrently with the (authed) layout, so for an anonymous visitor the
18
+ * layout's `SignInGate` wins but this page would still have called
19
+ * `leads.list()` — spending (or, with no API reachable, failing) a request
20
+ * for a result nobody sees. No session renders `ForbiddenState` with no
21
+ * apps/api request, the same guard `../account/page.tsx` carries.
15
22
  */
16
23
 
17
24
  import { leads, type LeadInboxPage } from "@working-theory/api-client";
18
25
  import { PageHeader } from "@working-theory/ui/blocks";
19
26
  import { Container, Heading, Text } from "@working-theory/ui/primitives";
20
27
  import type { Metadata } from "next";
28
+ import { headers } from "next/headers";
21
29
  import { getTranslations } from "next-intl/server";
22
30
  import type { ReactElement } from "react";
23
31
 
32
+ import { auth } from "../../../../auth";
24
33
  import { ApiError, forwardedCookieInit, unwrapApiResult } from "../../../../lib/server-api";
25
34
  import { ForbiddenState } from "../waitlist/components/ForbiddenState";
26
35
  import { formatDateTime } from "../waitlist/format";
@@ -46,6 +55,13 @@ export default async function LeadsInboxPage({
46
55
  const { cursor } = await searchParams;
47
56
  const t = await getTranslations({ locale, namespace: "leadsInbox" });
48
57
 
58
+ const forbidden = (
59
+ <ForbiddenState headline={t("forbiddenHeadline")} description={t("forbiddenDescription")} />
60
+ );
61
+
62
+ const session = await auth.api.getSession({ headers: await headers() });
63
+ if (!session) return forbidden;
64
+
49
65
  const init = await forwardedCookieInit();
50
66
 
51
67
  let page: LeadInboxPage;
@@ -53,12 +69,7 @@ export default async function LeadsInboxPage({
53
69
  page = await unwrapApiResult(leads.list(cursor ? { cursor } : {}, init));
54
70
  } catch (err) {
55
71
  if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
56
- return (
57
- <ForbiddenState
58
- headline={t("forbiddenHeadline")}
59
- description={t("forbiddenDescription")}
60
- />
61
- );
72
+ return forbidden;
62
73
  }
63
74
  // A 400 means the `?cursor=` in the URL did not resolve — it was typed,
64
75
  // or it is a stale next-page link whose row has since gone. That is a bad
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Tests for the `[slug]/[detail]` route — the Free `/meet/<slug>` placeholder
3
+ * detail pages (#6897 D2).
4
+ *
5
+ * - `slug !== "meet"` → notFound() immediately, never reaching getMdxSource.
6
+ * - `slug === "meet"` and a BookCall source exists at `detail` → renders
7
+ * through renderPage sections mode, no notFound() call.
8
+ * - `slug === "meet"` and no BookCall source exists at `detail` → renderPage's
9
+ * own detail-route branch (no translationsNamespace passed) calls
10
+ * notFound().
11
+ */
12
+ import React from "react";
13
+ import { render } from "@testing-library/react";
14
+
15
+ jest.mock("next/navigation", () => ({
16
+ __esModule: true,
17
+ notFound: jest.fn(() => {
18
+ throw new Error("NEXT_NOT_FOUND");
19
+ }),
20
+ }));
21
+
22
+ jest.mock("@working-theory/ui", () => ({
23
+ Heading: ({
24
+ children,
25
+ level,
26
+ }: {
27
+ children: React.ReactNode;
28
+ level: number;
29
+ }): React.ReactElement => React.createElement(`h${level}`, null, children),
30
+ SectionRenderer: ({ sections }: { sections: unknown[] }) =>
31
+ React.createElement("div", {
32
+ "data-testid": "section-renderer",
33
+ "data-section-count": String(sections.length),
34
+ }),
35
+ }));
36
+
37
+ import { notFound } from "next/navigation";
38
+
39
+ import DetailPage from "../page";
40
+
41
+ const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
42
+
43
+ function makeSource(slug: string) {
44
+ return {
45
+ frontmatter: {
46
+ type: "BookCall" as const,
47
+ slug,
48
+ title: `Title for ${slug}`,
49
+ description: `Description for ${slug}`,
50
+ sections: [
51
+ {
52
+ id: "hero",
53
+ type: "hero" as const,
54
+ data: { headline: `Title for ${slug}` },
55
+ },
56
+ ],
57
+ },
58
+ body: "",
59
+ };
60
+ }
61
+
62
+ describe("[slug]/[detail] — the Free /meet/<slug> placeholder route (#6897 D2)", () => {
63
+ beforeEach(() => {
64
+ mockNotFound.mockClear();
65
+ });
66
+
67
+ it("renders a BookCall seed at meet/<detail> through renderPage sections mode", async () => {
68
+ const getMdxSource = jest.fn().mockResolvedValue(makeSource("book-a-call"));
69
+
70
+ const element = await DetailPage({
71
+ params: Promise.resolve({ locale: "en", slug: "meet", detail: "book-a-call" }),
72
+ getMdxSource,
73
+ });
74
+ const { container } = render(element);
75
+
76
+ expect(getMdxSource).toHaveBeenCalledWith("BookCall", "book-a-call", "en");
77
+ expect(
78
+ container.querySelector("[data-testid='section-renderer']"),
79
+ ).not.toBeNull();
80
+ expect(
81
+ container.querySelector("script[type='application/ld+json']"),
82
+ ).not.toBeNull();
83
+ expect(mockNotFound).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it("404s for another hub — the guard fires before getMdxSource is ever called", async () => {
87
+ const getMdxSource = jest.fn().mockResolvedValue(makeSource("book-a-call"));
88
+
89
+ await expect(
90
+ DetailPage({
91
+ params: Promise.resolve({ locale: "en", slug: "not-meet", detail: "book-a-call" }),
92
+ getMdxSource,
93
+ }),
94
+ ).rejects.toThrow("NEXT_NOT_FOUND");
95
+ expect(mockNotFound).toHaveBeenCalledTimes(1);
96
+ expect(getMdxSource).not.toHaveBeenCalled();
97
+ });
98
+
99
+ it("404s for an unknown detail slug under the meet hub", async () => {
100
+ const getMdxSource = jest.fn().mockResolvedValue(null);
101
+
102
+ await expect(
103
+ DetailPage({
104
+ params: Promise.resolve({ locale: "en", slug: "meet", detail: "not-a-page" }),
105
+ getMdxSource,
106
+ }),
107
+ ).rejects.toThrow("NEXT_NOT_FOUND");
108
+ expect(mockNotFound).toHaveBeenCalledTimes(1);
109
+ });
110
+ });