create-ailk 0.4.0 → 0.6.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 (396) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +12248 -0
  3. package/component-catalog.md +193 -68
  4. package/dist/cli.js +29 -1
  5. package/dist/derive-architecture.js +5 -2
  6. package/dist/lib/api-boot-assertion.d.ts +46 -0
  7. package/dist/lib/api-boot-assertion.js +70 -0
  8. package/dist/lib/extract-module-bundle.d.ts +50 -2
  9. package/dist/lib/extract-module-bundle.js +223 -15
  10. package/dist/lib/fetch-guard.d.ts +28 -0
  11. package/dist/lib/fetch-guard.js +101 -0
  12. package/dist/lib/fetch-module.js +4 -0
  13. package/dist/lib/fetch-template.js +4 -0
  14. package/dist/lib/import-graph.d.ts +29 -0
  15. package/dist/lib/import-graph.js +140 -0
  16. package/dist/module-architecture.d.ts +60 -8
  17. package/dist/module-architecture.js +587 -242
  18. package/dist/parse-args.js +6 -2
  19. package/dist/programmatic.d.ts +10 -0
  20. package/dist/programmatic.js +134 -19
  21. package/dist/site-chrome.d.ts +80 -0
  22. package/dist/site-chrome.js +135 -0
  23. package/dist/surfaces.d.ts +21 -6
  24. package/dist/surfaces.js +209 -36
  25. package/dist/template-manifest.d.ts +88 -3
  26. package/dist/template-manifest.js +281 -29
  27. package/dist/template-names.d.ts +1 -1
  28. package/dist/template-names.js +2 -0
  29. package/package.json +10 -2
  30. package/site-templates/_base/ailk-template.json +6 -0
  31. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
  34. package/site-templates/marketing-site/ailk-template.json +11 -0
  35. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  36. package/site-templates/marketing-site/provenance.json +4 -0
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
  39. package/site-templates/marketing-site-free/ailk-template.json +2 -1
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
  41. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
  42. package/site-templates/minimalism-site/ailk-template.json +22 -0
  43. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  44. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  49. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  50. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  51. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  53. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  54. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  55. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  56. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  57. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
  58. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  60. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  65. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  68. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  69. package/site-templates/minimalism-site/provenance.json +4 -0
  70. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  71. package/site-templates/saas-product/ailk-template.json +16 -0
  72. package/site-templates/saas-product/provenance.json +10 -0
  73. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  74. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  75. package/site-templates/simple-service/ailk-template.json +10 -0
  76. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  77. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  78. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  79. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  80. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  81. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  82. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  83. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  84. package/site-templates/simple-service/provenance.json +4 -0
  85. package/templates/.claude/skills/README.md +16 -25
  86. package/templates/.env.example +102 -12
  87. package/templates/.github/workflows/ci.yml +48 -0
  88. package/templates/README.md +3 -1
  89. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  90. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  91. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  92. package/templates/apps/api/.env.example +13 -0
  93. package/templates/apps/api/CLAUDE.md +29 -2
  94. package/templates/apps/api/eslint.config.mjs +69 -0
  95. package/templates/apps/api/jest.config.cjs +3 -0
  96. package/templates/apps/api/package.json +7 -2
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  98. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
  99. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  100. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  101. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  102. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
  103. package/templates/apps/api/src/config/index.ts +62 -0
  104. package/templates/apps/api/src/config/modules.ts +7 -3
  105. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
  106. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  107. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  108. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  109. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  110. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  111. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  112. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  113. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  114. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  115. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  116. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  117. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  118. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  119. package/templates/apps/api/src/lib/site.ts +208 -9
  120. package/templates/apps/api/src/lib/stripe.ts +66 -0
  121. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
  122. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
  123. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  124. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  125. package/templates/apps/api/src/openapi/spec.ts +543 -2
  126. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  127. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  128. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  129. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  130. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  131. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  132. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  133. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  134. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  135. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  136. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  137. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  138. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  139. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  140. package/templates/apps/api/src/routes/content/index.ts +13 -73
  141. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  142. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  143. package/templates/apps/api/src/routes/content/search.ts +129 -0
  144. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  145. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  146. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  147. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  148. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  149. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  150. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  151. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  152. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  153. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  154. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  155. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  156. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  157. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  158. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  159. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  160. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  161. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  162. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  163. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  164. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  165. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  166. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  167. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  168. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  169. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  170. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  171. package/templates/apps/api/src/server.ts +521 -55
  172. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  173. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  174. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  175. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  176. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  177. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  178. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  179. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  180. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  181. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  182. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  183. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  184. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  185. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  186. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  187. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  188. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  189. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  190. package/templates/apps/api/src/services/audit-log.ts +668 -0
  191. package/templates/apps/api/src/services/consent.ts +18 -0
  192. package/templates/apps/api/src/services/default-tenant.ts +300 -0
  193. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  194. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  195. package/templates/apps/api/src/services/entitlements.ts +80 -0
  196. package/templates/apps/api/src/services/lead-promotion.ts +160 -0
  197. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  198. package/templates/apps/api/src/services/leads-inbox.ts +284 -0
  199. package/templates/apps/api/src/services/listing-config.ts +27 -9
  200. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  201. package/templates/apps/api/src/services/project-listings.ts +201 -6
  202. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  203. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  204. package/templates/apps/api/src/services/revalidation.ts +173 -0
  205. package/templates/apps/api/src/services/verified-email.ts +71 -0
  206. package/templates/apps/api/src/vercel-handler.ts +75 -35
  207. package/templates/apps/mcp/.env.example +17 -0
  208. package/templates/apps/mcp/CLAUDE.md +58 -25
  209. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  210. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  211. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  212. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  213. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  214. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  215. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  216. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  217. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  218. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  219. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  220. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  221. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  222. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  223. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  224. package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
  225. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  226. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  227. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  228. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  229. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  230. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  231. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  232. package/templates/apps/mcp/eslint.config.mjs +69 -0
  233. package/templates/apps/mcp/jest.config.cjs +13 -0
  234. package/templates/apps/mcp/package.json +8 -2
  235. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  236. package/templates/apps/mcp/src/config/modules.ts +10 -5
  237. package/templates/apps/mcp/src/http-transport.ts +102 -29
  238. package/templates/apps/mcp/src/index.ts +73 -11
  239. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  240. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  241. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  242. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  243. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  244. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  245. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  246. package/templates/apps/mcp/src/server.ts +61 -37
  247. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  248. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  249. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  250. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  251. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  252. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  253. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  254. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  255. package/templates/apps/mcp/src/tools/index.ts +9 -7
  256. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  257. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  258. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  259. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  260. package/templates/apps/web/.env.example +74 -8
  261. package/templates/apps/web/CLAUDE.md +46 -0
  262. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  263. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  264. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  265. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  266. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  267. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  268. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  269. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  270. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  271. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  272. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  273. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  274. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  275. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  276. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  277. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  278. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  279. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  280. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  281. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
  282. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
  283. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  284. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  285. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  286. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  287. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  288. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  289. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  290. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  291. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  292. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  293. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  294. package/templates/apps/web/app/[locale]/layout.tsx +308 -65
  295. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  296. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  297. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  298. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  299. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  300. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  301. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  302. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  303. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  304. package/templates/apps/web/auth.ts +407 -8
  305. package/templates/apps/web/components/google-analytics.tsx +18 -1
  306. package/templates/apps/web/eslint.config.mjs +69 -0
  307. package/templates/apps/web/jest.config.cjs +15 -19
  308. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  309. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  310. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  311. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
  312. package/templates/apps/web/lib/auth-email.ts +318 -0
  313. package/templates/apps/web/lib/auth-env.ts +85 -0
  314. package/templates/apps/web/lib/auth-routes.ts +55 -0
  315. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  316. package/templates/apps/web/lib/nav-config.ts +27 -7
  317. package/templates/apps/web/lib/server-api.ts +1 -1
  318. package/templates/apps/web/lib/site-chrome.ts +29 -0
  319. package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
  320. package/templates/apps/web/messages/en.json +216 -2
  321. package/templates/apps/web/next.config.mjs +20 -0
  322. package/templates/apps/web/package.json +8 -3
  323. package/templates/apps/web/sentry.client.config.ts +131 -0
  324. package/templates/apps/web/sentry.edge.config.ts +72 -0
  325. package/templates/apps/web/sentry.server.config.ts +72 -0
  326. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  327. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  328. package/templates/apps/web/src/lib/routes.ts +13 -18
  329. package/templates/brand_assets/README.md +24 -0
  330. package/templates/content/en/AboutPage/about.mdx +160 -15
  331. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  332. package/templates/content/en/ContactPage/contact.mdx +90 -13
  333. package/templates/content/en/FAQPage/faq.mdx +62 -22
  334. package/templates/content/en/HomePage/home.mdx +244 -20
  335. package/templates/content/en/LandingPage/blog.mdx +83 -0
  336. package/templates/content/en/LandingPage/features.mdx +103 -0
  337. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  338. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  339. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  340. package/templates/database/CHANGELOG.md +149 -0
  341. package/templates/database/README.md +10 -3
  342. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  343. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  344. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  345. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  346. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  347. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  348. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  349. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  350. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  351. package/templates/database/__tests__/seed.test.ts +1 -1
  352. package/templates/database/__tests__/tenancy.test.ts +141 -0
  353. package/templates/database/config/schema.prisma +79 -2
  354. package/templates/database/content/schema.prisma +87 -11
  355. package/templates/database/eslint.config.mjs +69 -0
  356. package/templates/database/inbox/schema.prisma +452 -9
  357. package/templates/database/jest.config.cjs +5 -3
  358. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  359. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  360. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  361. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  362. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  363. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  364. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  365. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  366. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  367. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  368. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  369. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  370. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  371. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  372. package/templates/database/package.json +7 -3
  373. package/templates/database/seed.ts +1 -1
  374. package/templates/database/tenancy.ts +86 -1
  375. package/templates/database/tsconfig.json +1 -0
  376. package/templates/package.json +1 -14
  377. package/versions.json +52 -0
  378. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
  379. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  380. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  381. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  382. package/templates/apps/api/src/routes/content/create.ts +0 -115
  383. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  384. package/templates/apps/api/src/routes/content/update.ts +0 -112
  385. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  386. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  387. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  388. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  389. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  390. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  391. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  392. /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
  393. /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
  394. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  395. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
  396. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -20,6 +20,26 @@ import type { FastifyError, FastifyInstance } from "fastify";
20
20
 
21
21
  const GENERIC_INTERNAL_BODY = { error: "internal_error" } as const;
22
22
 
23
+ /**
24
+ * The request target with its QUERY STRING removed (#5970 security review,
25
+ * finding 1).
26
+ *
27
+ * A route may carry its credential in the query — `GET
28
+ * /v1/requests/:id/confirm?token=…` does, because a link in an email has
29
+ * nowhere else to put one — and this handler logs `path` on EVERY error on
30
+ * EVERY route. Logging the raw target would write that credential into the
31
+ * API's own log the moment anything went wrong, which is precisely when logs
32
+ * get read, copied into tickets, and shipped to third parties.
33
+ *
34
+ * The path itself is kept: it is what makes the line useful, and a route
35
+ * parameter like `:id` is an identifier rather than a secret. The matching
36
+ * strip lives on the Fastify logger's own `req` serializer in `server.ts`.
37
+ */
38
+ function pathWithoutQuery(url: string): string {
39
+ const queryAt = url.indexOf("?");
40
+ return queryAt === -1 ? url : url.slice(0, queryAt);
41
+ }
42
+
23
43
  export function registerErrorSerializer(server: FastifyInstance): void {
24
44
  server.setErrorHandler(async (err: FastifyError, request, reply) => {
25
45
  // Validation errors (Fastify's schema validator) — keep structured detail.
@@ -29,7 +49,7 @@ export function registerErrorSerializer(server: FastifyInstance): void {
29
49
  request.log.warn(
30
50
  {
31
51
  method: request.method,
32
- path: request.url,
52
+ path: pathWithoutQuery(request.url),
33
53
  validation: err.validation,
34
54
  },
35
55
  "request validation failed",
@@ -48,7 +68,7 @@ export function registerErrorSerializer(server: FastifyInstance): void {
48
68
  request.log.info(
49
69
  {
50
70
  method: request.method,
51
- path: request.url,
71
+ path: pathWithoutQuery(request.url),
52
72
  statusCode: status,
53
73
  errName: err.name,
54
74
  },
@@ -63,7 +83,7 @@ export function registerErrorSerializer(server: FastifyInstance): void {
63
83
  request.log.error(
64
84
  {
65
85
  method: request.method,
66
- path: request.url,
86
+ path: pathWithoutQuery(request.url),
67
87
  errName: err.name,
68
88
  errMessage: err.message,
69
89
  errStack: err.stack,
@@ -20,7 +20,11 @@
20
20
  * finding waiting to happen.
21
21
  *
22
22
  * Test-mode escape hatch — `allowPrivate: true` suppresses the SSRF gate.
23
- * Use only for explicit, internal-by-design traffic (typically tests).
23
+ * Use only for explicit, internal-by-design traffic (typically tests). One
24
+ * production case is sanctioned (#5851): a destination read from an
25
+ * operator-configured deployment environment variable that is never
26
+ * user-controlled, such as `CONTENT_REVALIDATE_URL`, whose legitimate value is
27
+ * loopback in local development and single-host deployments.
24
28
  */
25
29
 
26
30
  const DEFAULT_TIMEOUT_MS = 10_000;
@@ -29,7 +33,8 @@ export type OutboundFetchOptions = RequestInit & {
29
33
  /** Override the default 10s timeout. */
30
34
  timeoutMs?: number;
31
35
  /**
32
- * Suppress the SSRF allow-list — test-mode escape hatch only.
36
+ * Suppress the SSRF allow-list — tests, plus the sanctioned production case
37
+ * in the module doc (an operator-configured deployment-env destination).
33
38
  * NEVER pass true for user-controlled URLs.
34
39
  */
35
40
  allowPrivate?: boolean;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Commerce license-claim guard for the commerce PRIMITIVES that live in the
3
+ * base tree (DECISION-LOG D-64, epic #5770, #5796).
4
+ *
5
+ * The checkout launcher, the Stripe webhook receiver, customer mapping and
6
+ * `lib/stripe.ts` ship unconditionally now — they are no longer registered
7
+ * behind `isModuleEnabled("commerce")`. What decides whether they SERVE is the
8
+ * JWT `features[]` claim `commerce`, read at CALL time through `checkLicense()`
9
+ * from `@working-theory/license`.
10
+ *
11
+ * This is the same shape as `./require-paid-tier.ts` — that file is the
12
+ * PATTERN, not the code (D-64): this gate reads
13
+ * `result.features.includes("commerce")` and never looks at `result.tier` as
14
+ * the claim test. A `commerce` claim can ride any paid level, so a tier test
15
+ * would be the wrong question.
16
+ *
17
+ * Response without the claim: the SAME module-disabled response the
18
+ * module-conditional path produced — a 404. `server.ts`'s root-endpoint
19
+ * comment (§ `deriveRootEndpoints`) documents that a build with commerce off
20
+ * simply does not advertise these routes and answers 404 on them, and
21
+ * `src/__tests__/module-exclusion.test.ts` measures exactly that status. The
22
+ * body is `notFoundError()` from `./errors.ts` — this repo's one reusable 404
23
+ * body — rather than a newly invented `module_disabled` code, which exists
24
+ * nowhere in the tree. The message deliberately does not mention commerce: a
25
+ * site without the claim is indistinguishable from a site built without the
26
+ * module, which is the point.
27
+ *
28
+ * ── TRI-STATE, not a boolean ────────────────────────────────────────────────
29
+ *
30
+ * Security review 2026-09-16, High; widened by the operator in round 2. A
31
+ * single `false` for "no claim" and "could not tell" is a data-loss bug on the
32
+ * webhook: a signed `checkout.session.completed` on a live commerce site whose
33
+ * licence read merely FAILED would be acked 200 and dropped, Stripe would
34
+ * record success and never retry, and the charge would never be mapped.
35
+ *
36
+ * So `resolveCommerceClaim()` answers `claimed | unclaimed | undetermined`:
37
+ *
38
+ * claimed — read successfully; `features[]` carries `commerce`.
39
+ * unclaimed — a positively-established "no": either a licence that reads
40
+ * successfully and simply lacks the claim, or a KEYLESS
41
+ * deployment that configures no licence at all.
42
+ * undetermined — the read established no "no". These states reach it:
43
+ * 1. `checkLicense()` threw.
44
+ * 2. `revoked === true` — a revoked licence is still a
45
+ * PAYING site inside its JWT window (ADR 0014).
46
+ * 3. a licence IS configured yet resolves free — the JWT
47
+ * expired, or no longer verifies.
48
+ * 4. (3) also absorbs a configured-but-unparseable key.
49
+ * 5. the licence STORAGE FILE could not be read — EACCES,
50
+ * EIO, EMFILE, a directory at its path (#5820).
51
+ *
52
+ * Why (3) needs the env read. `checkLicense()` returns the SAME `FREE_RESULT`
53
+ * for expired, invalid and keyless alike (`check-license.ts` — `expiresAt` is
54
+ * `null` and `revoked` is `false` in all three), so nothing on `LicenseResult`
55
+ * separates "this deployment's licence stopped verifying" from "this
56
+ * deployment never had one". The one signal that does is whether a licence is
57
+ * CONFIGURED at all, which is an env read this app already makes elsewhere
58
+ * (`services/enrichment-shim.ts`). No JWT is parsed here and no verification
59
+ * is re-implemented — presence is the entire test.
60
+ *
61
+ * ── The storage file, read for what it actually says (#5820) ────────────────
62
+ *
63
+ * A licence sourced from the refresh STORAGE FILE is covered on both axes.
64
+ * `readStoredJwtState()` — added to `@working-theory/license` for this gate —
65
+ * answers `absent | unreadable | present` where `readStoredJwt()` answered one
66
+ * `null` for the first two. Only `absent` (no file, or a file holding nothing
67
+ * but whitespace) is evidence that a deployment is unlicensed; a file that
68
+ * merely could not be READ is evidence of nothing, so it holds. An EXPIRED
69
+ * storage licence (`present`, resolving free) holds too.
70
+ *
71
+ * Before that distinction existed, a transient EACCES on a storage-licensed
72
+ * site read as keyless: a signed `checkout.session.completed` arriving in that
73
+ * window was acked 200, Stripe never redelivered it, and the charge was never
74
+ * mapped. `readStoredJwt()`'s own contract is untouched — `checkLicense()` and
75
+ * every other caller still see `null` for both states.
76
+ *
77
+ * ── Why the two routes answer differently ───────────────────────────────────
78
+ *
79
+ * The checkout launcher answers the module-disabled 404 on BOTH non-serving
80
+ * states: a checkout that cannot be started loses nothing, so there is no call
81
+ * to leak a "configured but unreadable" shape on it. The webhook receiver
82
+ * holds an `undetermined` event with a retryable 503 + `Retry-After`, because
83
+ * by the time it arrives the buyer's money has already moved.
84
+ *
85
+ * Usage as a Fastify preHandler (the checkout launcher):
86
+ *
87
+ * fastify.post("/path", { preHandler: requireCommerceClaim }, handler);
88
+ *
89
+ * The Stripe webhook receiver does NOT use the preHandler: D3 puts signature
90
+ * verification BEFORE the claim check, so it consults `resolveCommerceClaim()`
91
+ * from inside its handler. See `routes/webhooks/README.md` § Commerce claim.
92
+ */
93
+
94
+ import {
95
+ checkLicense,
96
+ readStoredJwtState,
97
+ type ProFeature,
98
+ } from "@working-theory/license";
99
+ import type { FastifyReply, FastifyRequest } from "fastify";
100
+
101
+ import { notFoundError } from "./errors.js";
102
+
103
+ /** The claim the commerce primitives serve behind (`ProFeature`, verify-jwt.ts). */
104
+ export const COMMERCE_FEATURE: ProFeature = "commerce";
105
+
106
+ const MODULE_DISABLED_MESSAGE = "Not found";
107
+
108
+ /**
109
+ * What the licence read actually established. See the header for the four
110
+ * states that reach `undetermined`.
111
+ */
112
+ export type CommerceClaimState = "claimed" | "unclaimed" | "undetermined";
113
+
114
+ /** The module-disabled response — a 404, the canonical `not_found` body. */
115
+ export function commerceDisabledResponse() {
116
+ return notFoundError(MODULE_DISABLED_MESSAGE);
117
+ }
118
+
119
+ /**
120
+ * The retryable answer for `undetermined` on the webhook. A local error
121
+ * vocabulary (`{ error: "..." }`), matching what `routes/webhooks/stripe.ts`
122
+ * already emits for its own failure modes, rather than `errors.ts`'s closed
123
+ * `ErrorCode` set, which has no service-unavailable member and whose MCP twin
124
+ * would have to grow one too.
125
+ *
126
+ * `Retry-After` is in SECONDS and is read by Stripe: five minutes is long
127
+ * enough for a transient licence-read failure or a refresh round-trip to
128
+ * clear, and short enough that a queued `checkout.session.completed` is mapped
129
+ * well inside Stripe's three-day redelivery window.
130
+ */
131
+ export const LICENSE_UNDETERMINED_STATUS = 503;
132
+ export const LICENSE_UNDETERMINED_BODY = { error: "license-undetermined" };
133
+ export const LICENSE_UNDETERMINED_RETRY_AFTER_SECONDS = 300;
134
+
135
+ /**
136
+ * What this deployment's licence CONFIGURATION says — three answers, not two.
137
+ *
138
+ * configured — an env key is set, or the storage file holds a token.
139
+ * absent — no env key, and the storage file is genuinely not there (or
140
+ * holds nothing but whitespace). The only state that is
141
+ * evidence of an unlicensed site.
142
+ * unreadable — no env key, and the storage file could not be read at all.
143
+ *
144
+ * Presence only — the token is never parsed, verified or logged. This is the
145
+ * one signal that separates an EXPIRED/unverifiable licence from a keyless
146
+ * site, because `checkLicense()` collapses both into the same free result.
147
+ *
148
+ * It asks the question the SAME WAY `check-license.ts#resolveToken` does —
149
+ * `AILK_LICENSE_KEY`, else the refresh storage file — because that is where
150
+ * `checkLicense()` itself just looked. An env-only test would miss a
151
+ * deployment licensed purely from storage, so an EXPIRED storage licence would
152
+ * read as keyless and its already-charged events would be dropped.
153
+ *
154
+ * `unreadable` is kept separate from `absent` rather than folded into
155
+ * `configured` so the caller can LOG why it is holding (#5820): "this site has
156
+ * no licence" and "this site's licence could not be read" are different
157
+ * operator problems, and only the first is a fact.
158
+ */
159
+ type LicenseConfigurationState = "configured" | "absent" | "unreadable";
160
+
161
+ async function resolveLicenseConfiguration(): Promise<LicenseConfigurationState> {
162
+ const key = process.env["AILK_LICENSE_KEY"];
163
+ if (typeof key === "string" && key.length > 0) return "configured";
164
+
165
+ const stored = await readStoredJwtState();
166
+ if (stored.state === "present") return "configured";
167
+ if (stored.state === "absent") return "absent";
168
+
169
+ // `console.warn` rather than a request logger: `resolveCommerceClaim()` is
170
+ // called from the webhook handler with no logger threaded through, and the
171
+ // routes are out of scope for this change. The cause's `code` is the whole
172
+ // point of the line — EACCES and EISDIR are different operator fixes. The
173
+ // token is never in scope here, so nothing secret can reach the log.
174
+ const code =
175
+ (stored.cause as NodeJS.ErrnoException | undefined)?.code ?? "UNKNOWN";
176
+ console.warn(
177
+ "commerce-claim: licence storage file unreadable — holding rather than dropping",
178
+ { outcome: "license_storage_unreadable", code },
179
+ );
180
+ return "unreadable";
181
+ }
182
+
183
+ /**
184
+ * Resolve what this deployment's licence says about `commerce`.
185
+ *
186
+ * Never throws: a failed read is a state, not an exception, because both
187
+ * callers must be able to tell "no" from "could not tell".
188
+ */
189
+ export async function resolveCommerceClaim(): Promise<CommerceClaimState> {
190
+ let result;
191
+ try {
192
+ result = await checkLicense();
193
+ } catch {
194
+ // The read FAILED. Not a "no" — callers hold rather than drop.
195
+ return "undetermined";
196
+ }
197
+
198
+ // A revoked licence keeps verifying locally until JWT `exp` (ADR 0014), so
199
+ // this is a PAYING commerce site mid-window whose events are still real.
200
+ // Closing the gate is right; DROPPING them is the security review's High
201
+ // under a different label.
202
+ if (result.revoked === true) return "undetermined";
203
+
204
+ if (result.features.includes(COMMERCE_FEATURE)) return "claimed";
205
+
206
+ if (result.tier === "free") {
207
+ const configuration = await resolveLicenseConfiguration();
208
+ // A licence is configured yet resolved free: expired, or no longer
209
+ // verifying. `checkLicense()` cannot say which, and neither is a "no".
210
+ if (configuration === "configured") return "undetermined";
211
+ // The storage file could not be read (#5820). An unreadable file is not
212
+ // evidence of a keyless site, and dropping a signed Stripe event needs
213
+ // evidence — so this holds and Stripe retries.
214
+ if (configuration === "unreadable") return "undetermined";
215
+ // `absent` — no env key and no storage file. A positively-established
216
+ // "no", and the only state on this path that may drop an event.
217
+ }
218
+
219
+ return "unclaimed";
220
+ }
221
+
222
+ /**
223
+ * Fastify preHandler for the checkout launcher: serve only with the claim.
224
+ *
225
+ * Both non-serving states answer the module-disabled 404 — see the header:
226
+ * an unstartable checkout loses nothing, so the route stays indistinguishable
227
+ * from a build without the module rather than advertising that a licence
228
+ * exists but could not be read. The `undetermined` case is still logged, so
229
+ * an operator can tell a lapsed licence from a deliberate free build.
230
+ *
231
+ * Returns void (proceed) with the claim and short-circuits with `reply.send`
232
+ * otherwise, matching Fastify's preHandler contract.
233
+ */
234
+ export async function requireCommerceClaim(
235
+ request: FastifyRequest,
236
+ reply: FastifyReply,
237
+ ): Promise<void> {
238
+ const state = await resolveCommerceClaim();
239
+ if (state === "claimed") return;
240
+
241
+ if (state === "undetermined") {
242
+ request.log.warn(
243
+ { outcome: "license_undetermined" },
244
+ "commerce licence could not be read — checkout answering the module-disabled response",
245
+ );
246
+ }
247
+
248
+ const err = commerceDisabledResponse();
249
+ return reply.status(err.status).send(err.body);
250
+ }
@@ -1,16 +1,215 @@
1
1
  /**
2
2
  * @file site.ts
3
- * @description The current-site resolution helper (#3958 §3.2).
4
- *
5
- * Extracted verbatim from `routes/flows/index.ts`'s private
6
- * `currentSiteId()` so the flow-preset seed (`services/flow-engine.ts` →
7
- * `bin/seed-presets.ts`) resolves the SAME site id the `/v1/flows/*` routes
8
- * resolve — a seed hard-coding `"default"` would write presets the API
9
- * cannot find whenever `AILK_SITE_ID` is set (a silent, environment-dependent
10
- * 404). Behavior is byte-identical to the pre-extraction helper: same env
11
- * var, same fallback, same call sites.
3
+ * @description The current-site resolution helper (#3958 §3.2), extended by
4
+ * #6155 (spec D2/D5/D6, ownership brief
5
+ * `docs/internal/decisions/workspace-owns-data-surfaces-render.md`) with the
6
+ * ALIAS -> WORKSPACE resolution the brief requires: `currentSiteId()`'s
7
+ * env-var alias is resolved to a real `workspaces.id` ONCE at startup,
8
+ * memoized, and ASSERTED — so a misconfigured deployment refuses to boot
9
+ * instead of serving empty lists (AC-5).
10
+ *
11
+ * `currentSiteId()` is extracted verbatim from `routes/flows/index.ts`'s
12
+ * private `currentSiteId()` so the flow-preset seed
13
+ * (`services/flow-engine.ts` -> `bin/seed-presets.ts`) resolves the SAME
14
+ * site id the `/v1/flows/*` routes resolve — a seed hard-coding `"default"`
15
+ * would write presets the API cannot find whenever `AILK_SITE_ID` is set (a
16
+ * silent, environment-dependent 404). Behavior is byte-identical to the
17
+ * pre-extraction helper: same env var, same fallback, same call sites.
18
+ * `currentSiteId()` stays SYNCHRONOUS and byte-identical (#6155 D5) — the
19
+ * new resolution is async (it reads the database), and making
20
+ * `currentSiteId()` async would edit every one of its dozen non-test call
21
+ * sites across routes/services/bins, including the two route files NFR1
22
+ * requires to stay byte-unchanged.
12
23
  */
24
+ import { resolvedAdapterKind } from "@working-theory/content-adapters";
25
+ import {
26
+ DEFAULT_WORKSPACE_ID,
27
+ DEFAULT_WORKSPACE_SLUG,
28
+ resolveSentinelWorkspaceRow,
29
+ } from "@working-theory/database/tenancy";
30
+ import { logger } from "@working-theory/observability";
31
+
32
+ import { prisma } from "./prisma.js";
13
33
 
14
34
  export function currentSiteId(): string {
15
35
  return process.env.AILK_SITE_ID ?? "default";
16
36
  }
37
+
38
+ /**
39
+ * The resolved workspace id, memoized after the first SUCCESSFUL resolution
40
+ * ("resolves the alias once at startup", ownership brief § Layer 2 / #6155
41
+ * D5). `null` is never cached — a failed resolution has already thrown from
42
+ * `build()` (D6), so there is no negative-cache case to hold, and a later
43
+ * retry (a test, a re-seeded database) is free to resolve on its next call.
44
+ */
45
+ let cachedWorkspaceId: string | null = null;
46
+
47
+ /**
48
+ * Resolves `currentSiteId()`'s alias to a real `workspaces.id`, per the
49
+ * two-and-only-two rule (#6155 spec D2):
50
+ *
51
+ * 1. The literal alias `"default"` resolves to the seeded sentinel
52
+ * workspace — `DEFAULT_WORKSPACE_ID` — IFF that row exists and hangs
53
+ * off the sentinel Organization
54
+ * (`@working-theory/database/tenancy`'s `resolveSentinelWorkspaceRow`,
55
+ * which carries NO `workspace.count()` gate — unlike
56
+ * `resolveDefaultWorkspaceRow`, whose single-tenancy meaning this unit
57
+ * must NOT widen; see D2's "why a NEW resolver"). This is what lets a
58
+ * correctly-configured MULTI-workspace deployment that has not pinned
59
+ * `AILK_SITE_ID` still resolve — `resolveDefaultWorkspaceRow`'s count
60
+ * gate would refuse it.
61
+ * 2. Any OTHER alias resolves ONLY by exact `workspaces.id` equality.
62
+ * 3. Anything that resolves neither way is `null` — never a fallback,
63
+ * never a guess (`assertSiteAliasResolves` below is what turns a `null`
64
+ * into a startup refusal).
65
+ *
66
+ * Memoized (D5): the SECOND call issues zero Prisma queries once the first
67
+ * has resolved successfully. `__resetSiteWorkspaceCacheForTests()` clears
68
+ * the memo for test isolation.
69
+ */
70
+ export async function resolveSiteWorkspaceId(): Promise<string | null> {
71
+ if (cachedWorkspaceId !== null) {
72
+ return cachedWorkspaceId;
73
+ }
74
+
75
+ const resolved = await resolveWorkspaceIdForAlias(currentSiteId());
76
+ if (resolved !== null) {
77
+ cachedWorkspaceId = resolved;
78
+ }
79
+ return resolved;
80
+ }
81
+
82
+ /**
83
+ * The two-and-only-two resolution rule (#6155 spec D2), extracted so a
84
+ * write site holding an EXPLICIT `siteId` parameter (not necessarily
85
+ * `currentSiteId()` — e.g. a `ProjectListing` resolved through its own
86
+ * `SiteListingConfig.siteId`) can resolve it the same way
87
+ * `resolveSiteWorkspaceId()` resolves the env alias, without going through
88
+ * that function's `currentSiteId()`-only memo (#6254 spec D1a): the write
89
+ * paths that still stamp a raw, unresolved alias resolve it first, through
90
+ * this resolver, before it ever reaches a `workspace_id NOT NULL` /
91
+ * `CHECK (workspace_id = site_id)` column.
92
+ *
93
+ * 1. The literal alias `"default"` resolves to the seeded sentinel
94
+ * workspace IFF that row exists (`resolveSentinelWorkspaceRow`).
95
+ * 2. Any OTHER alias resolves ONLY by exact `workspaces.id` equality.
96
+ * 3. Anything that resolves neither way is `null` — never a fallback,
97
+ * never a guess.
98
+ *
99
+ * Not memoized — callers passing a per-request `siteId` would otherwise
100
+ * share a cache keyed on nothing. `resolveSiteWorkspaceId()` above keeps
101
+ * its own single-value memo for the hot, env-alias-only path.
102
+ */
103
+ export async function resolveWorkspaceIdForAlias(
104
+ alias: string,
105
+ ): Promise<string | null> {
106
+ if (alias === DEFAULT_WORKSPACE_SLUG) {
107
+ const row = await resolveSentinelWorkspaceRow(prisma);
108
+ return row?.id ?? null;
109
+ }
110
+ const row = await prisma.workspace.findUnique({ where: { id: alias } });
111
+ return row ? alias : null;
112
+ }
113
+
114
+ /**
115
+ * Asserts `currentSiteId()`'s alias resolves to a real workspace — the
116
+ * misconfigured-deployment refusal AC-5 requires (D6). Awaited from
117
+ * `build()` (`apps/api/src/server.ts`), beside the two startup assertions
118
+ * already there; `apps/api/src/index.ts` already catches, logs and
119
+ * `process.exit(1)`s on a `build()` throw, so this is a loud refusal to
120
+ * start with no new exit-path code.
121
+ *
122
+ * THE CARVE-OUT (D6): when the alias genuinely CANNOT be checked — or NEED
123
+ * not be — this logs a warning and returns rather than throwing: a
124
+ * "database-less dev boot," never a misconfiguration. Three arms feed it, and
125
+ * ALL THREE are presence checks made BEFORE any query is attempted — never a
126
+ * catch around a query that already ran, which would just as readily swallow a
127
+ * genuine connection failure as it would an absent delegate (code-review
128
+ * finding, #6155):
129
+ *
130
+ * 0. **The content adapter is not `database`** (#6228) — the one arm that is
131
+ * "need not," not "cannot." The alias→workspace resolution exists so a
132
+ * database-backed content read has a workspace to read within; under
133
+ * `mdx` or `projection` NO read takes a `workspaceId` at all, so the
134
+ * assertion guards nothing and its only effect is to require a REACHABLE
135
+ * database of a deployment that never queries one. That is the #6228
136
+ * defect: `create-ailk`'s scaffold check sets a placeholder
137
+ * `DATABASE_URL` (presence is all `validateConfig()` wants, and Prisma
138
+ * connects lazily), so arm 1 does not fire, and the generated MDX site
139
+ * crashed at boot on `Can't reach database server`. Read through
140
+ * `resolvedAdapterKind()` — the ONE switch over `CONTENT_ADAPTER`
141
+ * (`packages/content-adapters/src/resolve.ts`) — rather than a second,
142
+ * drifting read of the env var here.
143
+ *
144
+ * 1. **No `DATABASE_URL` configured** — the literal `requireDb()` guard
145
+ * `apps/api/src/services/flow-engine.ts` uses for the flow services:
146
+ * presence, not reachability. (`apps/api/src/config/index.ts`'s own
147
+ * eager `validateConfig()` already refuses a real `apps/api` boot with
148
+ * `DATABASE_URL` unset before `build()` is ever reached, so this arm is
149
+ * for any OTHER caller of `build()`.)
150
+ * 2. **`prisma.workspace.findUnique` is not even a function** — the case
151
+ * that surfaced this arm: `apps/api`'s shared/global prisma mocks
152
+ * (`lib/__mocks__/prisma-client.js`, the module-map-level double every
153
+ * test gets; the many hand-rolled hoisted factories in route/service
154
+ * suites that never touch tenancy) predate this unit and declare no
155
+ * `workspace` delegate at all — the FIRST real Prisma read `build()`
156
+ * itself has ever issued. Widening dozens of unrelated test files'
157
+ * prisma doubles to add a resolving `workspace.findUnique` has a far
158
+ * larger and harder-to-audit blast radius than making THIS one
159
+ * assertion resilient to "the check itself is unavailable." A resolution
160
+ * that could not even be ATTEMPTED is not a misconfiguration to fail
161
+ * loudly about — but a resolution that WAS attempted and threw (a real
162
+ * connection error, a permissions error, a bug in the resolver) is
163
+ * exactly the class D6/AC-5 exist to surface, so it is deliberately NOT
164
+ * caught here: it propagates out of `resolveSiteWorkspaceId()` and
165
+ * crashes the boot, the same as any other unhandled `build()` throw.
166
+ *
167
+ * A reachable database with no matching workspace is the failure case, and
168
+ * it throws. A reachable database that ERRORS mid-query throws too — that is
169
+ * NOT the carve-out, it is the misconfiguration AC-5 asks to fail loudly
170
+ * about.
171
+ */
172
+ export async function assertSiteAliasResolves(): Promise<void> {
173
+ const adapterKind = resolvedAdapterKind();
174
+ if (adapterKind !== "database") {
175
+ logger.warn(
176
+ { alias: currentSiteId(), contentAdapter: adapterKind },
177
+ "[site] content adapter is not `database` — skipping the site-alias workspace assertion (no read takes a workspaceId on this boot)",
178
+ );
179
+ return;
180
+ }
181
+
182
+ if (!process.env.DATABASE_URL) {
183
+ logger.warn(
184
+ { alias: currentSiteId() },
185
+ "[site] DATABASE_URL not set — skipping the site-alias workspace assertion (database-less boot)",
186
+ );
187
+ return;
188
+ }
189
+
190
+ if (typeof prisma.workspace?.findUnique !== "function") {
191
+ logger.warn(
192
+ { alias: currentSiteId() },
193
+ "[site] prisma.workspace is unavailable — skipping the site-alias workspace assertion (the resolution could not be attempted)",
194
+ );
195
+ return;
196
+ }
197
+
198
+ const resolved = await resolveSiteWorkspaceId();
199
+ if (resolved !== null) {
200
+ return;
201
+ }
202
+
203
+ const alias = currentSiteId();
204
+ throw new Error(
205
+ `AILK_SITE_ID="${alias}" names no workspace. The site alias must be an ` +
206
+ 'existing workspaces.id, or the literal "default" (the seeded ' +
207
+ `sentinel workspace ${DEFAULT_WORKSPACE_ID}). Run \`pnpm db:seed\`, or ` +
208
+ "set AILK_SITE_ID to a workspace this database holds.",
209
+ );
210
+ }
211
+
212
+ /** Test-only: clears the memoized resolution (#6155 D5). */
213
+ export function __resetSiteWorkspaceCacheForTests(): void {
214
+ cachedWorkspaceId = null;
215
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * apps/api Stripe shim.
3
+ *
4
+ * Re-exports @working-theory/stripe-client's public surface for use by routes and
5
+ * services under apps/api. This is the SOLE Stripe-touching file in apps/api.
6
+ *
7
+ * Future routes import from here (`from '../lib/stripe'`), not directly from
8
+ * '@working-theory/stripe-client' — single chokepoint for AILK-internal ergonomics
9
+ * (logger context, request-id propagation) without touching every consumer.
10
+ */
11
+ import { getStripeClient } from "@working-theory/stripe-client";
12
+
13
+ export {
14
+ assertSandboxKey,
15
+ createAdHocPrice,
16
+ createBillingPortalSession,
17
+ createCheckoutSession,
18
+ createPaymentLink,
19
+ createQuotedCheckoutSession,
20
+ getStripeClient,
21
+ // Checkout session read (#6084 D9) — re-exported through this sole
22
+ // Stripe-touching chokepoint, same as every other Stripe call here.
23
+ retrieveCheckoutSession,
24
+ STRIPE_API_VERSION,
25
+ StripeConfigError,
26
+ StripeWebhookSignatureError,
27
+ toSubscriptionLifecycleStatus,
28
+ UnknownProductCodeError,
29
+ UnknownSessionError,
30
+ verifyWebhookSignature,
31
+ WEBHOOK_TOLERANCE_SECONDS,
32
+ } from "@working-theory/stripe-client";
33
+
34
+ export type {
35
+ CheckoutSessionLineItem,
36
+ CreateAdHocPriceInput,
37
+ CreateAdHocPriceOutput,
38
+ CreateBillingPortalSessionInput,
39
+ CreateBillingPortalSessionOutput,
40
+ CreatePaymentLinkInput,
41
+ CreatePaymentLinkOutput,
42
+ CreateCheckoutSessionInput,
43
+ CreateCheckoutSessionOutput,
44
+ CreateCheckoutSessionParams,
45
+ CreateQuotedCheckoutSessionInput,
46
+ CreateQuotedCheckoutSessionOutput,
47
+ RetrieveCheckoutSessionInput,
48
+ RetrieveCheckoutSessionOutput,
49
+ Stripe,
50
+ StripeCheckoutSession,
51
+ StripeWebhookEvent,
52
+ } from "@working-theory/stripe-client";
53
+
54
+ /**
55
+ * Expire a Checkout Session (#4912 FR7/D7).
56
+ *
57
+ * The compensation the recommender's attach-miss branch owes: the Stripe
58
+ * session already exists, its id never reached the `FlowCheckout` row, and
59
+ * nothing will ever join the two — so the session is closed rather than left
60
+ * live and payable (#4863). Built on the already-re-exported
61
+ * `getStripeClient`, whose construction asserts the `sk_test_` allow-list, so
62
+ * this call inherits the test-mode gate like every other Stripe call here.
63
+ */
64
+ export async function expireCheckoutSession(stripeSessionId: string): Promise<void> {
65
+ await getStripeClient().checkout.sessions.expire(stripeSessionId);
66
+ }