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
@@ -29,7 +29,7 @@ Open <http://localhost:3000>. The home page renders from `content/en/HomePage/ho
29
29
  To verify everything is wired before committing or deploying:
30
30
 
31
31
  ```bash
32
- pnpm preflight # architecture + types + lint + build
32
+ pnpm preflight # architecture + CLAUDE.md drift + types + lint + build
33
33
  pnpm build # production build
34
34
  ```
35
35
 
@@ -39,38 +39,41 @@ pnpm build # production build
39
39
 
40
40
  The `content/` directory is locale-prefixed: every page lives at `content/<locale>/<type>/<slug>.mdx`. The default locale is `en` (English); the scaffold ships English content only. Each subdirectory under `content/en/` is a schema-typed page surface, and the filename is the slug.
41
41
 
42
- The pages of the Free template, and where each one's copy lives:
42
+ The pages of the Free `organization-product` shape, and where each one's copy lives:
43
43
 
44
44
  <!-- generated:begin content -->
45
45
 
46
- | Page | Content file | Ships in |
47
- | ------------------- | --------------------------------------------------- | -------------------------------- |
48
- | `/` | `content/en/HomePage/home.mdx` | every scaffold |
49
- | `/software` | — (a code route) | every scaffold |
50
- | `/software/[slug]` | — (a code route) | every scaffold |
51
- | `/use-cases` | `content/en/UseCasesList/use-cases.mdx` | `--template marketing-site-free` |
52
- | `/use-cases/[slug]` | — (a code route) | `--template marketing-site-free` |
53
- | `/pricing` | `content/en/Pricing/pricing.mdx` | `--template marketing-site-free` |
54
- | `/about` | `content/en/AboutPage/about.mdx` | every scaffold |
55
- | `/faq` | `content/en/FAQPage/faq.mdx` | every scaffold |
56
- | `/contact` | `content/en/ContactPage/contact.mdx` | every scaffold |
57
- | `/glossary` | `content/en/GlossaryList/glossary.mdx` | `--template marketing-site-free` |
58
- | `/newsletter` | `content/en/NewsletterSignup/newsletter.mdx` | `--template marketing-site-free` |
59
- | `/book-a-call` | `content/en/BookCall/book-call.mdx` | `--template marketing-site-free` |
60
- | `/sign-in` | — (a code route) | every scaffold |
61
- | `/sign-up` | — (a code route) | every scaffold |
62
- | `/privacy` | `content/en/PrivacyPage/privacy.mdx` | `--template marketing-site-free` |
63
- | `/terms` | `content/en/TermsPage/terms.mdx` | `--template marketing-site-free` |
64
- | `/purchase-policy` | `content/en/PurchasePolicyPage/purchase-policy.mdx` | every scaffold |
65
- | `/blog` | `content/en/BlogList/blog.mdx` | every scaffold |
46
+ | Page | Content file | Ships in |
47
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
48
+ | `/` | `content/en/HomePage/home.mdx` | every scaffold |
49
+ | `/features` | `content/en/FeaturesList/features.mdx` | `--shape organization-product` |
50
+ | `/features/[slug]` | — (a code route) | `--shape organization-product` |
51
+ | `/for` | `content/en/UseCasesList/for.mdx` | `--shape organization-product` |
52
+ | `/for/[slug]` | — (a code route) | `--shape organization-product` |
53
+ | `/pricing` | `content/en/Pricing/pricing.mdx` | `--shape organization-product` |
54
+ | `/about` | `content/en/AboutPage/about.mdx` | every scaffold |
55
+ | `/faq` | `content/en/FAQPage/faq.mdx` | every scaffold |
56
+ | `/contact` | `content/en/ContactPage/contact.mdx` | every scaffold |
57
+ | `/glossary` | `content/en/GlossaryList/glossary.mdx` | `--shape organization-product` |
58
+ | `/newsletter` | `content/en/NewsletterSignup/newsletter.mdx` | `--shape organization-product` |
59
+ | `/sign-in` | — (a code route) | every scaffold |
60
+ | `/sign-up` | — (a code route) | every scaffold |
61
+ | `/privacy` | `content/en/PrivacyPage/privacy.mdx` | `--shape organization-product` |
62
+ | `/terms` | `content/en/TermsPage/terms.mdx` | `--shape organization-product` |
63
+ | `/purchase-policy` | `content/en/PurchasePolicyPage/purchase-policy.mdx` | every scaffold |
64
+ | `/blog` | `content/en/BlogList/blog.mdx` | every scaffold |
65
+ | `/meet` | `content/en/LandingPage/meet.mdx` | every scaffold |
66
+ | `/meet/[slug]` | `content/en/BookCall/book-a-call.mdx`, `content/en/BookCall/onsite-estimate.mdx`, `content/en/BookCall/start-project.mdx`, `content/en/BookCall/onsite-service.mdx` | every scaffold |
67
+ | `/portfolio` | `content/en/PortfolioList/portfolio.mdx` | `--shape organization-product` |
68
+ | `/portfolio/[slug]` | — (a code route) | `--shape organization-product` |
66
69
 
67
70
  <!-- generated:end -->
68
71
 
69
- A page marked `--template marketing-site-free` is in your site when you scaffolded with that template.
72
+ A page marked `--shape organization-product` is in your site when you scaffolded that shape, or an alias of it such as `--template marketing-site-free`. Another shape has its own page set; your site's roster of record is `.ailk-site-manifest.json` at its root.
70
73
 
71
74
  To customize the home page, edit [`content/en/HomePage/home.mdx`](./content/en/HomePage/home.mdx). Save and reload the dev server. No build step.
72
75
 
73
- To add a new page, ask Claude Code to run the `add-page` skill — it writes the route, the MDX content stub and the page's typed JSON-LD in one confirm-before-write pass. See [Skills](#skills).
76
+ To add a new page, ask Claude Code to run the `add-page` skill — it writes the page's content file and nav link in one confirm-before-write pass, and adds a missing route with `create-ailk add-route` first. See [Skills](#skills).
74
77
 
75
78
  > `content/_site.mdx` stays at the top level (not under `content/en/`) — site identity is locale-blind.
76
79
 
@@ -82,13 +85,17 @@ Everything that makes this site yours lives in three places. Nothing in `apps/*`
82
85
  | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
83
86
  | Site name, canonical URL, default locale | `content/_site.mdx` — `siteName`, `baseUrl`, `defaultLocale` |
84
87
  | Who the site is (the entity answer engines read) | `content/_site.mdx` — `siteIdentity` (`kind`, `name`, `url`, `logo`, `description`, `sameAs`) |
85
- | Colours, light and dark | `content/_site.mdx` — `theme.colors.light` / `theme.colors.dark` (`bg`, `text`, `primary`, `accent`, …) |
88
+ | Your brand colours, light and dark | `content/_site.mdx` — `brand.colors.light` / `brand.colors.dark` (`primary`, `primaryFg`, `accent`) |
89
+ | Your fonts | `content/_site.mdx` — `brand.fonts` (`display`, `body`) |
90
+ | The design's own colours | `content/_site.mdx` — `theme.colors.light` / `theme.colors.dark` (`bg`, `text`, `primary`, `accent`, …) |
86
91
  | Corners, section-label case, image placeholders, check marks | `content/_site.mdx` — `theme.corners`, `theme.eyebrowCase`, `theme.placeholders`, `theme.checkmarks` |
87
92
  | Every page's copy and section order | `content/en/<Type>/<slug>.mdx` — the frontmatter `sections[]`; each section's `data` is its copy and its layout options |
88
93
  | Header links, footer columns, social links | `apps/web/lib/nav-config.ts` — generated at scaffold, yours to edit; labels come from `apps/web/messages/<locale>.json` |
89
94
  | Logo and brand assets | `apps/web/public/` and the `logo` field in `siteIdentity` |
90
95
 
91
- The `theme` block is the whole design surface. Declare only what you change — an absent colour role or shape knob keeps the platform default — and the build turns it into the CSS custom properties every component reads. There is no token file to fork and no provider to wrap. The [theme and brand guide](https://ailaunchkit.ai/docs/guides/customize-theme-and-brand) is where the full walkthrough will live.
96
+ Two blocks carry the look. `theme:` is the design's — its colours and shape knobs; `brand:` is yours — the logomark, `brand.colors` and `brand.fonts`, set on top of whatever design the site wears. Declare only what you change — an absent colour role or shape knob keeps the default — and the build turns both into the CSS custom properties every component reads. There is no token file to fork and no provider to wrap.
97
+
98
+ To change the whole look, apply another design: `pnpm create ailk@latest apply-design <name|path>` in the site root, with a shipped design name (`standard`, `minimalism`) or a folder holding a `DESIGN.md`. It rewrites `theme:`, the generated `apps/web/lib/site-chrome.ts` and the look fields of your sections; it never touches your routes, your copy or the `brand:` block. That is why your own colours belong in `brand.colors`, not `theme.colors`: applying a design replaces `theme:` wholesale. A design other than `standard` needs a Pro or Pro Plus key. The [theme and brand guide](https://ailaunchkit.ai/docs/guides/customize-theme-and-brand) is where the full walkthrough will live.
92
99
 
93
100
  A rebrand touches only the files in this table. If you find yourself editing a component, stop: the trait you want is either a section option (check the section's `data` fields), a theme knob, or a feature request for the [upstream library](https://github.com/working-theory-labs/ai-launch-kit).
94
101
 
@@ -124,12 +131,12 @@ Set `AILK_PUBLIC_DOMAIN` to your own domain. `STEALTH=true` keeps a fresh deploy
124
131
 
125
132
  You need Postgres once you turn on one of these:
126
133
 
127
- | Surface | What it stores |
128
- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
129
- | **Auth** — `/sign-in`, `/sign-up`, `/account` | users, sessions, accounts |
130
- | **Lead inbox** — `/leads` | submitted leads, for you to review signed in. Lead capture itself always appends to a local JSONL file (`LEAD_CAPTURE_FILE`) and needs nothing. |
131
- | **Checkout returns** — `/purchase/success`, `/purchase/cancel` | the paid checkouts the Stripe webhook records. These pages serve only on a site licensed for commerce. |
132
- | **`CONTENT_ADAPTER=database`** | pages authored in the Artifact store rather than in `content/*.mdx` |
134
+ | Surface | What it stores |
135
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | **Auth** — `/sign-in`, `/sign-up`, `/account` | users, sessions, accounts |
137
+ | **Lead inbox** — `/leads` | submitted leads, for you to review signed in, and newsletter signups. Without a database, lead capture emails each contact-form message to you instead (`RESEND_API_KEY` + `AILK_LEAD_NOTIFICATION_TO` + `AILK_LEAD_NOTIFICATION_FROM` on the API); with neither in production, `/contact` shows your `_site.mdx` `contactEmail` instead of the form. The `setup-database` skill walks you from no database to a working inbox. |
138
+ | **Checkout returns** — `/purchase/success`, `/purchase/cancel` | the paid checkouts the Stripe webhook records. These pages serve only on a site licensed for commerce. |
139
+ | **`CONTENT_ADAPTER=database`** | pages authored in the Artifact store rather than in `content/*.mdx` |
133
140
 
134
141
  ### One command to get one
135
142
 
@@ -162,13 +169,24 @@ The [deploy guide](https://ailaunchkit.ai/docs/guides/deploy-to-production) is w
162
169
 
163
170
  Paid levels and the commerce add-on are bought at <https://ailaunchkit.ai/pricing>; your licence key arrives by email.
164
171
 
165
- Today the key is used at scaffold time. Set `AILK_LICENSE_KEY` and scaffold the paid template with the modules it needs:
172
+ For a new site, set `AILK_LICENSE_KEY` and scaffold with the shape you want — the key sets the level and switches on its modules:
173
+
174
+ ```bash
175
+ # Free — no key
176
+ pnpm create ailk@latest <dir> --shape <shape>
177
+ # Paid — the key sets the level and switches on its modules
178
+ AILK_LICENSE_KEY=<your key> pnpm create ailk@latest <dir> --shape <shape>
179
+ ```
180
+
181
+ `<shape>` is one of `organization-product`, `organization-service`, `local-business-service`, `person-service`. `--modules` trims the level's modules, or adds the commerce add-on or the workspace module. `--design minimalism` picks the other look, and `--design <folder>` renders the starter in a design of your own (a folder holding a `DESIGN.md`); either needs a paid key.
182
+
183
+ To add a paid module to this site in place, set the key and run `add-module` in the site root:
166
184
 
167
185
  ```bash
168
- AILK_LICENSE_KEY=<your key> pnpm create ailk@latest <dir> --template <paid-template> --modules <modules>
186
+ AILK_LICENSE_KEY=<your key> pnpm create ailk@latest add-module <module>
169
187
  ```
170
188
 
171
- Applying a paid module to an existing site in place is not yet supported. To move this site onto a paid template, scaffold the new one and bring your `content/` directory and `content/_site.mdx` across.
189
+ It writes the module's files the site lacks and never overwrites one you have: a file whose content differs is left as-is and listed for review. It patches only `package.json` dependency and script keys, `next.config.*` and `jest.config.*`, then regenerates `architecture.yaml`. `pnpm create ailk@latest license set <key>` (or `license set -` to read the key from stdin, out of your shell history) keeps the key with the site — in `ailk.key` and `apps/api/.env`, both gitignored — and, when the site is linked to Vercel, sets it there and redeploys.
172
190
 
173
191
  ## Upgrading
174
192
 
@@ -186,14 +204,16 @@ This project ships Claude Code skills in `.claude/skills/`. Claude Code discover
186
204
 
187
205
  <!-- generated:begin skills -->
188
206
 
189
- | Skill | What it does |
190
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
191
- | `add-page` | Generate a new page: App Router route + MDX content stub + typed JSON-LD, in one confirm-before-write pass. Also documents the `schema:` frontmatter override for adding supplementary structured data to a page that already exists. |
192
- | `add-auth` | Activate auth providers — magic-link via Resend, Google OAuth, GitHub OAuth. |
193
- | `setup-deploy` | Emit deploy configuration — `vercel.json`, `.env.example`, and `RUNBOOK.md` — for a Vercel deploy. |
194
- | `check-site` | Run `ailk-audit --json` and explain the score and findings in plain language. |
195
- | `fill-page` | Fill one page's section content from a category profile: labelled sample content (tier 1) or your own first draft from a short intake (tier 2). |
196
- | `upgrade-plan` | Compare Free, Pro, Pro Plus, Agency and the commerce add-on side by side, with the prices the public pricing page publishes right now, and recommend one option for this site with a reason. |
207
+ | Skill | What it does |
208
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
209
+ | `add-page` | Add a page this site can serve — its content file, structured data and nav link — adding a missing route with `create-ailk add-route` first. Also documents the `schema:` frontmatter override for any page. |
210
+ | `update-page` | Rename a page — give it a new URL and/or a new title — in one plan-then-confirm pass: its content files, its own route folder, exact internal links and the manifest row move together, and the old URL gets a permanent redirect. |
211
+ | `add-auth` | Activate auth providers — magic-link via Resend, Google OAuth, GitHub OAuth. |
212
+ | `setup-deploy` | Emit deploy configuration — `vercel.json`, `.env.example`, and `RUNBOOK.md` — for a Vercel deploy. Asks where leads go (email or a database) and warns when neither is set. |
213
+ | `setup-database` | Take the site from no database to a working lead inbox: provision Postgres, create the tables, pin the owner, and verify one test message lands in `/leads`. |
214
+ | `check-site` | Run `ailk-audit --json` and explain the score and findings in plain language. |
215
+ | `fill-page` | Fill one page's section content from a category profile: labelled sample content (tier 1) or your own first draft from a short intake (tier 2). |
216
+ | `upgrade-plan` | Compare Free, Pro, Pro Plus, Agency and the commerce add-on side by side, with the prices the public pricing page publishes right now, and recommend one option for this site with a reason. |
197
217
 
198
218
  <!-- generated:end -->
199
219
 
@@ -36,7 +36,8 @@ STRIPE_WEBHOOK_SECRET=whsec_xxx
36
36
  # =============================================================================
37
37
  # When set, POST /v1/checkout/sessions requires the X-Internal-Auth header
38
38
  # to match this value. Unset for OSS standalone deployments (no gate applied).
39
- # Must match OSS_API_INTERNAL_SECRET in apps-paid/marketing-api/.env.local.
39
+ # Must match OSS_API_INTERNAL_SECRET in apps-paid/marketing-api/.env.local
40
+ # and in apps/web's environment (the /dev/purchase Server Action, #6876).
40
41
  # Generate with: openssl rand -hex 32
41
42
  # OSS_API_INTERNAL_SECRET=your-shared-secret-here
42
43
 
@@ -36,8 +36,9 @@ Every endpoint here has a 1:1 MCP tool in `apps/mcp`. When you add, change, or r
36
36
 
37
37
  Routes are versioned under `/v1/*`:
38
38
 
39
- - `/v1/leads/{submit,subscribe,callback}` — lead capture, newsletter subscription, callback request
40
- - `/v1/leads` (GET, `?cursor=&limit=`) — the leads INBOX read (#5678): the caller's OWN sites' leads, newest first, cursor-paginated (`limit` defaults to 50, max 200; `(createdAt desc, id desc)`; `cursor` is the previous page's last row id, and one that does not resolve within the caller's scope is a 400, never a silent page 1 — the API keeps that contract, and the web inbox deliberately softens it on its own side by re-reading page one when a URL-supplied cursor 400s, which is a rendering choice and not a change to this response). Five fields per row and nothing more — `id`, `name` (lifted from `payload.name` when the site's form captured one), `email`, `source`, `createdAt`; `payload`'s other keys, `siteId`, `promotedUserId` and `promotedAt` are never selected (`src/services/leads-inbox.ts` is the only read path). Registered in `authScope` only (auth, no tenant guard) and — alone in that scope — **unconditionally**, with no `isModuleEnabled()` gate: lead capture is a never-gate surface at every level, so reading a lead back cannot be gated either. Identity is `request.userId`; there is no site, org or workspace parameter, so the scope cannot be widened from the wire. It is resolved from the caller's own memberships by `src/services/owner-workspaces.ts`'s `resolveOwnerWorkspaces` — the ONE site→workspace resolution in this app, shared with `lead-promotion.ts` rather than written a second time — and every read filters `siteId ∈ workspaceIds`. Role floor `owner|admin` (contact PII), the same floor and the same reason as `/v1/waitlist-experiments/*`; the scope is the UNION of every workspace the caller owns or administers, not one active tenant, because this route registers outside the tenant scope. **Two deployment preconditions decide whether it returns anything, and both fail EMPTY rather than wide:** a build with no `multi-tenant` module resolves no workspaces (a deliberate scope limit since #5677, not an absence of anything to resolve), and the `siteId` the leads were WRITTEN under must equal a workspace id. That `siteId` has TWO sources, and `currentSiteId()` is not the one on the primary path: the three POST capture routes read it from the REQUEST BODY (`routes/leads/index.ts`), which the capture sections declare in content and default to `"default"` (`ContactForm`/`Newsletter`/`StepFlow`, `ext.siteId ?? "default"`), while the recommender's capture path (`routes/flows/recommender.ts`) is the one that uses `currentSiteId()` (`lib/site.ts`, `AILK_SITE_ID ?? "default"`). A workspace id is `wksp_<uuidv7>`, so `siteId ∈ workspaceIds` matches only when the content-declared value — and, for recommender leads, `AILK_SITE_ID` — is set to it (the same convention `waitlist-dashboard.ts` already relies on). The rendered `name` is lifted from `payload.name` and truncated at 200 characters: the write side bounds `payload` at 64 keys and no per-value length, and `POST /v1/leads/submit` is anonymous with no ownership check on `siteId`. No MCP twin — identity-bound (#3591 exception, the same shape as `/v1/workspaces/*` and `/v1/billing/*`).
39
+ - `/v1/leads/{submit,subscribe,callback}` — lead capture, newsletter subscription, callback request. Where a lead goes is `src/lib/lead-delivery.ts` (#6730): `DATABASE_URL` set → the database (the inbox); no database but `RESEND_API_KEY` + `AILK_LEAD_NOTIFICATION_TO` + `AILK_LEAD_NOTIFICATION_FROM` → the owner email IS the delivery (`persisted: "email"`, a refused send is `503 delivery_failed`); neither outside production → the local file fallback; neither in production → `503 lead_capture_not_configured` before any write. A newsletter signup needs a database (or the local file). A lead is never reported captured when nothing stored or delivered it.
40
+ - `/v1/leads/capture` (GET) — `{ ok: true, data: { contact, newsletter } }`, two booleans saying whether a submit / a subscribe would be accepted (#6730). The web contact page reads it to show the form or the site's contact email. Never names the delivery mode, an address or an env var. Registered at the top level, outside `leadsScope`'s strict per-IP limit. No MCP twin — a recorded exception on the same footing as the anonymous form-lifecycle routes: an agent needs no preflight, because the three lead-write tools already return the 503 when capture is not accepted.
41
+ - `/v1/leads` (GET, `?cursor=&limit=`) — the leads INBOX read (#5678): the caller's OWN sites' leads, newest first, cursor-paginated (`limit` defaults to 50, max 200; `(createdAt desc, id desc)`; `cursor` is the previous page's last row id, and one that does not resolve within the caller's scope is a 400, never a silent page 1 — the API keeps that contract, and the web inbox deliberately softens it on its own side by re-reading page one when a URL-supplied cursor 400s, which is a rendering choice and not a change to this response). Five fields per row and nothing more — `id`, `name` (lifted from `payload.name` when the site's form captured one), `email`, `source`, `createdAt`; `payload`'s other keys, `siteId`, `promotedUserId` and `promotedAt` are never selected (`src/services/leads-inbox.ts` is the only read path). Registered in `authScope` only (auth, no tenant guard) and — alone in that scope — **unconditionally**, with no `isModuleEnabled()` gate: lead capture is a never-gate surface at every level, so reading a lead back cannot be gated either. Identity is `request.userId`; there is no site, org or workspace parameter, so the scope cannot be widened from the wire. It is resolved from the caller's own memberships by `src/services/owner-workspaces.ts`'s `resolveOwnerWorkspaces` — the ONE site→workspace resolution in this app, shared with `lead-promotion.ts` rather than written a second time — and every read filters `siteId ∈ workspaceIds`. Role floor `owner|admin` (contact PII), the same floor and the same reason as `/v1/waitlist-experiments/*`; the scope is the UNION of every workspace the caller owns or administers, not one active tenant, because this route registers outside the tenant scope. **Two deployment preconditions decide whether it returns anything, and both fail EMPTY rather than wide:** on a build with no `multi-tenant` module `resolveOwnerWorkspaces` resolves none (a deliberate scope limit since #5677), so there the scope is the default workspace, granted only through `services/default-tenant.ts`'s `resolveDefaultTenantForOperator` — `AILK_OPERATOR_EMAIL` set, the caller's verified email equal to it, a one-workspace deployment, role `owner|admin` — and every other caller gets the empty page (#6730); and the `siteId` the leads were WRITTEN under must equal a workspace id. That `siteId` has TWO sources, and `currentSiteId()` is not the one on the primary path: the three POST capture routes read it from the REQUEST BODY (`routes/leads/index.ts`), which the capture sections declare in content and default to `"default"` (`ContactForm`/`Newsletter`/`StepFlow`, `ext.siteId ?? "default"`), while the recommender's capture path (`routes/flows/recommender.ts`) is the one that uses `currentSiteId()` (`lib/site.ts`, `AILK_SITE_ID ?? "default"`). A workspace id is `wksp_<uuidv7>`, so `siteId ∈ workspaceIds` matches only when the content-declared value — and, for recommender leads, `AILK_SITE_ID` — is set to it (the same convention `waitlist-dashboard.ts` already relies on). The rendered `name` is lifted from `payload.name` and truncated at 200 characters: the write side bounds `payload` at 64 keys and no per-value length, and `POST /v1/leads/submit` is anonymous with no ownership check on `siteId`. No MCP twin — identity-bound (#3591 exception, the same shape as `/v1/workspaces/*` and `/v1/billing/*`).
41
42
  - `/v1/flows/:slug` (GET), `/v1/flows/:slug/steps` (POST) — serve a flow config + submit a step's answers; server-side per-step partial-state persistence (#3956). Module: `signal-collectors` (D-48, #5366) — registers only when the module is enabled; MCP twins `get_flow`/`submit_flow_step` carry the same `module` tag (D7).
42
43
  - `/v1/flows/:slug/aggregate` (GET) — anonymous, site-scoped per-question response counts across all completed respondents for the results reveal (#4771): respondents are completed `FlowSession` rows (not `Lead` rows — a recommending-mode respondent has no Lead until the email moment), question metadata + the chart flag come from the `WaitlistExperiment` register, and only `resultsReveal`-flagged `single`/`multi` questions are returned (`src/services/flow-aggregate.ts`). No individual answer, email, name, session id, or score in the response; no auth. Module: `signal-collectors`. No MCP twin — recorded exception (an agent walking a flow already gets everything it needs from `submit_flow_step`'s own response; "how everyone else answered" changes none of it).
43
44
  - `/v1/deliverables/:grantId` (GET) — the capability-token download route for gated PDFs and reports, including client deliverables (#3961 D5); the token itself is the sole gate, no auth preHandler applies (`src/routes/deliverables/index.ts`). Module: `signal-collectors` (D-48, #5366). No MCP twin — identity-bound capability-link download (#3591 exception, the same shape as `/v1/waitlist-signups/me`). **Three grant classes since #6310**, keyed on `grant.assetId`'s ADR-0031 prefix: `playbook:` (unchanged, the FEAT-074 U4b virtual asset); `asset_` — a workspace `Asset`, resolved through the named boundary `src/lib/deliverable-asset-store.ts`, scoped by `grant.workspaceId` (a serve-time tenancy re-check restored for this class — D15) — served `attachment` with an extension pinned to the stored content type, `nosniff`, a non-raster `Content-Security-Policy` (for EVERY content type, including raster — never `inline`), and `Cache-Control: private, no-store` so a deleted asset stops downloading immediately; `dasset_` — the LEGACY `DeliverableAsset` store (unchanged, tried only when a magnet's `assetKey` does not resolve against `Asset`). Every miss on the `asset_` class — a NULL `workspaceId`, no row, a foreign-workspace row — folds into the SAME uniform 404 as every other class; there is deliberately no redirect to the public `GET /v1/assets/:assetId` (that route's year-long `immutable` cache would defeat deletion as a revocation lever, and would collapse every lead's capability onto one workspace-canonical id — spec #6310 §9 SEC-001/SEC-002).
@@ -78,6 +78,8 @@ jest.mock('@working-theory/stripe-client', () => ({
78
78
  StripeConfigError: class StripeConfigError extends Error {},
79
79
  UnknownProductCodeError: class UnknownProductCodeError extends Error {},
80
80
  UnknownSessionError: class UnknownSessionError extends Error {},
81
+ // #6866 — sessions.ts tests `instanceof UnknownCustomerError`.
82
+ UnknownCustomerError: class UnknownCustomerError extends Error {},
81
83
  WEBHOOK_TOLERANCE_SECONDS: 300,
82
84
  }));
83
85
 
@@ -469,3 +469,119 @@ describe("#6216 D11 — a lead_… path segment is truncated to 8 characters, cs
469
469
  }
470
470
  });
471
471
  });
472
+
473
+ // ── #6889 — production startup warns when a checkout protection is unset ─────
474
+ //
475
+ // POST /v1/checkout/sessions admits a caller without X-Internal-Auth when
476
+ // OSS_API_INTERNAL_SECRET is unset, and with FRONTEND_URL unset its redirect
477
+ // lock answers 500 while the CORS/cookie origin checks pin to localhost. Both
478
+ // are optional by design (OSS standalone compat), so startup must never fail
479
+ // on them — but a production boot says so, once per variable. Each case rebuilds the module graph because `config/index.ts`
480
+ // snapshots NODE_ENV at import time.
481
+
482
+ describe("#6889 — production startup warns on an unset FRONTEND_URL or OSS_API_INTERNAL_SECRET", () => {
483
+ const ENV_KEYS = [
484
+ "NODE_ENV",
485
+ "FRONTEND_URL",
486
+ "OSS_API_INTERNAL_SECRET",
487
+ // Cleared so a production boot never trips config's database-module exit.
488
+ "AILK_ENABLED_MODULES",
489
+ ] as const;
490
+ type EnvKey = (typeof ENV_KEYS)[number];
491
+ const priorEnv = new Map<string, string | undefined>(
492
+ ENV_KEYS.map((key) => [key, process.env[key]]),
493
+ );
494
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- server is typed via Fastify generics; explicit any avoids pulling in the full FastifyInstance type here
495
+ let app: any;
496
+
497
+ afterEach(async () => {
498
+ if (app) {
499
+ await app.close();
500
+ app = undefined;
501
+ }
502
+ for (const [key, value] of priorEnv) {
503
+ if (value === undefined) {
504
+ delete process.env[key];
505
+ } else {
506
+ process.env[key] = value;
507
+ }
508
+ }
509
+ jest.restoreAllMocks();
510
+ });
511
+
512
+ /** Boot a fresh module graph under `env` and return every warning message. */
513
+ async function bootWarnings(
514
+ env: Partial<Record<EnvKey, string>>,
515
+ ): Promise<string[]> {
516
+ for (const key of ENV_KEYS) {
517
+ const value = env[key];
518
+ if (value === undefined) {
519
+ delete process.env[key];
520
+ } else {
521
+ process.env[key] = value;
522
+ }
523
+ }
524
+ jest.resetModules();
525
+ const { logger } =
526
+ require("@working-theory/observability") as typeof import("@working-theory/observability");
527
+ const warn = jest.spyOn(logger, "warn");
528
+ const { build: freshBuild } =
529
+ require("../server") as typeof import("../server");
530
+ app = await freshBuild();
531
+ return warn.mock.calls.map((args) =>
532
+ args
533
+ .map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg)))
534
+ .join(" "),
535
+ );
536
+ }
537
+
538
+ const naming = (messages: string[], name: string) =>
539
+ messages.filter((message) => message.includes(name));
540
+
541
+ it("warns exactly once naming FRONTEND_URL when it is unset in production", async () => {
542
+ const messages = await bootWarnings({
543
+ NODE_ENV: "production",
544
+ OSS_API_INTERNAL_SECRET: "internal-secret",
545
+ });
546
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(1);
547
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(0);
548
+ });
549
+
550
+ it("warns exactly once naming OSS_API_INTERNAL_SECRET when it is unset in production", async () => {
551
+ const messages = await bootWarnings({
552
+ NODE_ENV: "production",
553
+ FRONTEND_URL: "https://site.example.com",
554
+ });
555
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(1);
556
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(0);
557
+ });
558
+
559
+ it("warns once for each when both are unset in production, and still starts", async () => {
560
+ const messages = await bootWarnings({ NODE_ENV: "production" });
561
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(1);
562
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(1);
563
+ const health = await app.inject({ method: "GET", url: "/health" });
564
+ expect(health.statusCode).toBe(200);
565
+ });
566
+
567
+ it("logs no warning for either when both are set in production", async () => {
568
+ const messages = await bootWarnings({
569
+ NODE_ENV: "production",
570
+ FRONTEND_URL: "https://site.example.com",
571
+ OSS_API_INTERNAL_SECRET: "internal-secret",
572
+ });
573
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(0);
574
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(0);
575
+ });
576
+
577
+ it.each(["development", "test"])(
578
+ "logs no warning for either outside production (NODE_ENV=%s), both unset",
579
+ async (nodeEnv) => {
580
+ const messages = await bootWarnings({ NODE_ENV: nodeEnv });
581
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(0);
582
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(0);
583
+ const health = await app.inject({ method: "GET", url: "/health" });
584
+ expect(health.statusCode).toBe(200);
585
+ },
586
+ );
587
+ });
@@ -42,31 +42,49 @@ describe('Config', () => {
42
42
  expect(dotenv.config).toHaveBeenCalled();
43
43
  });
44
44
 
45
- it('should exit on missing DATABASE_URL in production', () => {
46
- // ARRANGE
45
+ // #6730 security review (M1): a production build whose enabled modules keep
46
+ // their state in the database still refuses to boot without DATABASE_URL —
47
+ // a missing database there is a broken deploy (e.g. a commerce licence read
48
+ // as absent would ack-and-drop signed Stripe events), not a Free site.
49
+ it.each(['commerce', 'content-write', 'multi-tenant', 'scheduling', 'signal-collectors', 'workspace'])(
50
+ 'should exit on missing DATABASE_URL in production when the %s module is enabled',
51
+ (moduleName) => {
52
+ process.env.NODE_ENV = 'production';
53
+ process.env.AILK_ENABLED_MODULES = `marketing,${moduleName}`;
54
+ delete process.env.DATABASE_URL;
55
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
56
+ throw new Error('Process exit called');
57
+ });
58
+
59
+ expect(() => require('../index')).toThrow('Process exit called');
60
+ expect(exitSpy).toHaveBeenCalledWith(1);
61
+ expect(mockLoggerError).toHaveBeenCalled();
62
+
63
+ exitSpy.mockRestore();
64
+ },
65
+ );
66
+
67
+ it('should start database-less in production with only non-database modules enabled', () => {
47
68
  process.env.NODE_ENV = 'production';
69
+ process.env.AILK_ENABLED_MODULES = 'marketing,docs-blog';
48
70
  delete process.env.DATABASE_URL;
49
-
50
71
  const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
51
72
  throw new Error('Process exit called');
52
73
  });
53
- mockLoggerError.mockClear();
54
74
 
55
- // ACT & ASSERT
56
- expect(() => {
57
- require('../index');
58
- }).toThrow('Process exit called');
59
-
60
- expect(exitSpy).toHaveBeenCalledWith(1);
61
- expect(mockLoggerError).toHaveBeenCalled();
75
+ const { config } = require('../index');
62
76
 
77
+ expect(exitSpy).not.toHaveBeenCalled();
78
+ expect(config.DATABASE_URL).toBeUndefined();
63
79
  exitSpy.mockRestore();
64
80
  });
65
81
 
66
- // #6482 — outside production a missing DATABASE_URL starts the API
67
- // database-less (one warning) instead of exiting, so a fresh scaffold's
68
- // `pnpm dev` keeps its "no database needed" promise.
82
+ // #6482 — a missing DATABASE_URL starts the API database-less (one
83
+ // warning) instead of exiting, so a fresh scaffold's `pnpm dev` keeps its
84
+ // "no database needed" promise. #6730 (operator decision (a)) extends that
85
+ // to production: a Free site delivers leads by email with no Postgres.
69
86
  it.each([
87
+ ['production', 'production'],
70
88
  ['development', 'development'],
71
89
  ['unset', undefined],
72
90
  ['test', 'test'],
@@ -130,3 +148,86 @@ describe('Config', () => {
130
148
  expect(config.FRONTEND_URL).toBe('http://localhost:3000');
131
149
  });
132
150
  });
151
+
152
+ // #6730 (operator decision (a)) — the REAL server boots in production with
153
+ // no database: an email-only Free site must be able to run its API, answer
154
+ // `GET /v1/leads/capture`, and refuse authenticated routes with a 503 rather
155
+ // than exit at startup.
156
+ describe('production boot without a database (#6730)', () => {
157
+ const originalEnv = process.env;
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the Fastify instance comes from an isolated require; its generics add nothing here
159
+ let app: any;
160
+
161
+ const EMAIL_ENV = {
162
+ RESEND_API_KEY: 're_test',
163
+ AILK_LEAD_NOTIFICATION_TO: 'owner@example.com',
164
+ AILK_LEAD_NOTIFICATION_FROM: 'noreply@example.com',
165
+ };
166
+
167
+ // External boundaries only, as src/__tests__/server.test.ts mocks them.
168
+ beforeAll(() => {
169
+ jest.doMock('@fastify/cors', () => jest.fn().mockResolvedValue(undefined));
170
+ jest.doMock('@fastify/rate-limit', () => jest.fn().mockResolvedValue(undefined));
171
+ jest.doMock('@working-theory/email', () => ({
172
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: 'm' }),
173
+ sendRequestConfirmEmail: jest.fn(),
174
+ sendRequestPaidLinkEmail: jest.fn(),
175
+ }));
176
+ });
177
+
178
+ afterEach(async () => {
179
+ if (app) await app.close();
180
+ app = undefined;
181
+ process.env = originalEnv;
182
+ });
183
+
184
+ async function bootProduction(extra: Record<string, string> = {}) {
185
+ jest.resetModules();
186
+ const env: NodeJS.ProcessEnv = { ...originalEnv, NODE_ENV: 'production', ...extra };
187
+ delete env.DATABASE_URL;
188
+ delete env.DATABASE_URL_NON_POOLED;
189
+ delete env.RESEND_API_KEY;
190
+ delete env.AILK_LEAD_NOTIFICATION_TO;
191
+ delete env.AILK_LEAD_NOTIFICATION_FROM;
192
+ Object.assign(env, extra);
193
+ process.env = env;
194
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
195
+ throw new Error('Process exit called');
196
+ });
197
+ try {
198
+ const { build } = require('../../server');
199
+ app = await build();
200
+ } finally {
201
+ expect(exitSpy).not.toHaveBeenCalled();
202
+ exitSpy.mockRestore();
203
+ }
204
+ return app;
205
+ }
206
+
207
+ it('with only the email vars, starts and reports contact accepted, newsletter not', async () => {
208
+ const server = await bootProduction(EMAIL_ENV);
209
+
210
+ const res = await server.inject({ method: 'GET', url: '/v1/leads/capture' });
211
+
212
+ expect(res.statusCode).toBe(200);
213
+ expect(res.json()).toEqual({ ok: true, data: { contact: true, newsletter: false } });
214
+ });
215
+
216
+ it('with nothing configured, starts and reports nothing accepted', async () => {
217
+ const server = await bootProduction();
218
+
219
+ const res = await server.inject({ method: 'GET', url: '/v1/leads/capture' });
220
+
221
+ expect(res.statusCode).toBe(200);
222
+ expect(res.json()).toEqual({ ok: true, data: { contact: false, newsletter: false } });
223
+ });
224
+
225
+ it('refuses an authenticated route with 503 database_not_configured', async () => {
226
+ const server = await bootProduction(EMAIL_ENV);
227
+
228
+ const res = await server.inject({ method: 'GET', url: '/v1/leads' });
229
+
230
+ expect(res.statusCode).toBe(503);
231
+ expect(res.json()).toEqual({ error: 'database_not_configured' });
232
+ });
233
+ });
@@ -7,6 +7,8 @@
7
7
  import { logger } from '@working-theory/observability';
8
8
  import * as dotenv from 'dotenv';
9
9
 
10
+ import { isModuleEnabled } from './modules.js';
11
+
10
12
  dotenv.config();
11
13
 
12
14
  export interface Config {
@@ -92,28 +94,45 @@ export interface Config {
92
94
  CAL_WEBHOOK_SECRET?: string;
93
95
  }
94
96
 
97
+ /** Enabled modules whose state lives in the database (see validateConfig). */
98
+ const DATABASE_MODULES = [
99
+ 'commerce',
100
+ 'content-write',
101
+ 'multi-tenant',
102
+ 'scheduling',
103
+ 'signal-collectors',
104
+ 'workspace',
105
+ ] as const;
106
+
95
107
  function validateConfig(): Config {
96
- const errors: string[] = [];
97
108
  const nodeEnv = (process.env.NODE_ENV || 'development') as Config['NODE_ENV'];
98
109
 
99
- // Local database for auth: required in production. Elsewhere a fresh
100
- // scaffold runs with no .env, so start database-less and let the auth
101
- // preHandler answer 503 database_not_configured (#6482).
110
+ // No database is a supported shape in every environment, production
111
+ // included (#6730 operator decision (a)): a Free site delivers leads by
112
+ // email and needs no Postgres. Start database-less and let the auth
113
+ // preHandler answer 503 database_not_configured (#6482) — it reads
114
+ // DATABASE_URL per request, whatever NODE_ENV says, so no authenticated
115
+ // route runs without a database.
116
+ //
117
+ // Except a production build whose ENABLED modules keep their state in the
118
+ // database. There a missing DATABASE_URL is a broken deploy, not a Free
119
+ // site: e.g. a commerce site's licence lives in the database-backed store,
120
+ // so booting without it would read the site as unlicensed and ack-and-drop
121
+ // every signed Stripe event (#6730 security review, M1). Those builds keep
122
+ // the loud startup failure.
102
123
  if (!process.env.DATABASE_URL) {
103
- if (nodeEnv === 'production') {
104
- errors.push('DATABASE_URL is required for authentication');
105
- } else {
106
- logger.warn(
107
- { nodeEnv },
108
- '⚠️ DATABASE_URL not set — starting database-less; authenticated routes answer 503 database_not_configured',
124
+ const needsDatabase = DATABASE_MODULES.filter((m) => isModuleEnabled(m));
125
+ if (nodeEnv === 'production' && needsDatabase.length > 0) {
126
+ logger.error(
127
+ { modules: needsDatabase },
128
+ '❌ DATABASE_URL is required in production when these modules are enabled',
109
129
  );
130
+ process.exit(1);
110
131
  }
111
- }
112
-
113
- // Exit on errors
114
- if (errors.length > 0) {
115
- logger.error({ errors }, '❌ Configuration validation failed');
116
- process.exit(1);
132
+ logger.warn(
133
+ { nodeEnv },
134
+ '⚠️ DATABASE_URL not set — starting database-less; authenticated routes answer 503 database_not_configured',
135
+ );
117
136
  }
118
137
 
119
138
  return {