create-ailk 0.21.0 → 0.23.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 (342) hide show
  1. package/component-catalog.json +186 -8
  2. package/component-catalog.md +9 -7
  3. package/dist/add-module.js +20 -14
  4. package/dist/add-route.d.ts +55 -1
  5. package/dist/add-route.js +294 -12
  6. package/dist/cli.js +27 -10
  7. package/dist/derive-architecture.d.ts +12 -3
  8. package/dist/derive-architecture.js +20 -8
  9. package/dist/design-package.js +7 -2
  10. package/dist/lib/design-checks.js +4 -2
  11. package/dist/lib/env-catalog.js +5 -5
  12. package/dist/lib/npm-auth.d.ts +9 -0
  13. package/dist/lib/npm-auth.js +48 -0
  14. package/dist/lib/scaffold-render-sweep.d.ts +3 -2
  15. package/dist/lib/scaffold-render-sweep.js +5 -8
  16. package/dist/module-architecture.js +99 -9
  17. package/dist/paid-sections.d.ts +21 -2
  18. package/dist/paid-sections.js +69 -7
  19. package/dist/programmatic.js +19 -5
  20. package/dist/seed-flows.d.ts +4 -1
  21. package/dist/seed-flows.js +50 -4
  22. package/dist/site-chrome.d.ts +8 -0
  23. package/dist/site-chrome.js +17 -1
  24. package/dist/spoke-checkout.d.ts +65 -41
  25. package/dist/spoke-checkout.js +208 -430
  26. package/dist/starter-grid.d.ts +0 -3
  27. package/dist/starter-grid.js +24 -14
  28. package/dist/surfaces.js +52 -26
  29. package/dist/sync-routes.d.ts +38 -1
  30. package/dist/sync-routes.js +187 -13
  31. package/dist/template-record.d.ts +4 -3
  32. package/dist/template-record.js +8 -4
  33. package/dist/topology.js +25 -1
  34. package/dist/update.js +15 -0
  35. package/package.json +3 -3
  36. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  37. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  38. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  39. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  41. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  43. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  46. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  48. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  49. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  50. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  51. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  52. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  53. package/templates/.env.example +9 -7
  54. package/templates/.github/workflows/ci.yml +13 -0
  55. package/templates/README.md +1 -0
  56. package/templates/_gitignore +4 -0
  57. package/templates/apps/api/.env.example +27 -0
  58. package/templates/apps/api/CLAUDE.md +87 -17
  59. package/templates/apps/api/package.json +1 -0
  60. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  61. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  62. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  63. package/templates/apps/api/src/config/index.ts +109 -0
  64. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  65. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  66. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  67. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  68. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  69. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  70. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  71. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  72. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  73. package/templates/apps/api/src/lib/stripe.ts +74 -4
  74. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  75. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  77. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  78. package/templates/apps/api/src/middleware/auth.ts +13 -6
  79. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  80. package/templates/apps/api/src/openapi/spec.ts +33 -3
  81. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  82. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  83. package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
  84. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  85. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
  86. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  87. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  88. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  89. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  90. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  91. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  92. package/templates/apps/api/src/routes/consent/account.ts +268 -0
  93. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  94. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  95. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  96. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  97. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  98. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  99. package/templates/apps/api/src/routes/content/index.ts +16 -2
  100. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  101. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  102. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  103. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  104. package/templates/apps/api/src/routes/leads/index.ts +164 -7
  105. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  106. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  111. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  112. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  113. package/templates/apps/api/src/routes/webhooks/stripe.ts +245 -36
  114. package/templates/apps/api/src/server.ts +163 -46
  115. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  116. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
  117. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  118. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1383 -133
  119. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  120. package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
  121. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
  122. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  123. package/templates/apps/api/src/services/consent.ts +95 -0
  124. package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
  125. package/templates/apps/api/src/services/entitlements.ts +157 -1
  126. package/templates/apps/api/src/services/lead-routing.ts +431 -47
  127. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  128. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
  129. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  130. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
  131. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  132. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
  133. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
  134. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  135. package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
  136. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
  137. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  138. package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
  139. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  140. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  141. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  142. package/templates/apps/api/src/services/person.ts +164 -17
  143. package/templates/apps/api/src/services/plan-offerings.ts +125 -0
  144. package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
  145. package/templates/apps/mcp/CLAUDE.md +22 -14
  146. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  147. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  148. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  149. package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
  150. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  151. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  152. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  153. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  154. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  155. package/templates/apps/mcp/src/server.ts +5 -0
  156. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  157. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  158. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  159. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  160. package/templates/apps/web/.env.example +24 -31
  161. package/templates/apps/web/CLAUDE.md +393 -9
  162. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  163. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  164. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  165. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  166. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  167. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  168. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  169. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  170. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  171. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  172. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  173. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  174. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  175. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  176. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  177. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  178. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  179. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  180. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  181. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  182. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  183. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  184. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  185. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  186. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  187. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  188. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  189. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  190. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  191. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  192. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  193. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  194. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  195. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  196. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
  197. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
  198. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
  199. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -0
  200. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  201. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  202. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +141 -0
  203. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
  204. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
  205. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  206. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  207. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  208. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  209. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  210. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  211. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  212. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
  213. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  214. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
  215. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
  216. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
  217. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
  218. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  219. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
  220. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  221. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  222. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
  223. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  224. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  225. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  226. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  227. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  228. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  229. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  230. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  231. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
  232. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  233. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  234. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  235. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  236. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  237. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  238. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  239. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  240. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  241. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  242. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  243. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  244. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  245. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  246. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  247. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  248. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  249. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  250. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  251. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  252. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  253. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  254. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  255. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  256. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  257. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  258. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  259. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  260. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  261. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  262. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  263. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  264. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  265. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  266. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  267. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  268. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  269. package/templates/apps/web/auth.ts +28 -2
  270. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  271. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  272. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  273. package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
  274. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  275. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
  276. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  277. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  278. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +101 -25
  279. package/templates/apps/web/lib/account-invite.ts +37 -0
  280. package/templates/apps/web/lib/auth-routes.ts +36 -2
  281. package/templates/apps/web/lib/checkout-config.ts +16 -0
  282. package/templates/apps/web/lib/consent-policies.ts +22 -0
  283. package/templates/apps/web/lib/deck-page.ts +7 -0
  284. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  285. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  286. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  287. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  288. package/templates/apps/web/lib/magnet-routes.ts +42 -0
  289. package/templates/apps/web/lib/modules.generated.ts +9 -0
  290. package/templates/apps/web/lib/newsletter.actions.ts +202 -0
  291. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  292. package/templates/apps/web/lib/posting-title.ts +17 -0
  293. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  294. package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
  295. package/templates/apps/web/lib/site-chrome.ts +2 -0
  296. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  297. package/templates/apps/web/messages/en.json +71 -20
  298. package/templates/apps/web/middleware.ts +12 -2
  299. package/templates/apps/web/package.json +1 -0
  300. package/templates/apps/web/security-headers.config.ts +17 -0
  301. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  302. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  303. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  304. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  305. package/templates/content/en/BlogList/blog.mdx +4 -71
  306. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  307. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  308. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  309. package/templates/content/en/BookCall/start-project.mdx +1 -17
  310. package/templates/content/en/ContactPage/contact.mdx +6 -63
  311. package/templates/content/en/HomePage/home.mdx +3 -21
  312. package/templates/database/CHANGELOG.md +69 -0
  313. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  314. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  315. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  316. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  317. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  318. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  319. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  320. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  321. package/templates/database/__tests__/seed.test.ts +64 -1
  322. package/templates/database/auth/schema.prisma +4 -0
  323. package/templates/database/config/schema.prisma +91 -0
  324. package/templates/database/inbox/schema.prisma +17 -2
  325. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  326. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  327. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  328. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  329. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  330. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  331. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  332. package/templates/database/package.json +1 -1
  333. package/templates/database/seed.ts +62 -3
  334. package/versions.json +35 -35
  335. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  336. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
  337. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  338. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  339. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  340. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  341. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  342. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -0,0 +1,325 @@
1
+ // oss: false
2
+ /**
3
+ * POST /v1/billing/plan-checkout
4
+ *
5
+ * The Billing tab's Purchase plan (#9067 D3, D13; spec
6
+ * `docs/specs/9067-the-billing-tab-s-plan-spec.md`): starts a subscription
7
+ * Stripe Checkout for the caller's OWN Organization's plan, on the
8
+ * deployment's Stripe account, sandbox-only like the Organization portal.
9
+ * The Organization is ALWAYS `request.tenant.organizationId`. The session
10
+ * creates its own customer (no `buyer`, no `customerId`, so no
11
+ * `ailk_user_id`), and the webhook binds that customer to the Organization
12
+ * only because this route recorded the session's id (D13).
13
+ *
14
+ * Check order — each step refuses `{ ok: false, error: { code, message } }`
15
+ * or passes to the next, so a refusal answers before the work it guards:
16
+ *
17
+ * 1. tenant 401 unauthorized
18
+ * 2. `billing-manage` 403 forbidden — before any lookup or budget
19
+ * 3. write budget 429 rate_limited (10/h per Organization and per person)
20
+ * 4. commerce claim 409 purchase_unavailable · 503 license_undetermined
21
+ * 5. body 400 validation_failed
22
+ * 6. return URL 400 return_url_refused · 500 config-error
23
+ * 7. no customer; a plan tier 409 billing_customer_exists · 409 plan_not_purchasable
24
+ * 8. Stripe key 500 stripe-not-configured · 500 live-mode-forbidden
25
+ * 7b. one payable session 409 purchase_pending · 502 checkout-unavailable
26
+ * 9. create 500 plan_price_missing · 500 config-error · 502 checkout-unavailable
27
+ * 10. a Checkout URL 502 checkout-unavailable (the session expired)
28
+ * 9b. record (compare-and-set) 409 purchase_pending · 502 (the new session expired)
29
+ *
30
+ * PII/secret-min: no response body or log line carries the return URL, an
31
+ * Organization or Stripe id, or a Stripe error message — errors are logged
32
+ * by `name` only.
33
+ *
34
+ * MCP parity: owner-only purchase, and `billing-manage` is never
35
+ * agent-grantable, so no twin could succeed. Recorded in
36
+ * `tests/parity/README.md` § Routes with no tool, by decision.
37
+ */
38
+ import {
39
+ can,
40
+ planCheckoutRequestSchema,
41
+ planCheckoutResponseSchema,
42
+ planPurchaseLookupKey,
43
+ } from "@working-theory/validation";
44
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
45
+
46
+ import { config } from "../../config/index.js";
47
+ import {
48
+ LICENSE_UNDETERMINED_RETRY_AFTER_SECONDS,
49
+ resolveCommerceClaim,
50
+ } from "../../lib/require-commerce-claim.js";
51
+ import { siteStamp } from "../../lib/site.js";
52
+ import {
53
+ assertSandboxKey,
54
+ CheckoutModeMismatchError,
55
+ createCheckoutSession,
56
+ expireCheckoutSession,
57
+ PLATFORM_TERMS_OF_SERVICE,
58
+ retrieveCheckoutSession,
59
+ StripeConfigError,
60
+ UnknownProductCodeError,
61
+ UnknownSessionError,
62
+ } from "../../lib/stripe.js";
63
+ import { chargeWriteBudget } from "../../middleware/rate-limit.js";
64
+ import {
65
+ bindOrgPlanCheckout,
66
+ getOrgBillingIdentity,
67
+ readOrgPlanCheckoutSession,
68
+ resolveEntitlements,
69
+ setOrgPlanCheckoutSession,
70
+ } from "../../services/entitlements.js";
71
+
72
+ /** Route-level body bound (D3 step 5). */
73
+ const BODY_LIMIT = 4096;
74
+ const WRITE_BUDGET_SCOPE = "plancheckout";
75
+ const WRITE_BUDGET_WINDOW_MS = 60 * 60 * 1000;
76
+ const PLAN_CHECKOUTS_PER_HOUR = 10;
77
+ /** The quantity range the buyer picks on Checkout (sites). */
78
+ const SITES_MIN = 1;
79
+ const SITES_MAX = 50;
80
+ const NO_STORE = "private, no-store";
81
+ const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
82
+
83
+ function refuse(reply: FastifyReply, status: number, code: string, message: string) {
84
+ return reply.status(status).send({ ok: false, error: { code, message } });
85
+ }
86
+
87
+ function checkoutUnavailable(reply: FastifyReply) {
88
+ return refuse(reply, 502, "checkout-unavailable", "The purchase is temporarily unavailable");
89
+ }
90
+
91
+ /**
92
+ * D3 step 6 — a return URL the Admin may hand Stripe: canonical (its own
93
+ * parse's `href`, which refuses CR, LF, tab, backslash and every other
94
+ * re-spelling), no userinfo, query or fragment (the Admin sends none, and a
95
+ * `{CHECKOUT_SESSION_ID}` template would leak the session id), no `//` path,
96
+ * `https:` (or `http:` on a local host), on a trusted origin.
97
+ */
98
+ function isAcceptableReturnUrl(value: string, trusted: readonly string[]): boolean {
99
+ let url: URL;
100
+ try {
101
+ url = new URL(value);
102
+ } catch {
103
+ return false;
104
+ }
105
+ if (url.href !== value) return false;
106
+ if (url.username !== "" || url.password !== "" || url.hash !== "" || value.includes("#")) {
107
+ return false;
108
+ }
109
+ if (url.search !== "" || value.includes("?") || url.pathname.startsWith("//")) return false;
110
+ const secure =
111
+ url.protocol === "https:" || (url.protocol === "http:" && LOCAL_HOSTNAMES.has(url.hostname));
112
+ return secure && trusted.includes(url.origin);
113
+ }
114
+
115
+ /** Expire a session this route opened and will not hand out; a failure is logged by name. */
116
+ async function expireQuietly(request: FastifyRequest, sessionId: string): Promise<void> {
117
+ try {
118
+ await expireCheckoutSession(sessionId);
119
+ } catch (err) {
120
+ request.log.error(
121
+ { errName: err instanceof Error ? err.name : "unknown" },
122
+ "[billing] plan checkout expire failed",
123
+ );
124
+ }
125
+ }
126
+
127
+ export async function planCheckoutHandler(
128
+ request: FastifyRequest,
129
+ reply: FastifyReply,
130
+ ): Promise<void> {
131
+ // 1. Tenant — the preHandler denies an unresolvable tenant first.
132
+ const tenant = request.tenant;
133
+ if (!tenant) return refuse(reply, 401, "unauthorized", "Authentication required");
134
+
135
+ // 2. Permission — before any lookup, budget or body read.
136
+ if (!can(tenant.role, "billing-manage")) {
137
+ request.log.warn(
138
+ { method: request.method, path: request.url, status: 403 },
139
+ "[billing] plan checkout denied; insufficient role",
140
+ );
141
+ return refuse(reply, 403, "forbidden", "Forbidden");
142
+ }
143
+
144
+ // 3. Write budget — every request that got this far is counted.
145
+ const budget = await chargeWriteBudget({
146
+ scope: WRITE_BUDGET_SCOPE,
147
+ windowMs: WRITE_BUDGET_WINDOW_MS,
148
+ now: Date.now(),
149
+ charges: [
150
+ { key: `org,${tenant.organizationId}`, limit: PLAN_CHECKOUTS_PER_HOUR },
151
+ { key: `user,${tenant.userId}`, limit: PLAN_CHECKOUTS_PER_HOUR },
152
+ ],
153
+ warn: (obj, msg) => request.log.warn(obj, msg),
154
+ });
155
+ if (!budget.ok) {
156
+ reply.header("Retry-After", String(budget.retryAfterSeconds));
157
+ return refuse(reply, 429, "rate_limited", "Too many purchase attempts; try again later");
158
+ }
159
+
160
+ // 4. Commerce claim — the webhook that binds the customer serves only with it.
161
+ const claim = await resolveCommerceClaim();
162
+ if (claim === "undetermined") {
163
+ reply.header("Retry-After", String(LICENSE_UNDETERMINED_RETRY_AFTER_SECONDS));
164
+ return refuse(reply, 503, "license_undetermined", "The licence could not be read; try again later");
165
+ }
166
+ if (claim !== "claimed") {
167
+ return refuse(reply, 409, "purchase_unavailable", "Purchasing a plan is not available on this site");
168
+ }
169
+
170
+ // 5. Body.
171
+ const body = planCheckoutRequestSchema.safeParse(request.body);
172
+ if (!body.success) return refuse(reply, 400, "validation_failed", "Invalid request body");
173
+ const { returnUrl } = body.data;
174
+
175
+ // 6. Return URL — never logged.
176
+ if (config.TRUSTED_ORIGINS.length === 0) {
177
+ request.log.error({ outcome: "no_trusted_origins" }, "[billing] plan checkout misconfigured");
178
+ return refuse(reply, 500, "config-error", "The purchase is misconfigured");
179
+ }
180
+ if (!isAcceptableReturnUrl(returnUrl, config.TRUSTED_ORIGINS)) {
181
+ return refuse(reply, 400, "return_url_refused", "The return address is not allowed");
182
+ }
183
+
184
+ // 7. Not already a customer; a purchasable tier.
185
+ const [identity, entitlements] = await Promise.all([
186
+ getOrgBillingIdentity(tenant.organizationId),
187
+ resolveEntitlements(tenant.organizationId),
188
+ ]);
189
+ if (identity.billingCustomerId != null) {
190
+ return refuse(reply, 409, "billing_customer_exists", "This organization already has a plan");
191
+ }
192
+ const productCode = planPurchaseLookupKey(entitlements.limits.static.tier);
193
+ if (productCode === null) {
194
+ return refuse(reply, 409, "plan_not_purchasable", "This plan cannot be purchased here");
195
+ }
196
+
197
+ // 8. Stripe key — sandbox-only, as the Organization portal.
198
+ const stripeSecretKey = process.env["STRIPE_SECRET_KEY"];
199
+ if (!stripeSecretKey) {
200
+ return refuse(reply, 500, "stripe-not-configured", "Stripe is not configured");
201
+ }
202
+ try {
203
+ assertSandboxKey(stripeSecretKey);
204
+ } catch {
205
+ return refuse(reply, 500, "live-mode-forbidden", "Live Stripe keys are not permitted");
206
+ }
207
+
208
+ // 7b. One payable session at a time (D13).
209
+ const earlier = await readOrgPlanCheckoutSession(tenant.organizationId);
210
+ if (earlier !== null) {
211
+ let status: Awaited<ReturnType<typeof retrieveCheckoutSession>>["status"] | null;
212
+ let customer: string | null = null;
213
+ try {
214
+ const retrieved = await retrieveCheckoutSession({ sessionId: earlier });
215
+ status = retrieved.status;
216
+ customer = retrieved.customer;
217
+ } catch (err) {
218
+ if (!(err instanceof UnknownSessionError)) {
219
+ request.log.error(
220
+ { errName: err instanceof Error ? err.name : "unknown" },
221
+ "[billing] plan checkout read failed",
222
+ );
223
+ return checkoutUnavailable(reply);
224
+ }
225
+ status = null;
226
+ }
227
+ if (status === "complete") {
228
+ // The webhook's bind never landed (a lost event, a swallowed database
229
+ // error). A completed session carries its customer, so bind from it
230
+ // here, under the same guard the webhook uses; a bound Organization
231
+ // then reads exactly as step 7 reads one. Only a session with no
232
+ // customer yet stays pending.
233
+ if (customer !== null) {
234
+ let bound: boolean;
235
+ try {
236
+ bound = await bindOrgPlanCheckout(tenant.organizationId, earlier, customer);
237
+ } catch (err) {
238
+ request.log.error(
239
+ { errName: err instanceof Error ? err.name : "unknown" },
240
+ "[billing] plan checkout bind failed",
241
+ );
242
+ return checkoutUnavailable(reply);
243
+ }
244
+ if (bound) {
245
+ request.log.info({ outcome: "org_bound_from_completed_session" }, "[billing] plan checkout bound");
246
+ return refuse(reply, 409, "billing_customer_exists", "This organization already has a plan");
247
+ }
248
+ }
249
+ return refuse(reply, 409, "purchase_pending", "A purchase is already being confirmed");
250
+ }
251
+ if (status === "open") {
252
+ try {
253
+ await expireCheckoutSession(earlier);
254
+ } catch (err) {
255
+ request.log.error(
256
+ { errName: err instanceof Error ? err.name : "unknown" },
257
+ "[billing] plan checkout expire failed",
258
+ );
259
+ return checkoutUnavailable(reply);
260
+ }
261
+ }
262
+ }
263
+
264
+ // 9. Start.
265
+ const stamp = { organizationId: tenant.organizationId, ...siteStamp() };
266
+ let created: { sessionId: string; url: string };
267
+ try {
268
+ created = await createCheckoutSession({
269
+ productCode,
270
+ successUrl: returnUrl,
271
+ cancelUrl: returnUrl,
272
+ mode: "subscription",
273
+ adjustableQuantity: { enabled: true, minimum: SITES_MIN, maximum: SITES_MAX },
274
+ metadata: stamp,
275
+ subscriptionMetadata: { ...stamp },
276
+ termsOfService: PLATFORM_TERMS_OF_SERVICE,
277
+ });
278
+ } catch (err) {
279
+ const errName = err instanceof Error ? err.name : "unknown";
280
+ request.log.error({ errName }, "[billing] plan checkout create failed");
281
+ if (err instanceof UnknownProductCodeError || err instanceof CheckoutModeMismatchError) {
282
+ return refuse(reply, 500, "plan_price_missing", "The plan's price is not set up");
283
+ }
284
+ if (err instanceof StripeConfigError) {
285
+ return refuse(reply, 500, "config-error", "The purchase is misconfigured");
286
+ }
287
+ return checkoutUnavailable(reply);
288
+ }
289
+
290
+ // 10. A Checkout URL only — anything else is never handed out.
291
+ const answer = planCheckoutResponseSchema.safeParse({ url: created.url });
292
+ if (!answer.success) {
293
+ request.log.error({ outcome: "not_checkout_url" }, "[billing] plan checkout answer refused");
294
+ await expireQuietly(request, created.sessionId);
295
+ return checkoutUnavailable(reply);
296
+ }
297
+
298
+ // 9b. Record it — no payable session is left unrecorded.
299
+ let recorded: boolean;
300
+ try {
301
+ recorded = await setOrgPlanCheckoutSession(tenant.organizationId, earlier, created.sessionId);
302
+ } catch (err) {
303
+ request.log.error(
304
+ { errName: err instanceof Error ? err.name : "unknown" },
305
+ "[billing] plan checkout record failed",
306
+ );
307
+ await expireQuietly(request, created.sessionId);
308
+ return checkoutUnavailable(reply);
309
+ }
310
+ if (!recorded) {
311
+ await expireQuietly(request, created.sessionId);
312
+ return refuse(reply, 409, "purchase_pending", "A purchase is already being confirmed");
313
+ }
314
+
315
+ request.log.info(
316
+ { method: request.method, path: request.url, status: 200 },
317
+ "[billing] plan checkout created",
318
+ );
319
+ return reply.status(200).header("Cache-Control", NO_STORE).send({ ok: true, data: answer.data });
320
+ }
321
+
322
+ /** Registers the route beside the billing routes, in the same tenant scope (D3). */
323
+ export function registerPlanCheckoutRoute(fastify: FastifyInstance): void {
324
+ fastify.post("/v1/billing/plan-checkout", { bodyLimit: BODY_LIMIT }, planCheckoutHandler);
325
+ }
@@ -40,6 +40,17 @@ jest.mock("../../../lib/prisma.js", () => ({
40
40
  },
41
41
  }));
42
42
 
43
+ // #8931 — the route now asks the Offerings service which path a request
44
+ // takes before the env checks; every case in this suite is an env-path case,
45
+ // so the service answers `env` for both the POST and the GET.
46
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
47
+ jest.mock("../../../services/offerings/checkout-authorization.js", () => ({
48
+ resolveOfferingsCheckoutPath: jest.fn(async () => ({ kind: "env" })),
49
+ readOfferingsCheckoutSession: jest.fn(async () => ({ kind: "env" })),
50
+ startOfferingsCheckout: jest.fn(),
51
+ OFFERINGS_CHECKOUT_PER_WORKSPACE_PER_MINUTE: 60,
52
+ }));
53
+
43
54
  import { assertSandboxKey } from "@working-theory/stripe-client";
44
55
 
45
56
  import { config } from "../../../config/index.js";