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
@@ -96,6 +96,36 @@ describe("appendLeadRecord", () => {
96
96
  expect(filePath).toMatch(/leads\.jsonl$/);
97
97
  });
98
98
 
99
+ // #6730 D3 — the default file is local-only: in production it does not
100
+ // outlive a serverless instance, so it is skipped unless explicitly pointed
101
+ // at a durable path.
102
+ describe("in production (#6730 D3)", () => {
103
+ const savedNodeEnv = process.env.NODE_ENV;
104
+ afterEach(() => {
105
+ process.env.NODE_ENV = savedNodeEnv;
106
+ });
107
+
108
+ it("skips the write when LEAD_CAPTURE_FILE is unset", async () => {
109
+ process.env.NODE_ENV = "production";
110
+
111
+ const result = await appendLeadRecord(record);
112
+
113
+ expect(result).toEqual({ ok: true, skipped: true });
114
+ expect(mockAppendFile).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it("writes when LEAD_CAPTURE_FILE is set explicitly", async () => {
118
+ process.env.NODE_ENV = "production";
119
+ process.env.LEAD_CAPTURE_FILE = "/data/leads.jsonl";
120
+
121
+ const result = await appendLeadRecord(record);
122
+
123
+ expect(result).toEqual({ ok: true });
124
+ expect(mockAppendFile).toHaveBeenCalledTimes(1);
125
+ expect(mockAppendFile.mock.calls[0][0]).toBe("/data/leads.jsonl");
126
+ });
127
+ });
128
+
99
129
  // AC4 — LEAD_CAPTURE_FILE_DISABLED=true skips the write
100
130
  it("short-circuits write when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
101
131
  process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @file lead-delivery.test.ts
3
+ * @description The #6730 D1 delivery table, row by row.
4
+ */
5
+
6
+ import { acceptsKind, resolveLeadDelivery } from "../lead-delivery";
7
+
8
+ const EMAIL = {
9
+ RESEND_API_KEY: "re_test",
10
+ AILK_LEAD_NOTIFICATION_TO: "owner@example.com",
11
+ AILK_LEAD_NOTIFICATION_FROM: "noreply@example.com",
12
+ };
13
+
14
+ describe("resolveLeadDelivery", () => {
15
+ it("DATABASE_URL alone → inbox", () => {
16
+ expect(resolveLeadDelivery({ DATABASE_URL: "postgres://x", NODE_ENV: "production" })).toBe(
17
+ "inbox",
18
+ );
19
+ });
20
+
21
+ it("DATABASE_URL wins over the email vars", () => {
22
+ expect(resolveLeadDelivery({ DATABASE_URL: "postgres://x", ...EMAIL })).toBe("inbox");
23
+ });
24
+
25
+ it("the three email vars, no database → email", () => {
26
+ expect(resolveLeadDelivery({ ...EMAIL, NODE_ENV: "production" })).toBe("email");
27
+ expect(resolveLeadDelivery({ ...EMAIL, NODE_ENV: "development" })).toBe("email");
28
+ });
29
+
30
+ it("a bare RESEND_API_KEY is not email delivery — local in dev, none in production", () => {
31
+ expect(resolveLeadDelivery({ RESEND_API_KEY: "re_test", NODE_ENV: "development" })).toBe(
32
+ "local",
33
+ );
34
+ expect(resolveLeadDelivery({ RESEND_API_KEY: "re_test", NODE_ENV: "production" })).toBe(
35
+ "none",
36
+ );
37
+ });
38
+
39
+ it("the key and one address only → not email", () => {
40
+ expect(
41
+ resolveLeadDelivery({
42
+ RESEND_API_KEY: "re_test",
43
+ AILK_LEAD_NOTIFICATION_TO: "owner@example.com",
44
+ NODE_ENV: "production",
45
+ }),
46
+ ).toBe("none");
47
+ });
48
+
49
+ it("nothing configured in production → none", () => {
50
+ expect(resolveLeadDelivery({ NODE_ENV: "production" })).toBe("none");
51
+ });
52
+
53
+ it("nothing configured outside production → local", () => {
54
+ expect(resolveLeadDelivery({ NODE_ENV: "test" })).toBe("local");
55
+ expect(resolveLeadDelivery({})).toBe("local");
56
+ });
57
+
58
+ it("LEAD_CAPTURE_FILE_DISABLED=true removes the local path → none", () => {
59
+ expect(
60
+ resolveLeadDelivery({ NODE_ENV: "development", LEAD_CAPTURE_FILE_DISABLED: "true" }),
61
+ ).toBe("none");
62
+ });
63
+
64
+ it("reads process.env by default", () => {
65
+ const saved = process.env.DATABASE_URL;
66
+ process.env.DATABASE_URL = "postgres://x";
67
+ try {
68
+ expect(resolveLeadDelivery()).toBe("inbox");
69
+ } finally {
70
+ if (saved === undefined) delete process.env.DATABASE_URL;
71
+ else process.env.DATABASE_URL = saved;
72
+ }
73
+ });
74
+ });
75
+
76
+ describe("acceptsKind", () => {
77
+ it("none accepts nothing", () => {
78
+ expect(acceptsKind("none", "submit")).toBe(false);
79
+ expect(acceptsKind("none", "subscribe")).toBe(false);
80
+ expect(acceptsKind("none", "callback")).toBe(false);
81
+ });
82
+
83
+ it("email accepts submit and callback, never subscribe", () => {
84
+ expect(acceptsKind("email", "submit")).toBe(true);
85
+ expect(acceptsKind("email", "callback")).toBe(true);
86
+ expect(acceptsKind("email", "subscribe")).toBe(false);
87
+ });
88
+
89
+ it("inbox and local accept every kind", () => {
90
+ for (const d of ["inbox", "local"] as const) {
91
+ expect(acceptsKind(d, "submit")).toBe(true);
92
+ expect(acceptsKind(d, "subscribe")).toBe(true);
93
+ expect(acceptsKind(d, "callback")).toBe(true);
94
+ }
95
+ });
96
+ });
@@ -12,6 +12,15 @@
12
12
  *
13
13
  * Env vars (all read inside function bodies — module is import-side-effect-free):
14
14
  * LEAD_CAPTURE_FILE — output path (default: apps/api/leads.jsonl relative to cwd)
15
+ *
16
+ * The default path is LOCAL-ONLY (#6730 D3). In production
17
+ * (`NODE_ENV=production`) with `LEAD_CAPTURE_FILE` unset, the write is skipped:
18
+ * on serverless hosting a file under the process cwd does not outlive the
19
+ * instance, so it is not a record of anything. An operator with a durable
20
+ * volume can keep a side copy by setting `LEAD_CAPTURE_FILE` explicitly —
21
+ * a copy only: in production it never makes a lead count as delivered
22
+ * (`lib/lead-delivery.ts` needs a database or the email vars).
23
+ *
15
24
  * LEAD_CAPTURE_FILE_DISABLED — set to "true" to skip writes (opt-out)
16
25
  * LEAD_CAPTURE_INCLUDE_IP — set to "true" to include metadata.ip in the record
17
26
  *
@@ -47,6 +56,9 @@ export async function appendLeadRecord(
47
56
  if (disabled) {
48
57
  return { ok: true, skipped: true };
49
58
  }
59
+ if (process.env.NODE_ENV === "production" && !process.env.LEAD_CAPTURE_FILE) {
60
+ return { ok: true, skipped: true };
61
+ }
50
62
 
51
63
  const includeIp = process.env.LEAD_CAPTURE_INCLUDE_IP === "true";
52
64
  const outputPath = resolveOutputPath();
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @file lead-delivery.ts
3
+ * @description Where a captured lead goes, resolved from the environment (#6730 D1).
4
+ *
5
+ * inbox — `DATABASE_URL` is set: the database is the destination (the lead
6
+ * inbox at `/leads`); the owner email is still sent best-effort.
7
+ * email — no database, but `RESEND_API_KEY` plus both notification
8
+ * addresses are set: the owner email IS the delivery.
9
+ * local — neither, outside production (`pnpm dev`, tests): the local file
10
+ * fallback + `leads.jsonl`, so local work needs nothing configured.
11
+ * none — neither, in production: nothing is accepted, so no visitor is
12
+ * ever told a message was sent when it reached nobody.
13
+ *
14
+ * A key alone delivers nothing (`sendEmail()` also needs both addresses), so
15
+ * it is not counted as configured. `LEAD_CAPTURE_FILE_DISABLED=true` removes
16
+ * the local file path, so outside production it resolves to `none` too.
17
+ *
18
+ * Import-side-effect-free; the environment is read at call time.
19
+ */
20
+
21
+ export type LeadDelivery = "inbox" | "email" | "local" | "none";
22
+
23
+ export type LeadDeliveryKind = "submit" | "subscribe" | "callback";
24
+
25
+ export function resolveLeadDelivery(
26
+ env: NodeJS.ProcessEnv = process.env,
27
+ ): LeadDelivery {
28
+ if (env.DATABASE_URL) return "inbox";
29
+ if (
30
+ env.RESEND_API_KEY &&
31
+ env.AILK_LEAD_NOTIFICATION_TO &&
32
+ env.AILK_LEAD_NOTIFICATION_FROM
33
+ ) {
34
+ return "email";
35
+ }
36
+ if (env.NODE_ENV !== "production" && env.LEAD_CAPTURE_FILE_DISABLED !== "true") {
37
+ return "local";
38
+ }
39
+ return "none";
40
+ }
41
+
42
+ /**
43
+ * Whether a delivery mode accepts a lead of this kind. A newsletter
44
+ * subscription is a list entry, not a message — emailing the owner one
45
+ * address per signup is not a list — so `subscribe` needs somewhere to keep
46
+ * it (the database, or the local file in development).
47
+ */
48
+ export function acceptsKind(d: LeadDelivery, kind: LeadDeliveryKind): boolean {
49
+ if (d === "none") return false;
50
+ if (kind === "subscribe") return d === "inbox" || d === "local";
51
+ return true;
52
+ }
@@ -11,6 +11,13 @@
11
11
  import { getStripeClient } from "@working-theory/stripe-client";
12
12
 
13
13
  export {
14
+ // PRECONDITION (#6113): do not narrow or bypass this sandbox-only guard for a
15
+ // production key class until #6113's no-overwrite guard in
16
+ // routes/webhooks/stripe.ts upsertBillingIdentity is on main — an unverified
17
+ // Checkout email must never re-point an already-bound User.billingCustomerId.
18
+ // Widened by #6852 (the #6113 follow-up): the same function must bind only
19
+ // on a server-stamped metadata.ailk_user_id — an unverified Checkout email
20
+ // never makes a FIRST binding either.
14
21
  assertSandboxKey,
15
22
  createAdHocPrice,
16
23
  createBillingPortalSession,
@@ -25,6 +32,8 @@ export {
25
32
  StripeConfigError,
26
33
  StripeWebhookSignatureError,
27
34
  toSubscriptionLifecycleStatus,
35
+ // Signed-in Checkout (#6866 D8) — a pinned customer Stripe rejects.
36
+ UnknownCustomerError,
28
37
  UnknownProductCodeError,
29
38
  UnknownSessionError,
30
39
  verifyWebhookSignature,
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @file auth.optional-cookie.test.ts
3
+ *
4
+ * The non-rejecting cookie resolver `resolveOptionalCookieUser` (#6866 D2).
5
+ *
6
+ * Asserts each `none` reason (A1–A6), a `user` for a valid same-origin cookie
7
+ * (A7), and that the resolver never writes a reply and never throws.
8
+ */
9
+
10
+ import Fastify from "fastify";
11
+ import type { FastifyInstance } from "fastify";
12
+
13
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted; mirrors auth.cookie-path.test.ts
14
+ jest.mock("../../lib/prisma.js", () => ({
15
+ prisma: {
16
+ session: { findUnique: jest.fn() },
17
+ apiToken: { findUnique: jest.fn(), update: jest.fn() },
18
+ },
19
+ }));
20
+
21
+ import { prisma } from "../../lib/prisma.js";
22
+ import { resolveOptionalCookieUser } from "../auth.js";
23
+
24
+ const FRONTEND = "https://app.example.com";
25
+ const COOKIE = "better-auth.session_token=sess_tok_abc.signature";
26
+
27
+ /** Echoes the resolver's result; the reply is only ever written here. */
28
+ function buildApp(): FastifyInstance {
29
+ const app = Fastify();
30
+ app.post("/resolve", async (req) =>
31
+ resolveOptionalCookieUser(req, { frontendOrigin: FRONTEND }),
32
+ );
33
+ return app;
34
+ }
35
+
36
+ function validSession(overrides: Record<string, unknown> = {}) {
37
+ return {
38
+ id: "ses_1",
39
+ userId: "usr_buyer",
40
+ expiresAt: new Date(Date.now() + 60_000),
41
+ activeOrganizationId: null,
42
+ activeWorkspaceId: null,
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ describe("resolveOptionalCookieUser (#6866 D2)", () => {
48
+ const originalDatabaseUrl = process.env.DATABASE_URL;
49
+ let app: FastifyInstance;
50
+
51
+ beforeEach(async () => {
52
+ jest.clearAllMocks();
53
+ process.env.DATABASE_URL = "postgresql://test:test@localhost/test";
54
+ app = buildApp();
55
+ await app.ready();
56
+ });
57
+
58
+ afterEach(async () => {
59
+ await app.close();
60
+ if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL;
61
+ else process.env.DATABASE_URL = originalDatabaseUrl;
62
+ });
63
+
64
+ async function resolve(headers: Record<string, string>) {
65
+ const res = await app.inject({ method: "POST", url: "/resolve", headers });
66
+ // The route's own handler wrote the only reply — the resolver wrote none.
67
+ expect(res.statusCode).toBe(200);
68
+ return JSON.parse(res.body) as unknown;
69
+ }
70
+
71
+ it("A1: no cookie → none/no_cookie, with no database read", async () => {
72
+ expect(await resolve({ origin: FRONTEND })).toEqual({
73
+ kind: "none",
74
+ reason: "no_cookie",
75
+ });
76
+ expect(prisma.session.findUnique).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it("A1b: a bearer header alone is ignored — none/no_cookie, apiToken never read (F8)", async () => {
80
+ expect(
81
+ await resolve({ origin: FRONTEND, authorization: "Bearer tok_valid" }),
82
+ ).toEqual({ kind: "none", reason: "no_cookie" });
83
+ expect(prisma.apiToken.findUnique).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it("A2: DATABASE_URL unset with a cookie → none/no_database", async () => {
87
+ delete process.env.DATABASE_URL;
88
+ expect(await resolve({ origin: FRONTEND, cookie: COOKIE })).toEqual({
89
+ kind: "none",
90
+ reason: "no_database",
91
+ });
92
+ expect(prisma.session.findUnique).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it("A3: unknown token → none/unknown_session", async () => {
96
+ (prisma.session.findUnique as jest.Mock).mockResolvedValue(null);
97
+ expect(await resolve({ origin: FRONTEND, cookie: COOKIE })).toEqual({
98
+ kind: "none",
99
+ reason: "unknown_session",
100
+ });
101
+ });
102
+
103
+ it("A4: expired session → none/expired", async () => {
104
+ (prisma.session.findUnique as jest.Mock).mockResolvedValue(
105
+ validSession({ expiresAt: new Date(Date.now() - 1_000) }),
106
+ );
107
+ expect(await resolve({ origin: FRONTEND, cookie: COOKIE })).toEqual({
108
+ kind: "none",
109
+ reason: "expired",
110
+ });
111
+ });
112
+
113
+ it("A5: POST with a foreign Origin → none/cross_origin", async () => {
114
+ (prisma.session.findUnique as jest.Mock).mockResolvedValue(validSession());
115
+ expect(
116
+ await resolve({ origin: "https://evil.example.com", cookie: COOKIE }),
117
+ ).toEqual({ kind: "none", reason: "cross_origin" });
118
+ });
119
+
120
+ it("A6: the session lookup rejects → none/lookup_failed, never a throw", async () => {
121
+ (prisma.session.findUnique as jest.Mock).mockRejectedValue(
122
+ new Error("db down"),
123
+ );
124
+ expect(await resolve({ origin: FRONTEND, cookie: COOKIE })).toEqual({
125
+ kind: "none",
126
+ reason: "lookup_failed",
127
+ });
128
+ });
129
+
130
+ it("A7: a valid same-origin cookie → user, looked up by the unsigned token", async () => {
131
+ (prisma.session.findUnique as jest.Mock).mockResolvedValue(validSession());
132
+ expect(await resolve({ origin: FRONTEND, cookie: COOKIE })).toEqual({
133
+ kind: "user",
134
+ userId: "usr_buyer",
135
+ });
136
+ expect(prisma.session.findUnique).toHaveBeenCalledWith(
137
+ expect.objectContaining({ where: { token: "sess_tok_abc" } }),
138
+ );
139
+ });
140
+ });
@@ -191,6 +191,60 @@ export type AuthPreHandlerOptions = {
191
191
  frontendOrigin?: string | undefined;
192
192
  };
193
193
 
194
+ /**
195
+ * The result of `resolveOptionalCookieUser` — a signed-in user, or the reason
196
+ * there is none (#6866 D2).
197
+ */
198
+ export type OptionalCookieUser =
199
+ | { kind: "user"; userId: Id<"user"> }
200
+ | {
201
+ kind: "none";
202
+ reason:
203
+ | "no_cookie"
204
+ | "no_database"
205
+ | "unknown_session"
206
+ | "expired"
207
+ | "cross_origin"
208
+ | "lookup_failed";
209
+ };
210
+
211
+ /**
212
+ * Non-rejecting cookie resolver for routes that stay open to anonymous callers
213
+ * but act on a signed-in user when there is one (#6866 D2 — the checkout
214
+ * route's buyer stamp).
215
+ *
216
+ * Applies the cookie path's own rules — the same private cookie reader,
217
+ * Session lookup, expiry comparison and same-origin check as
218
+ * `makeAuthPreHandler` — so the two paths cannot drift. Reads the COOKIE only:
219
+ * a bearer `ApiToken` never identifies a user here. Never writes a reply and
220
+ * never throws; every failure is a `none` with its reason. The cookie is
221
+ * checked first, so an anonymous call on a database-less boot is `no_cookie`.
222
+ */
223
+ export async function resolveOptionalCookieUser(
224
+ request: FastifyRequest,
225
+ opts: AuthPreHandlerOptions,
226
+ ): Promise<OptionalCookieUser> {
227
+ const cookieToken = readSessionCookie(request);
228
+ if (!cookieToken) return { kind: "none", reason: "no_cookie" };
229
+ if (!process.env.DATABASE_URL) return { kind: "none", reason: "no_database" };
230
+
231
+ let session: SessionRow | undefined;
232
+ try {
233
+ session = await resolveCookieSession(cookieToken);
234
+ } catch {
235
+ return { kind: "none", reason: "lookup_failed" };
236
+ }
237
+ if (!session) return { kind: "none", reason: "unknown_session" };
238
+ if (session.expiresAt.getTime() <= getClock().now()) {
239
+ return { kind: "none", reason: "expired" };
240
+ }
241
+ if (!isOriginAllowed(request, opts.frontendOrigin)) {
242
+ return { kind: "none", reason: "cross_origin" };
243
+ }
244
+ // Brand assertion at the Prisma read edge (ADR-0031 rule 5), as above.
245
+ return { kind: "user", userId: session.userId as Id<"user"> };
246
+ }
247
+
194
248
  /**
195
249
  * Fastify preHandler implementing the dual-mode contract.
196
250
  *
@@ -747,9 +747,11 @@ export function buildOpenApiSpec(
747
747
  "form fields — for the given siteId. `source` is a free-form tag the caller " +
748
748
  "sets to identify the channel (for example `agent`); it is stored as-is and " +
749
749
  "not checked against a fixed vocabulary. A 201 response means the lead " +
750
- "landed, in the database or (if the database is unavailable) a file " +
751
- "fallback; the response's `email` field reports, best-effort, whether the " +
752
- "site operator was notified.",
750
+ "landed — in the database, a local file fallback (outside production " +
751
+ "only), or, on a deployment with no database, the site operator's inbox " +
752
+ "by email (`persisted: \"email\"`); with a database, the response's " +
753
+ "`email` field reports, best-effort, whether the operator was notified. " +
754
+ "A lead is never reported captured when nothing stored or delivered it.",
753
755
  requestBody: {
754
756
  required: true,
755
757
  content: {
@@ -777,8 +779,8 @@ export function buildOpenApiSpec(
777
779
  },
778
780
  "503": {
779
781
  description:
780
- "Lead capture unavailable — persistence failed, or neither file " +
781
- "capture nor a database is configured",
782
+ "Lead capture unavailable — persistence failed, delivery failed, or " +
783
+ "this deployment has nowhere to put the lead",
782
784
  content: {
783
785
  "application/json": {
784
786
  schema: {
@@ -787,6 +789,7 @@ export function buildOpenApiSpec(
787
789
  {
788
790
  $ref: "#/components/schemas/LeadCaptureNotConfiguredResponse",
789
791
  },
792
+ { $ref: "#/components/schemas/DeliveryFailedResponse" },
790
793
  ],
791
794
  },
792
795
  },
@@ -795,6 +798,36 @@ export function buildOpenApiSpec(
795
798
  },
796
799
  },
797
800
  },
801
+ "/v1/leads/capture": {
802
+ get: {
803
+ operationId: "getLeadCaptureStatus",
804
+ summary: "Whether this site accepts a contact message or a signup",
805
+ description:
806
+ "Two booleans: `contact` — a `POST /v1/leads/submit` would be accepted; " +
807
+ "`newsletter` — a `POST /v1/leads/subscribe` would be. A web page reads " +
808
+ "it to show the contact form or the site's contact email. It never says " +
809
+ "how a lead is delivered. No MCP twin (recorded parity exception): the " +
810
+ "three lead-write tools already answer the same question with a 503.",
811
+ responses: {
812
+ "200": {
813
+ description: "Lead-capture status",
814
+ content: {
815
+ "application/json": {
816
+ schema: {
817
+ type: "object",
818
+ required: ["ok", "data"],
819
+ properties: {
820
+ ok: { type: "boolean", enum: [true] },
821
+ data: { $ref: "#/components/schemas/LeadCaptureStatus" },
822
+ },
823
+ },
824
+ },
825
+ },
826
+ },
827
+ "429": { $ref: "#/components/responses/RateLimited" },
828
+ },
829
+ },
830
+ },
798
831
  "/v1/leads/subscribe": {
799
832
  post: {
800
833
  operationId: "subscribeLead",
@@ -833,8 +866,8 @@ export function buildOpenApiSpec(
833
866
  },
834
867
  "503": {
835
868
  description:
836
- "Lead capture unavailable — persistence failed, or neither file " +
837
- "capture nor a database is configured",
869
+ "Lead capture unavailable — persistence failed, delivery failed, or " +
870
+ "this deployment has nowhere to put the lead",
838
871
  content: {
839
872
  "application/json": {
840
873
  schema: {
@@ -1033,8 +1066,8 @@ export function buildOpenApiSpec(
1033
1066
  },
1034
1067
  "503": {
1035
1068
  description:
1036
- "Lead capture unavailable — persistence failed, or neither file " +
1037
- "capture nor a database is configured",
1069
+ "Lead capture unavailable — persistence failed, delivery failed, or " +
1070
+ "this deployment has nowhere to put the lead",
1038
1071
  content: {
1039
1072
  "application/json": {
1040
1073
  schema: {
@@ -1043,6 +1076,7 @@ export function buildOpenApiSpec(
1043
1076
  {
1044
1077
  $ref: "#/components/schemas/LeadCaptureNotConfiguredResponse",
1045
1078
  },
1079
+ { $ref: "#/components/schemas/DeliveryFailedResponse" },
1046
1080
  ],
1047
1081
  },
1048
1082
  },
@@ -1489,10 +1523,11 @@ export function buildOpenApiSpec(
1489
1523
  type: "string",
1490
1524
  description:
1491
1525
  "Server-assigned record id — a DB-prefixed entity id when " +
1492
- 'persisted is "db", or a `fallback-<ts>-<rand>` file id when ' +
1493
- 'persisted is "file"',
1526
+ 'persisted is "db", a `fallback-<ts>-<rand>` file id when ' +
1527
+ 'persisted is "file", or an `email-<ts>-<rand>` id when persisted ' +
1528
+ 'is "email" (no database; the operator email was the delivery)',
1494
1529
  },
1495
- persisted: { type: "string", enum: ["db", "file"] },
1530
+ persisted: { type: "string", enum: ["db", "file", "email"] },
1496
1531
  email: { $ref: "#/components/schemas/LeadEmailResult" },
1497
1532
  },
1498
1533
  },
@@ -1511,6 +1546,22 @@ export function buildOpenApiSpec(
1511
1546
  error: { type: "string", enum: ["persist_failed"] },
1512
1547
  },
1513
1548
  },
1549
+ DeliveryFailedResponse: {
1550
+ type: "object",
1551
+ required: ["error", "message"],
1552
+ properties: {
1553
+ error: { type: "string", enum: ["delivery_failed"] },
1554
+ message: { type: "string" },
1555
+ },
1556
+ },
1557
+ LeadCaptureStatus: {
1558
+ type: "object",
1559
+ required: ["contact", "newsletter"],
1560
+ properties: {
1561
+ contact: { type: "boolean" },
1562
+ newsletter: { type: "boolean" },
1563
+ },
1564
+ },
1514
1565
  LeadCaptureNotConfiguredResponse: {
1515
1566
  type: "object",
1516
1567
  required: ["error", "message"],
@@ -44,6 +44,8 @@ jest.mock("@working-theory/stripe-client", () => ({
44
44
  this.name = "UnknownSessionError";
45
45
  }
46
46
  },
47
+ // #6866 — sessions.ts tests `instanceof UnknownCustomerError`.
48
+ UnknownCustomerError: class UnknownCustomerError extends Error {},
47
49
  UnknownProductCodeError: class UnknownProductCodeError extends Error {
48
50
  constructor(productCode: string) {
49
51
  super(`No active price found for lookup_key: ${productCode}`);
@@ -59,6 +61,7 @@ jest.mock("@working-theory/stripe-client", () => ({
59
61
  }));
60
62
 
61
63
  // Import after mock setup
64
+ import { config } from "../../../config/index.js";
62
65
  import { checkoutRoutes } from "../index";
63
66
  import { __resetBuckets } from "../../../middleware/rate-limit.js";
64
67
 
@@ -370,8 +373,8 @@ describe("GET /v1/checkout/sessions/:id", () => {
370
373
  url: "/v1/checkout/sessions",
371
374
  payload: {
372
375
  productCode: "ailk-pro",
373
- successUrl: "https://example.com/success",
374
- cancelUrl: "https://example.com/cancel",
376
+ successUrl: `${config.FRONTEND_URL}/success`,
377
+ cancelUrl: `${config.FRONTEND_URL}/cancel`,
375
378
  },
376
379
  });
377
380