create-ailk 0.8.0 → 0.10.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 (238) hide show
  1. package/README.md +71 -3
  2. package/component-catalog.json +279 -45
  3. package/dist/add-module.d.ts +20 -0
  4. package/dist/add-module.js +43 -7
  5. package/dist/add-route.d.ts +56 -0
  6. package/dist/add-route.js +286 -0
  7. package/dist/apply-design-md.d.ts +7 -0
  8. package/dist/apply-design-md.js +50 -0
  9. package/dist/apply-design.d.ts +64 -0
  10. package/dist/apply-design.js +273 -0
  11. package/dist/claude-md.d.ts +111 -0
  12. package/dist/claude-md.js +334 -0
  13. package/dist/cli.js +104 -5
  14. package/dist/derive-architecture.js +41 -36
  15. package/dist/design-compile.d.ts +71 -0
  16. package/dist/design-compile.js +98 -0
  17. package/dist/design-package.d.ts +83 -0
  18. package/dist/design-package.js +468 -0
  19. package/dist/design-presets.d.ts +86 -0
  20. package/dist/design-presets.js +642 -0
  21. package/dist/design-scaffold.d.ts +76 -0
  22. package/dist/design-scaffold.js +220 -0
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +4 -1
  25. package/dist/legacy-nav-renderers.d.ts +45 -0
  26. package/dist/legacy-nav-renderers.js +545 -0
  27. package/dist/lib/auth-secret-file.d.ts +19 -0
  28. package/dist/lib/auth-secret-file.js +112 -0
  29. package/dist/lib/design-checks.d.ts +78 -0
  30. package/dist/lib/design-checks.js +174 -0
  31. package/dist/lib/license-key-files.d.ts +9 -0
  32. package/dist/lib/license-key-files.js +2 -2
  33. package/dist/lib/module-license-gate.d.ts +48 -32
  34. package/dist/lib/module-license-gate.js +117 -8
  35. package/dist/lib/render-verdict.d.ts +52 -0
  36. package/dist/lib/render-verdict.js +77 -0
  37. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  38. package/dist/lib/scaffold-render-sweep.js +236 -0
  39. package/dist/module-architecture.d.ts +19 -16
  40. package/dist/module-architecture.js +67 -23
  41. package/dist/parse-args.d.ts +12 -2
  42. package/dist/parse-args.js +74 -13
  43. package/dist/programmatic.d.ts +30 -6
  44. package/dist/programmatic.js +216 -19
  45. package/dist/site-chrome.d.ts +19 -9
  46. package/dist/site-chrome.js +23 -13
  47. package/dist/site-manifest.d.ts +25 -0
  48. package/dist/site-manifest.js +254 -0
  49. package/dist/site-roster.d.ts +41 -0
  50. package/dist/site-roster.js +141 -8
  51. package/dist/starter-grid.d.ts +16 -63
  52. package/dist/starter-grid.js +102 -84
  53. package/dist/surfaces.d.ts +41 -9
  54. package/dist/surfaces.js +200 -83
  55. package/dist/template-manifest.d.ts +30 -13
  56. package/dist/template-manifest.js +53 -28
  57. package/package.json +6 -3
  58. package/release-scope.json +25 -0
  59. package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
  60. package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
  61. package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
  62. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  63. package/site-templates/designs/minimalism/sample/README.md +48 -0
  64. package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +1 -1
  65. package/site-templates/designs/standard/DESIGN.md +98 -6
  66. package/site-templates/marketing-site/DESIGN.md +98 -6
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
  68. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
  69. package/site-templates/marketing-site-free/DESIGN.md +98 -6
  70. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
  71. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
  72. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  73. package/site-templates/saas-product/DESIGN.md +98 -6
  74. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
  75. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
  76. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  77. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
  78. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
  79. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  80. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
  82. package/site-templates/shapes/person-service/ailk-template.json +1 -1
  83. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
  84. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
  85. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
  86. package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
  87. package/templates/.claude/agents/web.md +10 -8
  88. package/templates/.claude/rules/architecture.md +17 -7
  89. package/templates/.claude/skills/README.md +3 -2
  90. package/templates/.claude/skills/add-auth/SKILL.md +142 -90
  91. package/templates/.claude/skills/add-auth/references/env-vars.md +32 -17
  92. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +17 -8
  93. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  94. package/templates/.claude/skills/add-page/references/page-types.md +94 -86
  95. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  96. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  97. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  98. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  99. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  100. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  101. package/templates/.claude/skills/check-site/SKILL.md +51 -18
  102. package/templates/.claude/skills/check-site/references/common-errors.md +12 -9
  103. package/templates/.claude/skills/check-site/references/findings-glossary.md +4 -4
  104. package/templates/.claude/skills/check-site/references/severity-levels.md +3 -3
  105. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  106. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  107. package/templates/.claude/skills/setup-deploy/SKILL.md +75 -38
  108. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  109. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  110. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  111. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  112. package/templates/.env.example +23 -2
  113. package/templates/.github/workflows/ci.yml +4 -0
  114. package/templates/.prettierignore +1 -0
  115. package/templates/CLAUDE.md +146 -103
  116. package/templates/DESIGN.md +98 -6
  117. package/templates/README.md +50 -38
  118. package/templates/apps/api/.env.example +2 -1
  119. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  120. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  121. package/templates/apps/api/src/lib/stripe.ts +9 -0
  122. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  123. package/templates/apps/api/src/middleware/auth.ts +54 -0
  124. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  125. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  126. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  127. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  128. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  129. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  130. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  131. package/templates/apps/api/src/server.ts +28 -0
  132. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  133. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  134. package/templates/apps/api/src/services/revalidation.ts +60 -15
  135. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  136. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  137. package/templates/apps/web/.env.example +13 -0
  138. package/templates/apps/web/CLAUDE.md +22 -28
  139. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  140. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  141. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  142. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  143. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  144. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  145. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  146. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  147. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
  148. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  149. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  150. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  151. package/templates/apps/web/app/[locale]/layout.tsx +35 -19
  152. package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
  153. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
  154. package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
  155. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  156. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  157. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  158. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  159. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  160. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  161. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  162. package/templates/apps/web/lib/locale-guard.ts +18 -0
  163. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  164. package/templates/apps/web/lib/nav-config.ts +14 -13
  165. package/templates/apps/web/lib/site-brand.tsx +13 -9
  166. package/templates/apps/web/lib/site-logo.tsx +36 -0
  167. package/templates/apps/web/lib/site-theme.ts +46 -26
  168. package/templates/apps/web/messages/en.json +23 -9
  169. package/templates/apps/web/middleware.ts +28 -4
  170. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  171. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  172. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  173. package/templates/content/en/BookCall/start-project.mdx +56 -0
  174. package/templates/content/en/ContactPage/contact.mdx +40 -1
  175. package/templates/content/en/HomePage/home.mdx +2 -6
  176. package/templates/content/en/LandingPage/meet.mdx +59 -0
  177. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  178. package/templates/database/CHANGELOG.md +7 -0
  179. package/templates/database/package.json +1 -1
  180. package/templates/package.json +4 -1
  181. package/templates/tools/check-claude-md.ts +128 -0
  182. package/versions.json +33 -33
  183. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  184. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  185. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  186. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  187. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  188. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  189. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  190. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  191. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  192. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  193. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  194. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  195. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  196. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  197. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  198. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  199. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
  200. package/site-templates/minimalism-site/DESIGN.md +0 -162
  201. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +0 -506
  202. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
  203. package/site-templates/minimalism-site/ailk-template.json +0 -23
  204. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  205. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
  206. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  207. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  208. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  209. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  210. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
  211. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  212. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  213. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  214. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  215. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
  216. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
  217. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
  218. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
  219. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  220. package/site-templates/minimalism-site/provenance.json +0 -4
  221. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  222. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +0 -175
  223. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +0 -8
  224. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +0 -12
  225. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +0 -12
  226. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
  227. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  228. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  229. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  230. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  231. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  232. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  233. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  234. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  235. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  236. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  237. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  238. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
@@ -29,7 +29,7 @@ Open <http://localhost:3000>. The home page renders from `content/en/HomePage/ho
29
29
  To verify everything is wired before committing or deploying:
30
30
 
31
31
  ```bash
32
- pnpm preflight # architecture + types + lint + build
32
+ pnpm preflight # architecture + CLAUDE.md drift + types + lint + build
33
33
  pnpm build # production build
34
34
  ```
35
35
 
@@ -43,28 +43,29 @@ The pages of the Free `organization-product` shape, and where each one's copy li
43
43
 
44
44
  <!-- generated:begin content -->
45
45
 
46
- | Page | Content file | Ships in |
47
- | ------------------- | --------------------------------------------------- | ------------------------------ |
48
- | `/` | `content/en/HomePage/home.mdx` | every scaffold |
49
- | `/features` | `content/en/FeaturesList/features.mdx` | `--shape organization-product` |
50
- | `/features/[slug]` | — (a code route) | `--shape organization-product` |
51
- | `/use-cases` | `content/en/UseCasesList/use-cases.mdx` | `--shape organization-product` |
52
- | `/use-cases/[slug]` | — (a code route) | `--shape organization-product` |
53
- | `/pricing` | `content/en/Pricing/pricing.mdx` | `--shape organization-product` |
54
- | `/about` | `content/en/AboutPage/about.mdx` | every scaffold |
55
- | `/faq` | `content/en/FAQPage/faq.mdx` | every scaffold |
56
- | `/contact` | `content/en/ContactPage/contact.mdx` | every scaffold |
57
- | `/glossary` | `content/en/GlossaryList/glossary.mdx` | `--shape organization-product` |
58
- | `/newsletter` | `content/en/NewsletterSignup/newsletter.mdx` | `--shape organization-product` |
59
- | `/book-a-call` | `content/en/BookCall/book-call.mdx` | `--shape organization-product` |
60
- | `/sign-in` | — (a code route) | every scaffold |
61
- | `/sign-up` | — (a code route) | every scaffold |
62
- | `/privacy` | `content/en/PrivacyPage/privacy.mdx` | `--shape organization-product` |
63
- | `/terms` | `content/en/TermsPage/terms.mdx` | `--shape organization-product` |
64
- | `/purchase-policy` | `content/en/PurchasePolicyPage/purchase-policy.mdx` | every scaffold |
65
- | `/blog` | `content/en/BlogList/blog.mdx` | every scaffold |
66
- | `/portfolio` | `content/en/PortfolioList/portfolio.mdx` | `--shape organization-product` |
67
- | `/portfolio/[slug]` | — (a code route) | `--shape organization-product` |
46
+ | Page | Content file | Ships in |
47
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
48
+ | `/` | `content/en/HomePage/home.mdx` | every scaffold |
49
+ | `/features` | `content/en/FeaturesList/features.mdx` | `--shape organization-product` |
50
+ | `/features/[slug]` | — (a code route) | `--shape organization-product` |
51
+ | `/for` | `content/en/UseCasesList/for.mdx` | `--shape organization-product` |
52
+ | `/for/[slug]` | — (a code route) | `--shape organization-product` |
53
+ | `/pricing` | `content/en/Pricing/pricing.mdx` | `--shape organization-product` |
54
+ | `/about` | `content/en/AboutPage/about.mdx` | every scaffold |
55
+ | `/faq` | `content/en/FAQPage/faq.mdx` | every scaffold |
56
+ | `/contact` | `content/en/ContactPage/contact.mdx` | every scaffold |
57
+ | `/glossary` | `content/en/GlossaryList/glossary.mdx` | `--shape organization-product` |
58
+ | `/newsletter` | `content/en/NewsletterSignup/newsletter.mdx` | `--shape organization-product` |
59
+ | `/sign-in` | — (a code route) | every scaffold |
60
+ | `/sign-up` | — (a code route) | every scaffold |
61
+ | `/privacy` | `content/en/PrivacyPage/privacy.mdx` | `--shape organization-product` |
62
+ | `/terms` | `content/en/TermsPage/terms.mdx` | `--shape organization-product` |
63
+ | `/purchase-policy` | `content/en/PurchasePolicyPage/purchase-policy.mdx` | every scaffold |
64
+ | `/blog` | `content/en/BlogList/blog.mdx` | every scaffold |
65
+ | `/meet` | `content/en/LandingPage/meet.mdx` | every scaffold |
66
+ | `/meet/[slug]` | `content/en/BookCall/book-a-call.mdx`, `content/en/BookCall/onsite-estimate.mdx`, `content/en/BookCall/start-project.mdx`, `content/en/BookCall/onsite-service.mdx` | every scaffold |
67
+ | `/portfolio` | `content/en/PortfolioList/portfolio.mdx` | `--shape organization-product` |
68
+ | `/portfolio/[slug]` | — (a code route) | `--shape organization-product` |
68
69
 
69
70
  <!-- generated:end -->
70
71
 
@@ -72,7 +73,7 @@ A page marked `--shape organization-product` is in your site when you scaffolded
72
73
 
73
74
  To customize the home page, edit [`content/en/HomePage/home.mdx`](./content/en/HomePage/home.mdx). Save and reload the dev server. No build step.
74
75
 
75
- To add a new page, ask Claude Code to run the `add-page` skill — it writes the route, the MDX content stub and the page's typed JSON-LD in one confirm-before-write pass. See [Skills](#skills).
76
+ To add a new page, ask Claude Code to run the `add-page` skill — it writes the page's content file and nav link in one confirm-before-write pass, and adds a missing route with `create-ailk add-route` first. See [Skills](#skills).
76
77
 
77
78
  > `content/_site.mdx` stays at the top level (not under `content/en/`) — site identity is locale-blind.
78
79
 
@@ -84,13 +85,17 @@ Everything that makes this site yours lives in three places. Nothing in `apps/*`
84
85
  | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
85
86
  | Site name, canonical URL, default locale | `content/_site.mdx` — `siteName`, `baseUrl`, `defaultLocale` |
86
87
  | Who the site is (the entity answer engines read) | `content/_site.mdx` — `siteIdentity` (`kind`, `name`, `url`, `logo`, `description`, `sameAs`) |
87
- | Colours, light and dark | `content/_site.mdx` — `theme.colors.light` / `theme.colors.dark` (`bg`, `text`, `primary`, `accent`, …) |
88
+ | Your brand colours, light and dark | `content/_site.mdx` — `brand.colors.light` / `brand.colors.dark` (`primary`, `primaryFg`, `accent`) |
89
+ | Your fonts | `content/_site.mdx` — `brand.fonts` (`display`, `body`) |
90
+ | The design's own colours | `content/_site.mdx` — `theme.colors.light` / `theme.colors.dark` (`bg`, `text`, `primary`, `accent`, …) |
88
91
  | Corners, section-label case, image placeholders, check marks | `content/_site.mdx` — `theme.corners`, `theme.eyebrowCase`, `theme.placeholders`, `theme.checkmarks` |
89
92
  | Every page's copy and section order | `content/en/<Type>/<slug>.mdx` — the frontmatter `sections[]`; each section's `data` is its copy and its layout options |
90
93
  | Header links, footer columns, social links | `apps/web/lib/nav-config.ts` — generated at scaffold, yours to edit; labels come from `apps/web/messages/<locale>.json` |
91
94
  | Logo and brand assets | `apps/web/public/` and the `logo` field in `siteIdentity` |
92
95
 
93
- The `theme` block is the whole design surface. Declare only what you change — an absent colour role or shape knob keeps the platform default — and the build turns it into the CSS custom properties every component reads. There is no token file to fork and no provider to wrap. The [theme and brand guide](https://ailaunchkit.ai/docs/guides/customize-theme-and-brand) is where the full walkthrough will live.
96
+ Two blocks carry the look. `theme:` is the design's — its colours and shape knobs; `brand:` is yours — the logomark, `brand.colors` and `brand.fonts`, set on top of whatever design the site wears. Declare only what you change — an absent colour role or shape knob keeps the default — and the build turns both into the CSS custom properties every component reads. There is no token file to fork and no provider to wrap.
97
+
98
+ To change the whole look, apply another design: `pnpm create ailk@latest apply-design <name|path>` in the site root, with a shipped design name (`standard`, `minimalism`) or a folder holding a `DESIGN.md`. It rewrites `theme:`, the generated `apps/web/lib/site-chrome.ts` and the look fields of your sections; it never touches your routes, your copy or the `brand:` block. That is why your own colours belong in `brand.colors`, not `theme.colors`: applying a design replaces `theme:` wholesale. A design other than `standard` needs a Pro or Pro Plus key. The [theme and brand guide](https://ailaunchkit.ai/docs/guides/customize-theme-and-brand) is where the full walkthrough will live.
94
99
 
95
100
  A rebrand touches only the files in this table. If you find yourself editing a component, stop: the trait you want is either a section option (check the section's `data` fields), a theme knob, or a feature request for the [upstream library](https://github.com/working-theory-labs/ai-launch-kit).
96
101
 
@@ -164,7 +169,7 @@ The [deploy guide](https://ailaunchkit.ai/docs/guides/deploy-to-production) is w
164
169
 
165
170
  Paid levels and the commerce add-on are bought at <https://ailaunchkit.ai/pricing>; your licence key arrives by email.
166
171
 
167
- Today the key is used at scaffold time. Set `AILK_LICENSE_KEY` and scaffold with the shape you want — the key sets the level and switches on its modules:
172
+ For a new site, set `AILK_LICENSE_KEY` and scaffold with the shape you want — the key sets the level and switches on its modules:
168
173
 
169
174
  ```bash
170
175
  # Free — no key
@@ -173,9 +178,15 @@ pnpm create ailk@latest <dir> --shape <shape>
173
178
  AILK_LICENSE_KEY=<your key> pnpm create ailk@latest <dir> --shape <shape>
174
179
  ```
175
180
 
176
- `<shape>` is one of `organization-product`, `organization-service`, `local-business-service`, `person-service`. `--modules` trims the level's modules, or adds the commerce add-on or the workspace module. `--design minimalism` picks the other look and needs a paid key.
181
+ `<shape>` is one of `organization-product`, `organization-service`, `local-business-service`, `person-service`. `--modules` trims the level's modules, or adds the commerce add-on or the workspace module. `--design minimalism` picks the other look, and `--design <folder>` renders the starter in a design of your own (a folder holding a `DESIGN.md`); either needs a paid key.
182
+
183
+ To add a paid module to this site in place, set the key and run `add-module` in the site root:
184
+
185
+ ```bash
186
+ AILK_LICENSE_KEY=<your key> pnpm create ailk@latest add-module <module>
187
+ ```
177
188
 
178
- Applying a paid module to an existing site in place is not yet supported. To move this site onto a paid template, scaffold the new one and bring your `content/` directory and `content/_site.mdx` across.
189
+ It writes the module's files the site lacks and never overwrites one you have: a file whose content differs is left as-is and listed for review. It patches only `package.json` dependency and script keys, `next.config.*` and `jest.config.*`, then regenerates `architecture.yaml`. `pnpm create ailk@latest license set <key>` (or `license set -` to read the key from stdin, out of your shell history) keeps the key with the site — in `ailk.key` and `apps/api/.env`, both gitignored — and, when the site is linked to Vercel, sets it there and redeploys.
179
190
 
180
191
  ## Upgrading
181
192
 
@@ -193,15 +204,16 @@ This project ships Claude Code skills in `.claude/skills/`. Claude Code discover
193
204
 
194
205
  <!-- generated:begin skills -->
195
206
 
196
- | Skill | What it does |
197
- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
198
- | `add-page` | Generate a new page: App Router route + MDX content stub + typed JSON-LD, in one confirm-before-write pass. Also documents the `schema:` frontmatter override for adding supplementary structured data to a page that already exists. |
199
- | `add-auth` | Activate auth providers — magic-link via Resend, Google OAuth, GitHub OAuth. |
200
- | `setup-deploy` | Emit deploy configuration — `vercel.json`, `.env.example`, and `RUNBOOK.md` — for a Vercel deploy. Asks where leads go (email or a database) and warns when neither is set. |
201
- | `setup-database` | Take the site from no database to a working lead inbox: provision Postgres, create the tables, pin the owner, and verify one test message lands in `/leads`. |
202
- | `check-site` | Run `ailk-audit --json` and explain the score and findings in plain language. |
203
- | `fill-page` | Fill one page's section content from a category profile: labelled sample content (tier 1) or your own first draft from a short intake (tier 2). |
204
- | `upgrade-plan` | Compare Free, Pro, Pro Plus, Agency and the commerce add-on side by side, with the prices the public pricing page publishes right now, and recommend one option for this site with a reason. |
207
+ | Skill | What it does |
208
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
209
+ | `add-page` | Add a page this site can serve — its content file, structured data and nav link — adding a missing route with `create-ailk add-route` first. Also documents the `schema:` frontmatter override for any page. |
210
+ | `update-page` | Rename a page — give it a new URL and/or a new title — in one plan-then-confirm pass: its content files, its own route folder, exact internal links and the manifest row move together, and the old URL gets a permanent redirect. |
211
+ | `add-auth` | Activate auth providers — magic-link via Resend, Google OAuth, GitHub OAuth. |
212
+ | `setup-deploy` | Emit deploy configuration — `vercel.json`, `.env.example`, and `RUNBOOK.md` — for a Vercel deploy. Asks where leads go (email or a database) and warns when neither is set. |
213
+ | `setup-database` | Take the site from no database to a working lead inbox: provision Postgres, create the tables, pin the owner, and verify one test message lands in `/leads`. |
214
+ | `check-site` | Run `ailk-audit --json` and explain the score and findings in plain language. |
215
+ | `fill-page` | Fill one page's section content from a category profile: labelled sample content (tier 1) or your own first draft from a short intake (tier 2). |
216
+ | `upgrade-plan` | Compare Free, Pro, Pro Plus, Agency and the commerce add-on side by side, with the prices the public pricing page publishes right now, and recommend one option for this site with a reason. |
205
217
 
206
218
  <!-- generated:end -->
207
219
 
@@ -36,7 +36,8 @@ STRIPE_WEBHOOK_SECRET=whsec_xxx
36
36
  # =============================================================================
37
37
  # When set, POST /v1/checkout/sessions requires the X-Internal-Auth header
38
38
  # to match this value. Unset for OSS standalone deployments (no gate applied).
39
- # Must match OSS_API_INTERNAL_SECRET in apps-paid/marketing-api/.env.local.
39
+ # Must match OSS_API_INTERNAL_SECRET in apps-paid/marketing-api/.env.local
40
+ # and in apps/web's environment (the /dev/purchase Server Action, #6876).
40
41
  # Generate with: openssl rand -hex 32
41
42
  # OSS_API_INTERNAL_SECRET=your-shared-secret-here
42
43
 
@@ -78,6 +78,8 @@ jest.mock('@working-theory/stripe-client', () => ({
78
78
  StripeConfigError: class StripeConfigError extends Error {},
79
79
  UnknownProductCodeError: class UnknownProductCodeError extends Error {},
80
80
  UnknownSessionError: class UnknownSessionError extends Error {},
81
+ // #6866 — sessions.ts tests `instanceof UnknownCustomerError`.
82
+ UnknownCustomerError: class UnknownCustomerError extends Error {},
81
83
  WEBHOOK_TOLERANCE_SECONDS: 300,
82
84
  }));
83
85
 
@@ -469,3 +469,119 @@ describe("#6216 D11 — a lead_… path segment is truncated to 8 characters, cs
469
469
  }
470
470
  });
471
471
  });
472
+
473
+ // ── #6889 — production startup warns when a checkout protection is unset ─────
474
+ //
475
+ // POST /v1/checkout/sessions admits a caller without X-Internal-Auth when
476
+ // OSS_API_INTERNAL_SECRET is unset, and with FRONTEND_URL unset its redirect
477
+ // lock answers 500 while the CORS/cookie origin checks pin to localhost. Both
478
+ // are optional by design (OSS standalone compat), so startup must never fail
479
+ // on them — but a production boot says so, once per variable. Each case rebuilds the module graph because `config/index.ts`
480
+ // snapshots NODE_ENV at import time.
481
+
482
+ describe("#6889 — production startup warns on an unset FRONTEND_URL or OSS_API_INTERNAL_SECRET", () => {
483
+ const ENV_KEYS = [
484
+ "NODE_ENV",
485
+ "FRONTEND_URL",
486
+ "OSS_API_INTERNAL_SECRET",
487
+ // Cleared so a production boot never trips config's database-module exit.
488
+ "AILK_ENABLED_MODULES",
489
+ ] as const;
490
+ type EnvKey = (typeof ENV_KEYS)[number];
491
+ const priorEnv = new Map<string, string | undefined>(
492
+ ENV_KEYS.map((key) => [key, process.env[key]]),
493
+ );
494
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- server is typed via Fastify generics; explicit any avoids pulling in the full FastifyInstance type here
495
+ let app: any;
496
+
497
+ afterEach(async () => {
498
+ if (app) {
499
+ await app.close();
500
+ app = undefined;
501
+ }
502
+ for (const [key, value] of priorEnv) {
503
+ if (value === undefined) {
504
+ delete process.env[key];
505
+ } else {
506
+ process.env[key] = value;
507
+ }
508
+ }
509
+ jest.restoreAllMocks();
510
+ });
511
+
512
+ /** Boot a fresh module graph under `env` and return every warning message. */
513
+ async function bootWarnings(
514
+ env: Partial<Record<EnvKey, string>>,
515
+ ): Promise<string[]> {
516
+ for (const key of ENV_KEYS) {
517
+ const value = env[key];
518
+ if (value === undefined) {
519
+ delete process.env[key];
520
+ } else {
521
+ process.env[key] = value;
522
+ }
523
+ }
524
+ jest.resetModules();
525
+ const { logger } =
526
+ require("@working-theory/observability") as typeof import("@working-theory/observability");
527
+ const warn = jest.spyOn(logger, "warn");
528
+ const { build: freshBuild } =
529
+ require("../server") as typeof import("../server");
530
+ app = await freshBuild();
531
+ return warn.mock.calls.map((args) =>
532
+ args
533
+ .map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg)))
534
+ .join(" "),
535
+ );
536
+ }
537
+
538
+ const naming = (messages: string[], name: string) =>
539
+ messages.filter((message) => message.includes(name));
540
+
541
+ it("warns exactly once naming FRONTEND_URL when it is unset in production", async () => {
542
+ const messages = await bootWarnings({
543
+ NODE_ENV: "production",
544
+ OSS_API_INTERNAL_SECRET: "internal-secret",
545
+ });
546
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(1);
547
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(0);
548
+ });
549
+
550
+ it("warns exactly once naming OSS_API_INTERNAL_SECRET when it is unset in production", async () => {
551
+ const messages = await bootWarnings({
552
+ NODE_ENV: "production",
553
+ FRONTEND_URL: "https://site.example.com",
554
+ });
555
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(1);
556
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(0);
557
+ });
558
+
559
+ it("warns once for each when both are unset in production, and still starts", async () => {
560
+ const messages = await bootWarnings({ NODE_ENV: "production" });
561
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(1);
562
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(1);
563
+ const health = await app.inject({ method: "GET", url: "/health" });
564
+ expect(health.statusCode).toBe(200);
565
+ });
566
+
567
+ it("logs no warning for either when both are set in production", async () => {
568
+ const messages = await bootWarnings({
569
+ NODE_ENV: "production",
570
+ FRONTEND_URL: "https://site.example.com",
571
+ OSS_API_INTERNAL_SECRET: "internal-secret",
572
+ });
573
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(0);
574
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(0);
575
+ });
576
+
577
+ it.each(["development", "test"])(
578
+ "logs no warning for either outside production (NODE_ENV=%s), both unset",
579
+ async (nodeEnv) => {
580
+ const messages = await bootWarnings({ NODE_ENV: nodeEnv });
581
+ expect(naming(messages, "FRONTEND_URL")).toHaveLength(0);
582
+ expect(naming(messages, "OSS_API_INTERNAL_SECRET")).toHaveLength(0);
583
+ const health = await app.inject({ method: "GET", url: "/health" });
584
+ expect(health.statusCode).toBe(200);
585
+ },
586
+ );
587
+ });
@@ -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
  *
@@ -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