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
@@ -62,11 +62,37 @@ jest.mock("@working-theory/stripe-client", () => ({
62
62
  this.name = "StripeConfigError";
63
63
  }
64
64
  },
65
+ UnknownCustomerError: class UnknownCustomerError extends Error {
66
+ constructor() {
67
+ super("The Checkout Session's pinned Stripe customer does not exist.");
68
+ this.name = "UnknownCustomerError";
69
+ }
70
+ },
71
+ }));
72
+
73
+ // #6866 — the route's optional cookie identity reads the Session and User
74
+ // rows (the auth.cookie-path.test.ts pattern), so the real resolver and
75
+ // buyer service run against this mock.
76
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted; the DB boundary, mirrors auth.cookie-path.test.ts
77
+ jest.mock("../../../lib/prisma.js", () => ({
78
+ prisma: {
79
+ session: { findUnique: jest.fn() },
80
+ user: { findUnique: jest.fn() },
81
+ apiToken: { findUnique: jest.fn(), update: jest.fn() },
82
+ },
65
83
  }));
66
84
 
67
85
  // Import after mock setup
86
+ import { config } from "../../../config/index.js";
87
+ import { UnknownCustomerError } from "../../../lib/stripe.js";
88
+ import { prisma } from "../../../lib/prisma.js";
68
89
  import { checkoutRoutes } from "../index";
69
90
 
91
+ // #6877 — pin the suite's site origin to the fixtures' own origin, so every
92
+ // anonymous case below sends same-origin redirects. Runs before any
93
+ // `describe` is collected, so the #6866 block's SAME_ORIGIN reads it too.
94
+ config.FRONTEND_URL = "https://example.com";
95
+
70
96
  // ── Helpers ─────────────────────────────────────────────────────────────────
71
97
 
72
98
  function buildApp(): FastifyInstance {
@@ -886,3 +912,605 @@ describe("CheckoutSessionRequestSchema preserves Stripe's {CHECKOUT_SESSION_ID}
886
912
  );
887
913
  });
888
914
  });
915
+
916
+ // ── Signed-in buyer stamp (#6866) ────────────────────────────────────────────
917
+
918
+ describe("POST /v1/checkout/sessions — signed-in buyer stamp (#6866)", () => {
919
+ const SESSION_TOKEN = "sess_tok_buyer";
920
+ const COOKIE = `better-auth.session_token=${SESSION_TOKEN}.signature`;
921
+ const SAME_ORIGIN = config.FRONTEND_URL;
922
+ const originalDatabaseUrl = process.env.DATABASE_URL;
923
+
924
+ type UserRow = {
925
+ id: string;
926
+ email: string;
927
+ emailVerified: boolean;
928
+ billingProvider: string | null;
929
+ billingCustomerId: string | null;
930
+ };
931
+
932
+ let app: FastifyInstance;
933
+ let logLines: Array<Record<string, unknown>>;
934
+ let rawLog: string;
935
+
936
+ function givenSession(overrides: Record<string, unknown> = {}) {
937
+ (prisma.session.findUnique as jest.Mock).mockResolvedValue({
938
+ id: "ses_1",
939
+ userId: "usr_buyer",
940
+ expiresAt: new Date(Date.now() + 60_000),
941
+ activeOrganizationId: null,
942
+ activeWorkspaceId: null,
943
+ ...overrides,
944
+ });
945
+ }
946
+
947
+ /** Answers each `user.findUnique` with only the fields it selected. */
948
+ function givenUser(row: Partial<UserRow> | null) {
949
+ const full: UserRow = {
950
+ id: "usr_buyer",
951
+ email: "buyer@example.com",
952
+ emailVerified: true,
953
+ billingProvider: null,
954
+ billingCustomerId: null,
955
+ ...row,
956
+ };
957
+ (prisma.user.findUnique as jest.Mock).mockImplementation(
958
+ async (args: { select: Record<string, boolean> }) =>
959
+ row === null
960
+ ? null
961
+ : Object.fromEntries(
962
+ Object.keys(args.select).map((k) => [
963
+ k,
964
+ full[k as keyof UserRow],
965
+ ]),
966
+ ),
967
+ );
968
+ }
969
+
970
+ function post(
971
+ headers: Record<string, string>,
972
+ payload: Record<string, unknown> = VALID_BODY,
973
+ ) {
974
+ return app.inject({
975
+ method: "POST",
976
+ url: "/v1/checkout/sessions",
977
+ headers,
978
+ payload,
979
+ });
980
+ }
981
+
982
+ function signedIn(payload?: Record<string, unknown>) {
983
+ return post({ cookie: COOKIE, origin: SAME_ORIGIN }, payload);
984
+ }
985
+
986
+ function createdLog() {
987
+ return logLines.find((l) => l["msg"] === "[checkout] session created");
988
+ }
989
+
990
+ beforeEach(async () => {
991
+ jest.clearAllMocks();
992
+ process.env["STRIPE_SECRET_KEY"] = "sk_test_route_fixture";
993
+ process.env.DATABASE_URL = "postgresql://test:test@localhost/test";
994
+ mockAssertSandboxKey.mockImplementation(() => undefined);
995
+ mockCreateCheckoutSession.mockResolvedValue({
996
+ sessionId: "cs_test_abc12345xyz",
997
+ url: "https://checkout.stripe.com/c/pay/cs_test_abc12345xyz",
998
+ });
999
+ givenSession();
1000
+ givenUser({});
1001
+
1002
+ logLines = [];
1003
+ rawLog = "";
1004
+ app = require("fastify")({
1005
+ logger: {
1006
+ level: "info",
1007
+ stream: {
1008
+ write: (line: string) => {
1009
+ rawLog += line;
1010
+ logLines.push(JSON.parse(line) as Record<string, unknown>);
1011
+ },
1012
+ },
1013
+ },
1014
+ });
1015
+ app.register(checkoutRoutes);
1016
+ await app.ready();
1017
+ });
1018
+
1019
+ afterEach(async () => {
1020
+ await app.close();
1021
+ delete process.env["STRIPE_SECRET_KEY"];
1022
+ if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL;
1023
+ else process.env.DATABASE_URL = originalDatabaseUrl;
1024
+ });
1025
+
1026
+ it("#6866 R1: an unbound verified buyer is passed from the Session row, never the body", async () => {
1027
+ const res = await signedIn({ ...VALID_BODY, ailk_user_id: "usr_attacker" });
1028
+
1029
+ expect(res.statusCode).toBe(201);
1030
+ expect(mockCreateCheckoutSession).toHaveBeenCalledTimes(1);
1031
+ const arg = mockCreateCheckoutSession.mock.calls[0][0];
1032
+ expect(arg.buyer).toEqual({
1033
+ userId: "usr_buyer",
1034
+ email: "buyer@example.com",
1035
+ });
1036
+ expect(arg.buyer).not.toHaveProperty("customerId");
1037
+ expect(prisma.session.findUnique).toHaveBeenCalledWith(
1038
+ expect.objectContaining({ where: { token: SESSION_TOKEN } }),
1039
+ );
1040
+ });
1041
+
1042
+ it("#6866 R2: a stripe-bound buyer passes their customerId", async () => {
1043
+ givenUser({ billingProvider: "stripe", billingCustomerId: "cus_bound" });
1044
+
1045
+ await signedIn();
1046
+
1047
+ expect(mockCreateCheckoutSession.mock.calls[0][0].buyer.customerId).toBe(
1048
+ "cus_bound",
1049
+ );
1050
+ });
1051
+
1052
+ it("#6866 R2b: a buyer bound to another provider checks out anonymously", async () => {
1053
+ givenUser({ billingProvider: "paddle", billingCustomerId: "ctm_other" });
1054
+
1055
+ const res = await signedIn();
1056
+
1057
+ expect(res.statusCode).toBe(201);
1058
+ expect(mockCreateCheckoutSession.mock.calls[0][0]).not.toHaveProperty(
1059
+ "buyer",
1060
+ );
1061
+ expect(createdLog()).toMatchObject({
1062
+ buyer: "anonymous",
1063
+ identity: "foreign_provider",
1064
+ });
1065
+ });
1066
+
1067
+ it("#6866 R3: an anonymous body-supplied ailk_user_id reaches neither buyer nor metadata", async () => {
1068
+ const res = await post(
1069
+ {},
1070
+ {
1071
+ ...VALID_BODY,
1072
+ ailk_user_id: "usr_attacker",
1073
+ metadata: { ailk_user_id: "usr_attacker" },
1074
+ },
1075
+ );
1076
+
1077
+ expect(res.statusCode).toBe(201);
1078
+ const arg = mockCreateCheckoutSession.mock.calls[0][0];
1079
+ expect(arg).not.toHaveProperty("buyer");
1080
+ expect(arg).not.toHaveProperty("metadata");
1081
+ expect(arg).not.toHaveProperty("ailk_user_id");
1082
+ });
1083
+
1084
+ it("#6866 R4: signed in, a body-supplied ailk_user_id loses to the session's user", async () => {
1085
+ await signedIn({
1086
+ ...VALID_BODY,
1087
+ ailk_user_id: "usr_attacker",
1088
+ metadata: { ailk_user_id: "usr_attacker" },
1089
+ });
1090
+
1091
+ const arg = mockCreateCheckoutSession.mock.calls[0][0];
1092
+ expect(arg.buyer.userId).toBe("usr_buyer");
1093
+ expect(arg).not.toHaveProperty("metadata");
1094
+ });
1095
+
1096
+ it("#6866 R5: with no cookie, the call is exactly today's argument object", async () => {
1097
+ const res = await post({});
1098
+
1099
+ expect(res.statusCode).toBe(201);
1100
+ expect(res.json()).toEqual({
1101
+ sessionId: "cs_test_abc12345xyz",
1102
+ url: "https://checkout.stripe.com/c/pay/cs_test_abc12345xyz",
1103
+ });
1104
+ expect(mockCreateCheckoutSession.mock.calls[0][0]).toEqual({
1105
+ productCode: VALID_BODY.productCode,
1106
+ successUrl: VALID_BODY.successUrl,
1107
+ cancelUrl: VALID_BODY.cancelUrl,
1108
+ quantity: 1,
1109
+ });
1110
+ expect(prisma.session.findUnique).not.toHaveBeenCalled();
1111
+ expect(prisma.user.findUnique).not.toHaveBeenCalled();
1112
+ expect(createdLog()).toMatchObject({ buyer: "anonymous" });
1113
+ expect(createdLog()).not.toHaveProperty("identity");
1114
+ });
1115
+
1116
+ it.each([
1117
+ [
1118
+ "an unknown session",
1119
+ () => (prisma.session.findUnique as jest.Mock).mockResolvedValue(null),
1120
+ {},
1121
+ "unknown_session",
1122
+ ],
1123
+ [
1124
+ "an expired session",
1125
+ () => givenSession({ expiresAt: new Date(Date.now() - 1_000) }),
1126
+ {},
1127
+ "expired",
1128
+ ],
1129
+ [
1130
+ "a cross-origin Origin",
1131
+ () => undefined,
1132
+ { origin: "https://evil.example.com" },
1133
+ "cross_origin",
1134
+ ],
1135
+ [
1136
+ "a rejecting session lookup",
1137
+ () =>
1138
+ (prisma.session.findUnique as jest.Mock).mockRejectedValue(
1139
+ new Error("db down"),
1140
+ ),
1141
+ {},
1142
+ "lookup_failed",
1143
+ ],
1144
+ [
1145
+ "an unverified email",
1146
+ () => givenUser({ emailVerified: false }),
1147
+ {},
1148
+ "email_unverified",
1149
+ ],
1150
+ ["a missing User row", () => givenUser(null), {}, "user_not_found"],
1151
+ [
1152
+ "a rejecting user lookup after a valid cookie",
1153
+ () =>
1154
+ (prisma.user.findUnique as jest.Mock).mockRejectedValue(
1155
+ new Error("db down"),
1156
+ ),
1157
+ {},
1158
+ "buyer_lookup_failed",
1159
+ ],
1160
+ ])(
1161
+ "#6866 R6: %s is an anonymous 201, never an error",
1162
+ async (_label, arrange, headers, identity) => {
1163
+ arrange();
1164
+
1165
+ const res = await post({
1166
+ cookie: COOKIE,
1167
+ origin: SAME_ORIGIN,
1168
+ ...(headers as Record<string, string>),
1169
+ });
1170
+
1171
+ expect(res.statusCode).toBe(201);
1172
+ expect(mockCreateCheckoutSession.mock.calls[0][0]).not.toHaveProperty(
1173
+ "buyer",
1174
+ );
1175
+ expect(createdLog()).toMatchObject({ buyer: "anonymous", identity });
1176
+ },
1177
+ );
1178
+
1179
+ it("#6866 R7: a bearer token alone never stamps, and apiToken is never read", async () => {
1180
+ const res = await post({ authorization: "Bearer tok_valid_looking_123" });
1181
+
1182
+ expect(res.statusCode).toBe(201);
1183
+ expect(mockCreateCheckoutSession.mock.calls[0][0]).not.toHaveProperty(
1184
+ "buyer",
1185
+ );
1186
+ expect(prisma.apiToken.findUnique).not.toHaveBeenCalled();
1187
+ expect(prisma.session.findUnique).not.toHaveBeenCalled();
1188
+ });
1189
+
1190
+ it("#6866 R8: the log names the buyer kind, never the user, email, customer or cookie", async () => {
1191
+ givenUser({ billingProvider: "stripe", billingCustomerId: "cus_bound" });
1192
+
1193
+ await signedIn();
1194
+
1195
+ expect(createdLog()).toMatchObject({ buyer: "signed_in" });
1196
+ expect(createdLog()).not.toHaveProperty("identity");
1197
+ expect(rawLog).not.toContain("usr_buyer");
1198
+ expect(rawLog).not.toContain("buyer@example.com");
1199
+ expect(rawLog).not.toContain("cus_");
1200
+ expect(rawLog).not.toContain(SESSION_TOKEN);
1201
+ });
1202
+
1203
+ it("#6866 R9: a cross-origin cookie logs identity cross_origin", async () => {
1204
+ await post({ cookie: COOKIE, origin: "https://evil.example.com" });
1205
+
1206
+ expect(createdLog()).toMatchObject({
1207
+ buyer: "anonymous",
1208
+ identity: "cross_origin",
1209
+ });
1210
+ });
1211
+
1212
+ it("#6866 R10: a generic Stripe failure on a signed-in call is still 502 checkout-unavailable", async () => {
1213
+ mockCreateCheckoutSession.mockRejectedValue(new Error("stripe down"));
1214
+
1215
+ const res = await signedIn();
1216
+
1217
+ expect(res.statusCode).toBe(502);
1218
+ expect(res.json()).toEqual({ error: "checkout-unavailable" });
1219
+ expect(mockCreateCheckoutSession).toHaveBeenCalledTimes(1);
1220
+ });
1221
+
1222
+ it("#6866 R12: a buyer the library's schema refuses retries once as anonymous (F7)", async () => {
1223
+ givenUser({ billingProvider: "stripe", billingCustomerId: "not_a_cus" });
1224
+ mockCreateCheckoutSession.mockRejectedValueOnce(
1225
+ Object.assign(new Error("invalid buyer"), { name: "ZodError" }),
1226
+ );
1227
+
1228
+ const res = await signedIn();
1229
+
1230
+ expect(res.statusCode).toBe(201);
1231
+ expect(mockCreateCheckoutSession).toHaveBeenCalledTimes(2);
1232
+ expect(mockCreateCheckoutSession.mock.calls[1][0]).not.toHaveProperty(
1233
+ "buyer",
1234
+ );
1235
+ expect(createdLog()).toMatchObject({
1236
+ buyer: "anonymous",
1237
+ identity: "buyer_invalid",
1238
+ });
1239
+ });
1240
+
1241
+ it("#6866 R12b: an anonymous ZodError is not retried — today's 502 mapping", async () => {
1242
+ mockCreateCheckoutSession.mockRejectedValue(
1243
+ Object.assign(new Error("invalid"), { name: "ZodError" }),
1244
+ );
1245
+
1246
+ const res = await post({});
1247
+
1248
+ expect(res.statusCode).toBe(502);
1249
+ expect(mockCreateCheckoutSession).toHaveBeenCalledTimes(1);
1250
+ });
1251
+
1252
+ it("#6866 R11: a pinned customer Stripe rejects retries once as anonymous", async () => {
1253
+ givenUser({ billingProvider: "stripe", billingCustomerId: "cus_bound" });
1254
+ mockCreateCheckoutSession.mockRejectedValueOnce(new UnknownCustomerError());
1255
+
1256
+ const res = await signedIn();
1257
+
1258
+ expect(res.statusCode).toBe(201);
1259
+ expect(mockCreateCheckoutSession).toHaveBeenCalledTimes(2);
1260
+ expect(mockCreateCheckoutSession.mock.calls[0][0].buyer.customerId).toBe(
1261
+ "cus_bound",
1262
+ );
1263
+ expect(mockCreateCheckoutSession.mock.calls[1][0]).not.toHaveProperty(
1264
+ "buyer",
1265
+ );
1266
+ expect(createdLog()).toMatchObject({
1267
+ buyer: "anonymous",
1268
+ identity: "stale_customer",
1269
+ });
1270
+ });
1271
+ });
1272
+
1273
+ // ── Same-origin redirect lock (#6877) ─────────────────────────────────────────
1274
+
1275
+ describe("POST /v1/checkout/sessions — same-origin redirect lock (#6877)", () => {
1276
+ const SITE_ORIGIN = "https://example.com";
1277
+ const GATE_TOKEN = "x-auth-fixture-for-6877";
1278
+ const SESSION_TOKEN = "sess_tok_6877";
1279
+ const COOKIE = `better-auth.session_token=${SESSION_TOKEN}.signature`;
1280
+ const originalDatabaseUrl = process.env.DATABASE_URL;
1281
+
1282
+ let app: FastifyInstance;
1283
+ let logLines: Array<Record<string, unknown>>;
1284
+ let rawLog: string;
1285
+
1286
+ function post(
1287
+ payload: Record<string, unknown>,
1288
+ headers: Record<string, string> = {},
1289
+ ) {
1290
+ return app.inject({
1291
+ method: "POST",
1292
+ url: "/v1/checkout/sessions",
1293
+ headers,
1294
+ payload,
1295
+ });
1296
+ }
1297
+
1298
+ beforeEach(async () => {
1299
+ jest.clearAllMocks();
1300
+ config.FRONTEND_URL = SITE_ORIGIN;
1301
+ process.env["STRIPE_SECRET_KEY"] = "sk_test_route_fixture";
1302
+ delete process.env["OSS_API_INTERNAL_SECRET"];
1303
+ mockAssertSandboxKey.mockImplementation(() => undefined);
1304
+ mockCreateCheckoutSession.mockResolvedValue({
1305
+ sessionId: "cs_test_lock6877",
1306
+ url: "https://checkout.stripe.com/c/pay/cs_test_lock6877",
1307
+ });
1308
+
1309
+ logLines = [];
1310
+ rawLog = "";
1311
+ app = require("fastify")({
1312
+ logger: {
1313
+ level: "info",
1314
+ stream: {
1315
+ write: (line: string) => {
1316
+ rawLog += line;
1317
+ logLines.push(JSON.parse(line) as Record<string, unknown>);
1318
+ },
1319
+ },
1320
+ },
1321
+ });
1322
+ app.register(checkoutRoutes);
1323
+ await app.ready();
1324
+ });
1325
+
1326
+ afterEach(async () => {
1327
+ await app.close();
1328
+ delete process.env["STRIPE_SECRET_KEY"];
1329
+ delete process.env["OSS_API_INTERNAL_SECRET"];
1330
+ config.FRONTEND_URL = SITE_ORIGIN;
1331
+ if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL;
1332
+ else process.env.DATABASE_URL = originalDatabaseUrl;
1333
+ });
1334
+
1335
+ it("#6877 R1: an off-origin successUrl is refused with 400 and no Stripe call", async () => {
1336
+ const res = await post({
1337
+ ...VALID_BODY,
1338
+ successUrl: "https://evil.test/x",
1339
+ });
1340
+
1341
+ expect(res.statusCode).toBe(400);
1342
+ const body = res.json();
1343
+ expect(body.error).toBe("validation-error");
1344
+ expect(body.details).toHaveLength(1);
1345
+ expect(body.details[0].path).toEqual(["successUrl"]);
1346
+ expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
1347
+ });
1348
+
1349
+ it("#6877 R2: an off-origin cancelUrl alone is refused with 400 and no Stripe call", async () => {
1350
+ const res = await post({ ...VALID_BODY, cancelUrl: "https://evil.test/x" });
1351
+
1352
+ expect(res.statusCode).toBe(400);
1353
+ expect(res.json().details[0].path).toEqual(["cancelUrl"]);
1354
+ expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
1355
+ });
1356
+
1357
+ /** Lookalikes WHATWG still parses to the site origin — refused only by the
1358
+ * raw-string checks (D2 checks 1 and 2). */
1359
+ const PARSER_DIFFERENTIAL = [
1360
+ "https://example.com\\@evil.test/x",
1361
+ "https://evil.test%40@example.com/",
1362
+ "https:example.com/x",
1363
+ "https://\uff45xample.com/x",
1364
+ " https://example.com/x",
1365
+ "https://example.com\t/x",
1366
+ ];
1367
+
1368
+ it("#6877 R3 precondition: every parser-differential row parses to the site origin", () => {
1369
+ for (const raw of PARSER_DIFFERENTIAL) {
1370
+ expect(new URL(raw).origin).toBe(SITE_ORIGIN);
1371
+ }
1372
+ });
1373
+
1374
+ it.each([
1375
+ "https://example.com.evil.test/x",
1376
+ "https://example.com@evil.test/x",
1377
+ "http://example.com/x",
1378
+ "https://example.com:8443/x",
1379
+ ...PARSER_DIFFERENTIAL,
1380
+ ])("#6877 R3: the lookalike %j is refused", async (successUrl) => {
1381
+ const res = await post({ ...VALID_BODY, successUrl });
1382
+
1383
+ expect(res.statusCode).toBe(400);
1384
+ expect(res.json().error).toBe("validation-error");
1385
+ expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
1386
+ });
1387
+
1388
+ it("#6877 R3b: a raw space on the site origin is refused (D2 check 1)", async () => {
1389
+ const res = await post({
1390
+ ...VALID_BODY,
1391
+ successUrl: "https://example.com/x y",
1392
+ });
1393
+
1394
+ expect(res.statusCode).toBe(400);
1395
+ expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
1396
+ });
1397
+
1398
+ it("#6877 R4: the refused URL is never echoed or logged", async () => {
1399
+ const res = await post({
1400
+ ...VALID_BODY,
1401
+ successUrl: "https://evil.test/x",
1402
+ });
1403
+
1404
+ expect(res.statusCode).toBe(400);
1405
+ expect(res.body).not.toContain("evil.test");
1406
+ expect(rawLog).not.toContain("evil.test");
1407
+ const refusals = logLines.filter(
1408
+ (l) => l["outcome"] === "redirect_origin_refused",
1409
+ );
1410
+ expect(refusals).toHaveLength(1);
1411
+ expect(refusals[0]).toMatchObject({ fields: ["successUrl"] });
1412
+ });
1413
+
1414
+ it("#6877 R5: a same-origin checkout is forwarded byte-for-byte", async () => {
1415
+ const successUrl =
1416
+ "https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}";
1417
+ const cancelUrl = "https://example.com/purchase/cancel";
1418
+
1419
+ const res = await post({ productCode: "ailk-pro", successUrl, cancelUrl });
1420
+
1421
+ expect(res.statusCode).toBe(201);
1422
+ expect(mockCreateCheckoutSession).toHaveBeenCalledWith(
1423
+ expect.objectContaining({ successUrl, cancelUrl }),
1424
+ );
1425
+ });
1426
+
1427
+ it("#6877 R6: an authenticated internal caller keeps its two-host redirects", async () => {
1428
+ process.env["OSS_API_INTERNAL_SECRET"] = GATE_TOKEN;
1429
+ const successUrl = "https://app.ailaunchkit.ai/dashboard?checkout=success";
1430
+ const cancelUrl = "https://ailaunchkit.ai/pricing?checkout=cancel";
1431
+
1432
+ const res = await post(
1433
+ { productCode: "ailk-pro", successUrl, cancelUrl },
1434
+ { "x-internal-auth": GATE_TOKEN },
1435
+ );
1436
+
1437
+ expect(res.statusCode).toBe(201);
1438
+ expect(mockCreateCheckoutSession).toHaveBeenCalledWith(
1439
+ expect.objectContaining({ successUrl, cancelUrl }),
1440
+ );
1441
+ });
1442
+
1443
+ it("#6877 R7: the gate refuses a missing header with 401 before any origin check", async () => {
1444
+ process.env["OSS_API_INTERNAL_SECRET"] = GATE_TOKEN;
1445
+
1446
+ const res = await post({
1447
+ ...VALID_BODY,
1448
+ successUrl: "https://evil.test/x",
1449
+ });
1450
+
1451
+ expect(res.statusCode).toBe(401);
1452
+ expect(res.json()).toEqual({ error: "internal-auth-required" });
1453
+ expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
1454
+ });
1455
+
1456
+ it.each([
1457
+ [
1458
+ "an unparseable FRONTEND_URL",
1459
+ () => {
1460
+ config.FRONTEND_URL = "not a url";
1461
+ },
1462
+ ],
1463
+ [
1464
+ "a non-http(s) FRONTEND_URL",
1465
+ () => {
1466
+ config.FRONTEND_URL = "mailto:ops@example.com";
1467
+ },
1468
+ ],
1469
+ [
1470
+ "an unset FRONTEND_URL in production",
1471
+ () => {
1472
+ delete process.env.FRONTEND_URL;
1473
+ config.NODE_ENV = "production";
1474
+ },
1475
+ ],
1476
+ ])(
1477
+ "#6877 R8: %s answers 500 config-error with no Stripe call",
1478
+ async (_label, arrange) => {
1479
+ const envFrontendUrl = process.env.FRONTEND_URL;
1480
+ const nodeEnv = config.NODE_ENV;
1481
+ try {
1482
+ arrange();
1483
+ const res = await post(VALID_BODY);
1484
+
1485
+ expect(res.statusCode).toBe(500);
1486
+ expect(res.json()).toEqual({ error: "config-error" });
1487
+ expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
1488
+ } finally {
1489
+ config.FRONTEND_URL = SITE_ORIGIN;
1490
+ config.NODE_ENV = nodeEnv;
1491
+ process.env.FRONTEND_URL = envFrontendUrl;
1492
+ }
1493
+ },
1494
+ );
1495
+
1496
+ it("#6877 R9: a refused request never reads the session cookie's row", async () => {
1497
+ process.env.DATABASE_URL = "postgresql://test:test@localhost/test";
1498
+ (prisma.session.findUnique as jest.Mock).mockResolvedValue(null);
1499
+ const headers = { cookie: COOKIE, origin: SITE_ORIGIN };
1500
+
1501
+ // Control: the same signed-in request on-origin DOES reach the lookup.
1502
+ const control = await post(VALID_BODY, headers);
1503
+ expect(control.statusCode).toBe(201);
1504
+ expect(prisma.session.findUnique).toHaveBeenCalled();
1505
+ jest.clearAllMocks();
1506
+
1507
+ const res = await post(
1508
+ { ...VALID_BODY, successUrl: "https://evil.test/x" },
1509
+ headers,
1510
+ );
1511
+
1512
+ expect(res.statusCode).toBe(400);
1513
+ expect(prisma.session.findUnique).not.toHaveBeenCalled();
1514
+ expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
1515
+ });
1516
+ });