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
@@ -57,6 +57,48 @@
57
57
  * file leaves nothing to find, and its retry writes again. `subscribe` and
58
58
  * `callback` never read the header. The key is never logged or echoed.
59
59
  *
60
+ * CONTACT TERMS ACCEPTANCE (#8730). A `submit` whose `source` is a
61
+ * consent-bearing source (`isConsentLeadSource` in @working-theory/validation:
62
+ * the contact sources `contact-form`, `conversational-intake`, `flow:contact`,
63
+ * and since #9130 the Free apply page's `flow:job-application`) must carry a
64
+ * top-level `consent: true` — the boolean, nothing else — or it is
65
+ * `400 consent_required` before the tenant read or any write. An accepted one
66
+ * gets `payload.consent = { statement, acceptedAt }` written HERE, replacing
67
+ * any `payload.consent` the client sent: `statement` is the platform's
68
+ * contact terms statement (`CONTACT_FORM_TERMS_STATEMENT`) and `acceptedAt`
69
+ * this request's receive time. The `consent` flag itself is never stored. A
70
+ * lead with any other source, `subscribe` and `callback` are unchanged. A
71
+ * direct caller can still send `consent: true`; the gate makes acceptance an
72
+ * explicit, recorded assertion, not proof that a person read the terms.
73
+ *
74
+ * API↔MCP PARITY for `consent` (#8730): no guard entry, for the reason the
75
+ * Idempotency-Key paragraph below gives — this route parses no validation
76
+ * export, so it binds as `api-args-unbound`. `capture_lead` stamps `mcp`, a
77
+ * source this check never reads, so the tool is unchanged.
78
+ *
79
+ * A MAGNET'S COMPLETION (#8757 D6). After a `flow:<slug>` submit is written
80
+ * to the database, `services/magnet-completion.ts` reads that LIVE magnet's
81
+ * completion setting. On `invite` the 201 gains `next: { completion:
82
+ * "invite" }` and — on a first write, never a replay — the submitter is sent
83
+ * one confirmation email whose link starts sign-in with their address
84
+ * prefilled (capped per recipient and per magnet, counted in the database);
85
+ * on `checkout`, re-checked against the `commerce` claim and the published
86
+ * offerings, it gains `next: { completion: "checkout", checkoutOffering }`.
87
+ * `lead`, a draft or missing magnet, or a caller with nowhere to sign in
88
+ * leaves the 201 as it was. A failed read or send never changes the 201,
89
+ * and the completion read has a time budget (`MAGNET_COMPLETION_BUDGET_MS`).
90
+ * `next` is reachable only through a `flow:` source: `capture_lead` posts
91
+ * `source: "mcp"`, so the tool's arguments get the same answer as before and
92
+ * no parity guard row changes (NFR5).
93
+ *
94
+ * A CHECKOUT'S ACCOUNT LINK (#8757 D14). A checkout fulfilment's submit
95
+ * (`source: "web"`, `payload.page: "checkout"`) carries the caller's account
96
+ * origin into `routeLead` as `accountOrigin`, resolved exactly as the invite
97
+ * link's (`accountTargetFor`): the spoke's enabled sign-in client's callback
98
+ * origin, or `FRONTEND_URL`'s on a keyless build with an account area. With
99
+ * none, or a failed read, it is left out and the buyer's purchase email goes
100
+ * without the link. No other submit makes this read, and the 201 is unchanged.
101
+ *
60
102
  * API↔MCP PARITY for the header (#7869 D9, roadmap §12 Q5): NO guard entry.
61
103
  * The guard binds a route's arguments to the validation export its handler
62
104
  * `.safeParse`s on `request.params|query|body`; the header check below is a
@@ -67,6 +109,11 @@
67
109
  */
68
110
 
69
111
  import { track } from "@working-theory/analytics/server";
112
+ import {
113
+ CONTACT_FORM_TERMS_STATEMENT,
114
+ isConsentLeadSource,
115
+ type LeadSubmitNext,
116
+ } from "@working-theory/validation";
70
117
  import type { FastifyPluginAsync } from "fastify";
71
118
 
72
119
  import { acceptsKind, resolveLeadDelivery } from "../../lib/lead-delivery.js";
@@ -81,6 +128,11 @@ import {
81
128
  routeLead,
82
129
  type SourceMeta,
83
130
  } from "../../services/lead-routing.js";
131
+ import {
132
+ accountTargetFor,
133
+ magnetCompletionWithinBudget,
134
+ sendInviteConfirmation,
135
+ } from "../../services/magnet-completion.js";
84
136
  import { resolveCallerSite } from "../../services/public-site-resolver.js";
85
137
  import { sendRequestConfirmLink } from "../../services/request-confirm.js";
86
138
 
@@ -105,6 +157,29 @@ function liftString(
105
157
  return (typeof fromPayload === "string" ? fromPayload : topLevel) as string;
106
158
  }
107
159
 
160
+ /** #8730, #9130 — the refusal a consent-bearing lead without terms acceptance gets. */
161
+ const CONSENT_REQUIRED_BODY = {
162
+ error: "consent_required",
163
+ message: "Accept the terms to send this form.",
164
+ } as const;
165
+
166
+ /**
167
+ * #8730 D3, #9130 — the accepted terms, recorded by the server on a
168
+ * consent-bearing lead's payload, replacing any `consent` the client wrote
169
+ * there. Only a plain object payload is stamped: anything else is left for
170
+ * `routeLead`'s schema to refuse, never spread into an object that would then
171
+ * pass it.
172
+ */
173
+ function withConsentRecord(payload: unknown, acceptedAt: string): unknown {
174
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
175
+ return payload;
176
+ }
177
+ return {
178
+ ...(payload as Record<string, unknown>),
179
+ consent: { statement: CONTACT_FORM_TERMS_STATEMENT, acceptedAt },
180
+ };
181
+ }
182
+
108
183
  /**
109
184
  * Build a typed RouteLeadInput from the raw request body for a given kind.
110
185
  * Explicit destructure per kind prevents unknown fields (e.g. attacker-supplied
@@ -122,6 +197,7 @@ function buildInput(
122
197
  kind: LeadKind,
123
198
  body: Record<string, unknown>,
124
199
  workspaceId: string,
200
+ receivedAt: string,
125
201
  ): RouteLeadInput {
126
202
  const payload = (body["payload"] ?? {}) as Record<string, unknown>;
127
203
 
@@ -131,7 +207,9 @@ function buildInput(
131
207
  siteId: workspaceId,
132
208
  source: body["source"] as string,
133
209
  email: liftString(body, payload, "email"),
134
- payload,
210
+ payload: isConsentLeadSource(body["source"])
211
+ ? (withConsentRecord(payload, receivedAt) as Record<string, unknown>)
212
+ : payload,
135
213
  };
136
214
  }
137
215
  if (kind === "subscribe") {
@@ -154,10 +232,32 @@ function buildInput(
154
232
  };
155
233
  }
156
234
 
235
+ /** A spoke checkout fulfilment's submit (#8757 D14): `source: "web"`, `payload.page: "checkout"`. */
236
+ function isCheckoutSubmit(body: Record<string, unknown>): boolean {
237
+ const payload = body["payload"];
238
+ return (
239
+ body["source"] === "web" &&
240
+ typeof payload === "object" &&
241
+ payload !== null &&
242
+ (payload as Record<string, unknown>)["page"] === "checkout"
243
+ );
244
+ }
245
+
157
246
  function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
158
247
  return async (fastify) => {
159
248
  fastify.post(`/v1/leads/${kind}`, async (request, reply) => {
160
249
  const body = request.body as Record<string, unknown>;
250
+
251
+ // #8730 D1, #9130 — a contact lead or a job application carries the
252
+ // visitor's terms acceptance, or it is refused before the tenant read
253
+ // and any write.
254
+ if (
255
+ kind === "submit" &&
256
+ isConsentLeadSource(body?.["source"]) &&
257
+ body?.["consent"] !== true
258
+ ) {
259
+ return reply.status(400).send(CONSENT_REQUIRED_BODY);
260
+ }
161
261
  // request.ip became attacker-influenceable when trustProxy: true went
162
262
  // instance-wide (server.ts, #4730/#4778) — leads/index.ts was never
163
263
  // itself touched by that PR. Traced every consumer of this value
@@ -264,10 +364,20 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
264
364
  sourceMeta.consentVisitorId = consentVisitorId;
265
365
  }
266
366
 
367
+ // #8757 D14 — where a buyer's account lives, for the purchase email.
368
+ let accountOrigin: string | undefined;
369
+ if (kind === "submit" && isCheckoutSubmit(body)) {
370
+ const target = await accountTargetFor(caller).catch(() => null);
371
+ accountOrigin = target?.origin;
372
+ }
373
+
267
374
  const result = await routeLead(
268
- buildInput(kind, body, workspaceId),
375
+ buildInput(kind, body, workspaceId, sourceMeta.receivedAt),
269
376
  sourceMeta,
270
- idempotencyKey === undefined ? {} : { idempotencyKey },
377
+ {
378
+ ...(idempotencyKey !== undefined && { idempotencyKey }),
379
+ ...(accountOrigin !== undefined && { accountOrigin }),
380
+ },
271
381
  );
272
382
 
273
383
  if (!result.ok) {
@@ -357,10 +467,12 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
357
467
  result.persisted === "db" &&
358
468
  caller.kind !== "spoke"
359
469
  ) {
360
- const callbackInput = buildInput(kind, body, workspaceId) as Extract<
361
- RouteLeadInput,
362
- { kind: "callback" }
363
- >;
470
+ const callbackInput = buildInput(
471
+ kind,
472
+ body,
473
+ workspaceId,
474
+ sourceMeta.receivedAt,
475
+ ) as Extract<RouteLeadInput, { kind: "callback" }>;
364
476
  sendRequestConfirmLink(result.id, {
365
477
  name: callbackInput.name,
366
478
  phone: callbackInput.phone,
@@ -370,6 +482,50 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
370
482
  });
371
483
  }
372
484
 
485
+ // #8757 D6 — a written `flow:<slug>` submit answers its magnet's
486
+ // completion. A read that fails, or outlasts its budget, leaves the 201
487
+ // as it was: the lead is saved.
488
+ let next: LeadSubmitNext | undefined;
489
+ const source = body["source"];
490
+ if (
491
+ kind === "submit" &&
492
+ result.persisted === "db" &&
493
+ typeof source === "string" &&
494
+ source.startsWith("flow:")
495
+ ) {
496
+ const slug = source.slice("flow:".length);
497
+ try {
498
+ const completion = await magnetCompletionWithinBudget(workspaceId, slug, () =>
499
+ accountTargetFor(caller),
500
+ );
501
+ if (completion !== null) {
502
+ next = completion.next;
503
+ if (completion.next.completion === "invite" && !result.replayed) {
504
+ const submitInput = buildInput(
505
+ kind,
506
+ body,
507
+ workspaceId,
508
+ sourceMeta.receivedAt,
509
+ ) as Extract<RouteLeadInput, { kind: "submit" }>;
510
+ const locale = submitInput.payload["locale"];
511
+ void sendInviteConfirmation({
512
+ workspaceId,
513
+ slug,
514
+ email: submitInput.email,
515
+ locale: typeof locale === "string" ? locale : undefined,
516
+ completion,
517
+ now: new Date(),
518
+ });
519
+ }
520
+ }
521
+ } catch (error) {
522
+ request.log.error(
523
+ { event: "magnet-completion", error: error instanceof Error ? error.name : "unknown" },
524
+ "lead magnet completion read failed",
525
+ );
526
+ }
527
+ }
528
+
373
529
  // Fire-and-forget: track() never throws; no need to hold the response.
374
530
  // #7869 D6 — a replay records nothing new: the first write counted it.
375
531
  if (!result.replayed) {
@@ -385,6 +541,7 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
385
541
  id: result.id,
386
542
  persisted: result.persisted,
387
543
  email: result.email,
544
+ ...(next === undefined ? {} : { next }),
388
545
  });
389
546
  });
390
547
  };
@@ -199,6 +199,72 @@ through `metadata.site`.
199
199
  site, which silences the hub's dunning record for that subscription — the
200
200
  same key can already cancel or re-price it outright.
201
201
 
202
+ ## Per-Organization endpoint (#8932)
203
+
204
+ `POST /v1/webhooks/stripe/:connectionToken` (`stripe-offerings.ts`) is the
205
+ receiver for one Organization's own Stripe connection — the Offerings money
206
+ path (spec `docs/specs/8932-the-per-organization-stripe-webhook-spec.md`,
207
+ contract `docs/specs/8879-the-offerings-security-contract-key-spec.md` W1,
208
+ W3–W11, G5, T8). The 43-character token selects one `StripeConnection` row
209
+ (`findStripeConnectionByWebhookToken`, #8907); that row's decrypted secret
210
+ verifies the signature through `verifyWebhookSignatureKeyless` (no env key),
211
+ and its `organizationId` is the only Organization the event can act for.
212
+ Registered beside the legacy route on every build, with its own 100/min
213
+ per-IP bucket; the commerce claim is read at call time.
214
+
215
+ **The ladder** — the first step that answers ends the request; every `200`
216
+ body is `{ received: true }` and the outcome is the log line's only:
217
+
218
+ | # | Step | Answer | Seen |
219
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------- |
220
+ | 1 | the token resolves to no row (or is malformed) | `404 not-found`, logs nothing | no |
221
+ | 1a | the token lookup rejects | `503 webhook-unavailable` | no |
222
+ | 2 | the row has no usable secret | `503 webhook-not-configured` | no |
223
+ | 3–6 | signature settled, then the claim (as the legacy route): `undetermined` → `503` + `Retry-After: 300`; `unclaimed` → signed `200 dropped_no_commerce_claim`, unsigned the module-disabled `404`; then the held `400` | as stated | no |
224
+ | 7 | `event.livemode` is not the row's mode | `200 livemode_mismatch` | no |
225
+ | 8 | not `checkout.session.completed` / `.async_payment_succeeded` on a session object | `200 ignored_type` | no |
226
+ | 9–11 | the hub's session record (#8930): rejects / unreadable / none | `503 record-unavailable` / `500 record-unreadable` / `200 unrecorded` | no |
227
+ | 12 | another Organization's record, or `metadata.site` is not the record's workspace | `200 foreign_organization` | no |
228
+ | 13 | the record's mode is not the row's | `200 livemode_mismatch` | no |
229
+ | 14 | already seen for this Organization | `200 duplicate` | — |
230
+ | 15 | a `completed` session neither `paid` nor `no_payment_required` | `200 unpaid` | yes |
231
+ | 16 | the connection's client or its `listLineItems` call fails | `503 stripe-unavailable` (never the env key) | no |
232
+ | 17 | not exactly one line item whose Price id is the record's | `200 offering_mismatch` | yes |
233
+ | 18 | `routeLead` (below) | `200 fulfilled` / `already_fulfilled` / `unfulfillable`, else `503 fulfilment-unavailable` | yes / no |
234
+
235
+ **The lead.** One per session: `routeLead` with the idempotency key
236
+ `"stripe-offerings " + sessionId` — a space, which no `Idempotency-Key`
237
+ header can carry, so no anonymous lead can take the key first — and
238
+ `offeringsCheckout: true`. Its offering is the record's FULL lookup key,
239
+ never `metadata.ailk_offering`. A live session's lead is marked
240
+ `spokeCheckout`, the hold #8793's `releaseHeldCheckoutPlan` releases at the
241
+ buyer's verified first link; no path here writes `Person.planOffering`. A
242
+ test-mode session's lead is written unmarked: no hold, purchase message,
243
+ email or consent grant. A replay or a conflict on that key is
244
+ `already_fulfilled`; a lead the database did not take is a `503`, so Stripe
245
+ retries.
246
+
247
+ **The legacy endpoint steps aside (W8).** `POST /v1/webhooks/stripe` acks
248
+ any event carrying an `ailk_offering` key — in a session's metadata or any
249
+ site-stamp source above — with `200 { received: true }` and
250
+ `outcome: "offerings_path"`, **before** `markSeen`, and writes nothing.
251
+ Every event without the key takes the legacy path unchanged.
252
+
253
+ **Two dedup stores.** The legacy route keeps `hasSeen` / `markSeen`; the
254
+ per-Organization route uses its own `hasSeenForOrganization` /
255
+ `markSeenForOrganization`, keyed `<organizationId>:<eventId>`. Neither
256
+ endpoint's store can swallow the other's event. Both are in-memory per
257
+ instance; the per-Organization route's correctness rests on the lead's
258
+ database key.
259
+
260
+ **Logs.** Each line carries `organizationId`, `eventType`, an 8-character
261
+ `eventIdPrefix` and `outcome` (fewer before an event is verified) — never
262
+ the token, the secret, a full Stripe id, an email, a lookup key, a metadata
263
+ value or Stripe product text. Request and error lines log the path with
264
+ the token replaced whole by `:connectionToken` (`redactLogPath`).
265
+
266
+ **W11.** No billing identity is bound from a connection's event.
267
+
202
268
  ## Dunning (#6034)
203
269
 
204
270
  Two arms record a per-subscriber lifecycle state so a failed renewal is no