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,216 @@
1
+ /**
2
+ * /pricing reads `?plan=<key>` into the Pricing section (#8842).
3
+ *
4
+ * A diagnostic that recommends a plan links to `/pricing?plan=<key>`. The
5
+ * page hands that key to every `pricing` section as its `recommended` value;
6
+ * the section moves the authored "Recommended" treatment onto the plan whose
7
+ * `key` matches (case-insensitive) and scrolls that card into view on load.
8
+ * No key, or a key no plan carries, leaves the authored recommendation and
9
+ * scrolls nothing.
10
+ *
11
+ * MDX is DI-injected (no fs reads); the real SectionRenderer and Pricing
12
+ * section render, so the assertions read the served DOM.
13
+ */
14
+ import { act, render, screen, within } from "@testing-library/react";
15
+
16
+ import PricingPage from "../page";
17
+
18
+ const PLANS = [
19
+ {
20
+ key: "basic",
21
+ name: "Basic",
22
+ price: { monthly: "$29" },
23
+ features: ["One site"],
24
+ cta: { label: "Choose Basic", href: "/checkout/basic", variant: "outline" },
25
+ },
26
+ {
27
+ key: "business",
28
+ name: "Business",
29
+ price: { monthly: "$79" },
30
+ features: ["Three sites"],
31
+ cta: {
32
+ label: "Choose Business",
33
+ href: "/checkout/business",
34
+ variant: "default",
35
+ },
36
+ highlighted: true,
37
+ badge: "Recommended",
38
+ },
39
+ {
40
+ key: "agency",
41
+ name: "Agency",
42
+ price: { monthly: "$199" },
43
+ features: ["Unlimited sites"],
44
+ cta: {
45
+ label: "Choose Agency",
46
+ href: "/checkout/agency",
47
+ variant: "outline",
48
+ },
49
+ },
50
+ ] as const;
51
+
52
+ const getMdxSource = jest.fn().mockResolvedValue({
53
+ frontmatter: {
54
+ type: "Pricing",
55
+ slug: "pricing",
56
+ title: "Pricing",
57
+ description: "Plans for every stage.",
58
+ },
59
+ body: "",
60
+ });
61
+
62
+ const getPageSections = jest.fn().mockResolvedValue([
63
+ {
64
+ id: "plans",
65
+ type: "pricing",
66
+ data: {
67
+ variant: "three-tiers-with-toggle",
68
+ billingToggle: false,
69
+ plans: PLANS,
70
+ },
71
+ },
72
+ ]);
73
+
74
+ const scrollIntoView = jest.fn();
75
+ let reducedMotion = false;
76
+
77
+ beforeAll(() => {
78
+ Element.prototype.scrollIntoView = scrollIntoView;
79
+ });
80
+
81
+ beforeEach(() => {
82
+ scrollIntoView.mockClear();
83
+ reducedMotion = false;
84
+ // Answer the reduced-motion query from `reducedMotion`.
85
+ jest.spyOn(window, "matchMedia").mockImplementation(
86
+ (query: string): MediaQueryList => ({
87
+ matches: query === "(prefers-reduced-motion: reduce)" && reducedMotion,
88
+ media: query,
89
+ onchange: null,
90
+ addEventListener: jest.fn(),
91
+ removeEventListener: jest.fn(),
92
+ addListener: jest.fn(),
93
+ removeListener: jest.fn(),
94
+ dispatchEvent: jest.fn(),
95
+ }),
96
+ );
97
+ });
98
+
99
+ // #8927 — the page reads the Offerings shown set first; these cases are an
100
+ // unconnected site's, so the read answers `unconnected` and the page is today's.
101
+ beforeEach(() => {
102
+ global.fetch = jest.fn(async () => ({
103
+ ok: true,
104
+ status: 200,
105
+ json: async () => ({ ok: true, data: { status: "unconnected" } }),
106
+ })) as jest.Mock;
107
+ });
108
+
109
+ afterEach(() => {
110
+ jest.restoreAllMocks();
111
+ });
112
+
113
+ async function renderPricing(search: Record<string, string | string[]>) {
114
+ const element = await PricingPage({
115
+ params: Promise.resolve({ locale: "en" }),
116
+ searchParams: Promise.resolve(search),
117
+ getMdxSource,
118
+ getPageSections,
119
+ });
120
+ await act(async () => {
121
+ render(element);
122
+ });
123
+ }
124
+
125
+ /** The tier card carrying the "Recommended" badge — the only highlighted card. */
126
+ function recommendedCard(): HTMLElement {
127
+ const badge = screen.getByTestId("tier-badge");
128
+ const card = badge.closest<HTMLElement>("[data-testid='pricing-tier-card']");
129
+ if (!card) throw new Error("badge is not inside a tier card");
130
+ return card;
131
+ }
132
+
133
+ describe("/pricing ?plan=<key> (#8842)", () => {
134
+ it("moves the Recommended badge and the filled button to the named plan", async () => {
135
+ await renderPricing({ plan: "agency" });
136
+
137
+ expect(screen.getAllByTestId("tier-badge")).toHaveLength(1);
138
+ const card = recommendedCard();
139
+ expect(within(card).getByText("Agency")).toBeTruthy();
140
+ expect(card.getAttribute("aria-current")).toBe("true");
141
+ // The badge says why this card is emphasised — a recommendation, never the
142
+ // authored text, which may be a popularity claim about a different plan.
143
+ expect(screen.getByTestId("tier-badge").textContent).toBe("Recommended for you");
144
+
145
+ // The authored recommendation gives up its emphasis and its filled CTA.
146
+ const cards = screen.getAllByTestId("pricing-tier-card");
147
+ expect(cards.filter((c) => c.getAttribute("aria-current") === "true")).toEqual([card]);
148
+ const business = cards.find((c) => within(c).queryByText("Business"));
149
+ expect(
150
+ within(business!).getByRole("link", { name: "Choose Business" }).className,
151
+ ).toBe(within(cards[0]!).getByRole("link", { name: "Choose Basic" }).className);
152
+ expect(within(card).getByRole("link", { name: "Choose Agency" }).className).not.toBe(
153
+ within(business!).getByRole("link", { name: "Choose Business" }).className,
154
+ );
155
+ });
156
+
157
+ it("matches the plan key case-insensitively", async () => {
158
+ await renderPricing({ plan: "BASIC" });
159
+
160
+ expect(within(recommendedCard()).getByText("Basic")).toBeTruthy();
161
+ });
162
+
163
+ it("scrolls the recommended card into view, centred and smooth", async () => {
164
+ await renderPricing({ plan: "agency" });
165
+
166
+ expect(scrollIntoView).toHaveBeenCalledTimes(1);
167
+ expect(scrollIntoView.mock.contexts[0]).toBe(recommendedCard());
168
+ expect(scrollIntoView).toHaveBeenCalledWith({
169
+ block: "center",
170
+ behavior: "smooth",
171
+ });
172
+ });
173
+
174
+ it("jumps without animation when the visitor prefers reduced motion", async () => {
175
+ reducedMotion = true;
176
+ await renderPricing({ plan: "agency" });
177
+
178
+ expect(scrollIntoView).toHaveBeenCalledWith({
179
+ block: "center",
180
+ behavior: "auto",
181
+ });
182
+ });
183
+
184
+ it.each([
185
+ ["no plan parameter", {}],
186
+ ["an unknown key", { plan: "enterprise" }],
187
+ ["an empty key", { plan: "" }],
188
+ ])("keeps the authored recommendation and does not scroll for %s", async (_label, search) => {
189
+ await renderPricing(search);
190
+
191
+ expect(screen.getAllByTestId("tier-badge")).toHaveLength(1);
192
+ expect(within(recommendedCard()).getByText("Business")).toBeTruthy();
193
+ expect(scrollIntoView).not.toHaveBeenCalled();
194
+ });
195
+
196
+ it("keeps the authored recommendation when the key names a plan the variant does not show", async () => {
197
+ // two-tiers-with-extra-tier renders the first two plans only.
198
+ getPageSections.mockResolvedValueOnce([
199
+ {
200
+ id: "plans",
201
+ type: "pricing",
202
+ data: { variant: "two-tiers-with-extra-tier", plans: PLANS },
203
+ },
204
+ ]);
205
+ await renderPricing({ plan: "agency" });
206
+
207
+ expect(within(recommendedCard()).getByText("Business")).toBeTruthy();
208
+ expect(scrollIntoView).not.toHaveBeenCalled();
209
+ });
210
+
211
+ it("reads the first value when the parameter repeats", async () => {
212
+ await renderPricing({ plan: ["agency", "basic"] });
213
+
214
+ expect(within(recommendedCard()).getByText("Agency")).toBeTruthy();
215
+ });
216
+ });
@@ -0,0 +1,236 @@
1
+ /**
2
+ * An unconnected site's /pricing is today's page, byte for byte (#8927 AC-4,
3
+ * spec docs/specs/8927-pricing-renders-the-shown-set-spec.md D2 step 1, D7).
4
+ *
5
+ * Two oracles. The committed snapshot was recorded against `page.tsx` as it
6
+ * stood before #8927, in an earlier commit than the page change, and is never
7
+ * regenerated. The equality oracle renders `renderPage` called directly with
8
+ * today's `?plan=` wrapper (copied below), so it holds whatever the commit
9
+ * order. Both read today's DI fixture; the equality oracle also reads the
10
+ * site's own `content/en/Pricing/pricing.mdx` through the default loader. The
11
+ * snapshot of the monorepo's own pricing.mdx is `monorepo-only.test.tsx`'s,
12
+ * which no scaffold ships (#9179).
13
+ *
14
+ * `global.fetch` answers the shown-offerings route's `unconnected`; with
15
+ * `NODE_ENV` `production` and no API origin, the page makes no call at all.
16
+ */
17
+ import { act, render } from "@testing-library/react";
18
+ import {
19
+ getPageSections as loadPageSections,
20
+ renderPage,
21
+ renderPageMetadata,
22
+ } from "@working-theory/page-renderer";
23
+ import type { PageSection } from "@working-theory/validation";
24
+ import type { ReactElement } from "react";
25
+
26
+ import * as pricingPage from "../page";
27
+ import PricingPage, { generateMetadata } from "../page";
28
+
29
+ const PLANS = [
30
+ {
31
+ key: "basic",
32
+ name: "Basic",
33
+ price: { monthly: "$29" },
34
+ features: ["One site"],
35
+ cta: { label: "Choose Basic", href: "/checkout/basic", variant: "outline" },
36
+ },
37
+ {
38
+ key: "business",
39
+ name: "Business",
40
+ price: { monthly: "$79" },
41
+ features: ["Three sites"],
42
+ cta: {
43
+ label: "Choose Business",
44
+ href: "/checkout/business",
45
+ variant: "default",
46
+ },
47
+ highlighted: true,
48
+ badge: "Recommended",
49
+ },
50
+ {
51
+ key: "agency",
52
+ name: "Agency",
53
+ price: { monthly: "$199" },
54
+ features: ["Unlimited sites"],
55
+ cta: {
56
+ label: "Choose Agency",
57
+ href: "/checkout/agency",
58
+ variant: "outline",
59
+ },
60
+ },
61
+ ] as const;
62
+
63
+ const getMdxSource = jest.fn().mockResolvedValue({
64
+ frontmatter: {
65
+ type: "Pricing",
66
+ slug: "pricing",
67
+ title: "Pricing",
68
+ description: "Plans for every stage.",
69
+ },
70
+ body: "",
71
+ });
72
+
73
+ const getPageSections = jest.fn().mockResolvedValue([
74
+ {
75
+ id: "plans",
76
+ type: "pricing",
77
+ data: {
78
+ variant: "three-tiers-with-toggle",
79
+ billingToggle: false,
80
+ plans: PLANS,
81
+ },
82
+ },
83
+ ]);
84
+
85
+ /** Today's `withRecommendedPlan` (#8842), copied as the oracle's wrapper. */
86
+ function todaysSections(
87
+ load: (frontmatter: unknown) => Promise<PageSection[]>,
88
+ plan: string | undefined,
89
+ ): (frontmatter: unknown) => Promise<PageSection[]> {
90
+ if (!plan) return load;
91
+ return async (frontmatter) =>
92
+ (await load(frontmatter)).map((section) =>
93
+ section.type === "pricing"
94
+ ? {
95
+ ...section,
96
+ data: { ...section.data, recommended: plan } as typeof section.data,
97
+ }
98
+ : section,
99
+ );
100
+ }
101
+
102
+ const API_ENV = [
103
+ "AILK_API_URL",
104
+ "NEXT_PUBLIC_AILK_API_URL",
105
+ "NEXT_PUBLIC_API_URL",
106
+ ] as const;
107
+ const priorEnv = new Map<string, string | undefined>(
108
+ [...API_ENV, "NODE_ENV"].map((name) => [name, process.env[name]]),
109
+ );
110
+
111
+ const fetchMock: jest.Mock = jest.fn(async () => ({
112
+ ok: true,
113
+ status: 200,
114
+ json: async () => ({ ok: true, data: { status: "unconnected" } }),
115
+ }));
116
+
117
+ let clock = Date.now();
118
+
119
+ beforeAll(() => {
120
+ Element.prototype.scrollIntoView = jest.fn();
121
+ });
122
+
123
+ beforeEach(() => {
124
+ // Each case reads afresh: the page shares one read for 15 seconds.
125
+ clock += 16_000;
126
+ jest.spyOn(Date, "now").mockImplementation(() => clock);
127
+ global.fetch = fetchMock;
128
+ fetchMock.mockClear();
129
+ });
130
+
131
+ afterEach(() => {
132
+ jest.restoreAllMocks();
133
+ for (const [name, value] of priorEnv) {
134
+ if (value === undefined)
135
+ delete (process.env as Record<string, string | undefined>)[name];
136
+ else (process.env as Record<string, string | undefined>)[name] = value;
137
+ }
138
+ });
139
+
140
+ /**
141
+ * `useId` values renumbered by first appearance. A client render draws them
142
+ * from one counter shared by every root in the process, so the same tree
143
+ * rendered second gets different numbers; the served (server) HTML does not.
144
+ */
145
+ function stableIds(markup: string): string {
146
+ const seen = new Map<string, string>();
147
+ return markup.replace(/_r_[0-9a-z]+_/g, (id) => {
148
+ if (!seen.has(id)) seen.set(id, `_r_id${seen.size}_`);
149
+ return seen.get(id)!;
150
+ });
151
+ }
152
+
153
+ async function html(element: ReactElement): Promise<string> {
154
+ let container: HTMLElement | undefined;
155
+ await act(async () => {
156
+ ({ container } = render(element));
157
+ });
158
+ return stableIds(container!.innerHTML);
159
+ }
160
+
161
+ async function pageHtml(
162
+ search: Record<string, string>,
163
+ di: boolean,
164
+ ): Promise<string> {
165
+ return html(
166
+ await PricingPage({
167
+ params: Promise.resolve({ locale: "en" }),
168
+ searchParams: Promise.resolve(search),
169
+ ...(di ? { getMdxSource, getPageSections } : {}),
170
+ }),
171
+ );
172
+ }
173
+
174
+ async function oracleHtml(
175
+ plan: string | undefined,
176
+ di: boolean,
177
+ ): Promise<string> {
178
+ return html(
179
+ await renderPage({
180
+ type: "Pricing",
181
+ slug: "pricing",
182
+ locale: "en",
183
+ translationsNamespace: "pricing",
184
+ ...(di ? { getMdxSource } : {}),
185
+ getPageSections: todaysSections(
186
+ di ? getPageSections : loadPageSections,
187
+ plan,
188
+ ),
189
+ }),
190
+ );
191
+ }
192
+
193
+ describe("/pricing on an unconnected site is today's page (#8927 AC-4)", () => {
194
+ it.each([
195
+ ["no plan", {}],
196
+ ["?plan=agency", { plan: "agency" }],
197
+ ["?plan=BASIC", { plan: "BASIC" }],
198
+ ] as const)("renders today's DI page for %s", async (_label, search) => {
199
+ const served = await pageHtml(search, true);
200
+ const plan = "plan" in search ? search.plan : undefined;
201
+
202
+ expect(served).toBe(await oracleHtml(plan, true));
203
+ expect(served).toMatchSnapshot();
204
+ });
205
+
206
+ it("renders the site's own pricing.mdx unchanged", async () => {
207
+ const served = await pageHtml({}, false);
208
+
209
+ expect(served).toBe(await oracleHtml(undefined, false));
210
+ });
211
+
212
+ it("makes no call and renders today's page in production with no API origin", async () => {
213
+ for (const name of API_ENV) delete process.env[name];
214
+ (process.env as Record<string, string>).NODE_ENV = "production";
215
+
216
+ const served = await pageHtml({}, true);
217
+
218
+ expect(fetchMock).not.toHaveBeenCalled();
219
+ expect(served).toBe(await oracleHtml(undefined, true));
220
+ });
221
+
222
+ it("keeps the MDX page's metadata and renders per request", async () => {
223
+ const params = Promise.resolve({ locale: "en" });
224
+
225
+ expect(await generateMetadata({ params })).toEqual(
226
+ await renderPageMetadata({
227
+ type: "Pricing",
228
+ slug: "pricing",
229
+ locale: "en",
230
+ }),
231
+ );
232
+ expect((pricingPage as { dynamic?: unknown }).dynamic).toBe(
233
+ "force-dynamic",
234
+ );
235
+ });
236
+ });