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
@@ -1,199 +0,0 @@
1
- ---
2
- type: "Pricing"
3
- slug: "pricing"
4
- title: "{Upgrade to Pro with Commerce}"
5
- description: "{Checkout isn't included on the Free template. Upgrade to Pro with Commerce and take payments on your site with Stripe.}"
6
- sections:
7
- [
8
- {
9
- "id": "hero",
10
- "type": "hero",
11
- "data":
12
- {
13
- "variant": "centered",
14
- "gapBelow": "tight",
15
- "eyebrow": "Pricing",
16
- "headline": "{Upgrade to Pro with Commerce}",
17
- "subheadline": "{Checkout isn't included on the Free template. Upgrade to Pro with Commerce and take payments on your site with Stripe.}",
18
- "primaryCta":
19
- {
20
- "label": "{Add Commerce for SMB}",
21
- "href": "https://ailaunchkit.ai/pricing",
22
- },
23
- "secondaryCta":
24
- {
25
- "label": "{Add Commerce for SaaS}",
26
- "href": "https://ailaunchkit.ai/pricing",
27
- "variant": "outline",
28
- },
29
- },
30
- },
31
- {
32
- "id": "plans",
33
- "type": "pricing",
34
- "data":
35
- {
36
- "variant": "three-tiers-with-toggle",
37
- "billingToggle": true,
38
- "toggleStyle": "switch",
39
- "gapAbove": "tight",
40
- "cardLayout": "centered",
41
- "plans":
42
- [
43
- {
44
- "name": "Basic plan",
45
- "price": { "monthly": "{price}/mo", "annual": "{price}/yr" },
46
- "features":
47
- [
48
- "{A feature this plan includes}",
49
- "{A feature this plan includes}",
50
- "{A feature this plan includes}",
51
- ],
52
- "cta": { "label": "{Primary CTA}", "href": "/contact" },
53
- },
54
- {
55
- "name": "Business plan",
56
- "price": { "monthly": "{price}/mo", "annual": "{price}/yr" },
57
- "features":
58
- [
59
- "{A feature this plan adds}",
60
- "{A feature this plan includes}",
61
- "{A feature this plan includes}",
62
- "{A feature this plan includes}",
63
- ],
64
- "cta": { "label": "{Primary CTA}", "href": "/contact" },
65
- },
66
- {
67
- "name": "Enterprise plan",
68
- "price": { "monthly": "{price}/mo", "annual": "{price}/yr" },
69
- "features":
70
- [
71
- "{A feature this plan adds}",
72
- "{A feature this plan includes}",
73
- "{A feature this plan includes}",
74
- "{A feature this plan includes}",
75
- "{A feature this plan includes}",
76
- ],
77
- "cta": { "label": "{Primary CTA}", "href": "/contact" },
78
- },
79
- ],
80
- },
81
- },
82
- {
83
- "id": "logo-cloud",
84
- "type": "logoCloud",
85
- "data":
86
- {
87
- "headline": "{Don’t just take our word for it}",
88
- "headlineStyle": "label",
89
- "logos":
90
- [
91
- {
92
- "name": "SalesSmyth",
93
- "src": "/logos/salessmyth.svg",
94
- "darkSrc": "/logos/salessmyth-light.svg",
95
- "width": 1118,
96
- "height": 147,
97
- },
98
- {
99
- "name": "AI Launch Kit",
100
- "src": "/logos/ai-launch-kit.svg",
101
- "darkSrc": "/logos/ai-launch-kit-light.svg",
102
- "width": 1248,
103
- "height": 144,
104
- },
105
- {
106
- "name": "Atomic Agent",
107
- "src": "/logos/atomic-agent.svg",
108
- "darkSrc": "/logos/atomic-agent-light.svg",
109
- "width": 1320,
110
- "height": 148,
111
- },
112
- ],
113
- "layout": "inline",
114
- "treatment": "monochrome",
115
- },
116
- },
117
- {
118
- "id": "testimonial",
119
- "type": "testimonials",
120
- "data":
121
- {
122
- "variant": "quote",
123
- "items":
124
- [
125
- {
126
- "quote": "{A customer quote, in their own words, about the result they got}",
127
- "author": "{Customer name}",
128
- "role": "{Their title, their company}",
129
- },
130
- ],
131
- },
132
- },
133
- {
134
- "id": "switch",
135
- "type": "cta",
136
- "data":
137
- {
138
- "variant": "box",
139
- "headline": "{Switch to your product in time amount or less}",
140
- "description": "{You shouldn't have to experience a painful problem. With your product, you can finally achieve a value outcome or get a big improvement.}",
141
- "checklist":
142
- ["No credit card required", "Cancel any time", "Live chat support"],
143
- "primaryCta": { "label": "{Primary CTA}", "href": "/contact" },
144
- "secondaryCta":
145
- {
146
- "label": "{Secondary CTA}",
147
- "href": "/pricing",
148
- "variant": "outline",
149
- },
150
- "closingLine": "{Social Proof goes here}",
151
- },
152
- },
153
- {
154
- "id": "faq",
155
- "type": "faq",
156
- "data":
157
- {
158
- "headline": "{FAQs}",
159
- "description": "{One sentence that invites visitors to ask what this list does not answer}",
160
- "actions":
161
- [
162
- {
163
- "kind": "button",
164
- "label": "Contact",
165
- "href": "/contact",
166
- "variant": "outline",
167
- },
168
- ],
169
- "presentation": "accordion",
170
- "initialOpen": "none",
171
- "items":
172
- [
173
- {
174
- "question": "{A question your customers actually ask}",
175
- "answer": "{Answer the question in two or three plain sentences}",
176
- },
177
- {
178
- "question": "{A question your customers actually ask}",
179
- "answer": "{Answer the question in two or three plain sentences}",
180
- },
181
- {
182
- "question": "{A question your customers actually ask}",
183
- "answer": "{Answer the question in two or three plain sentences}",
184
- },
185
- {
186
- "question": "{A question your customers actually ask}",
187
- "answer": "{Answer the question in two or three plain sentences}",
188
- },
189
- {
190
- "question": "{A question your customers actually ask}",
191
- "answer": "{Answer the question in two or three plain sentences}",
192
- },
193
- ],
194
- },
195
- },
196
- ]
197
- ---
198
-
199
- {/* Rendered from frontmatter `sections[]`. Seed content — replace the plan names, prices and features above with your own before you launch. */}
@@ -1,125 +0,0 @@
1
- "use server";
2
-
3
- /**
4
- * sign-up.actions.ts — mints the sign-up card's carried-agreement cookie
5
- * (#8438 D13, spec
6
- * docs/specs/8438-consent-registry-and-ledger-step-spec.md C6).
7
- *
8
- * `apps/web` never touches the database (the ledger writer is `apps/api`),
9
- * and Better Auth creates the user from THIS app before any Person exists to
10
- * record a grant against. So the card remembers which agreements the person
11
- * ticked, in a short-lived, signed, email-bound cookie — the account home's
12
- * first signed-in visit (`(authed)/account/actions.ts`'s
13
- * `acceptCarriedAgreementsAction`) consumes it, one write per agreement,
14
- * then deletes it either way (single-use).
15
- *
16
- * The CONSUME side lives in the account area, not here, deliberately:
17
- * `(auth)/**` is one of the sign-in seeds a sign-in-less spoke drops
18
- * (`SPOKE_REMOVAL_SEEDS`, `packages/create-ailk/src/topology.ts` D4), so a
19
- * file OUTSIDE `(auth)` (the account home's `agreements-prompt.tsx`) must
20
- * never import FROM here — doing so once failed the scaffolder's own
21
- * dangling-import check (`spokeExclusions`) for exactly this reason. This
22
- * file is reached only from `sign-up-card.tsx`, its own directory sibling,
23
- * so it carries no such obligation. The two files duplicate the small
24
- * cookie codec (encode here, decode there) rather than share a module,
25
- * precisely so each side's dependencies stay native to where it lives.
26
- *
27
- * Cookie value: `<id>@<version>,…|<issuedAtMs>|<sha256(lowercased email)>|
28
- * <HMAC-SHA256 over the rest>`, keyed by `BETTER_AUTH_SECRET` — the same
29
- * dev fallback `apps/web/auth.ts`'s own `resolveSecret()` publishes when it
30
- * is unset; production refuses to mint without one. That secret is read
31
- * directly HERE rather than importing `resolveSecret` from `auth.ts`: that
32
- * module statically imports the `better-auth` ESM package, which every
33
- * broad page-level test suite that renders `/sign-up` would then have to
34
- * add to its `../../../../auth` mock. `__Host-` prefixed, httpOnly,
35
- * `Secure`, one hour, `SameSite=Lax` — the `__Host-` prefix needs `Secure`,
36
- * so development over plain `http://localhost` uses the unprefixed name
37
- * instead (SEC-8438-5).
38
- */
39
-
40
- import { createHash, createHmac } from "node:crypto";
41
-
42
- import { consentPurposeById, SITE_CONSENT_CONFIG_DEFAULTS } from "@working-theory/validation";
43
- import { cookies } from "next/headers";
44
-
45
- const SECURE_COOKIE_NAME = "__Host-ailk-signup-agreements";
46
- const DEV_COOKIE_NAME = "ailk-signup-agreements";
47
- const MAX_AGE_SECONDS = 60 * 60;
48
-
49
- /** The string `auth.ts`'s own `resolveSecret()` publishes — see file header. */
50
- const DEV_FALLBACK_SECRET =
51
- "ailk-dev-fallback-secret-published-in-this-repo-never-use-in-production";
52
-
53
- /** Mirrors `auth.ts`'s `resolveSecret()` exactly, without importing it. */
54
- function signUpAgreementsSecret(): string {
55
- const configured = process.env.BETTER_AUTH_SECRET;
56
- if (configured) return configured;
57
- const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
58
- if (process.env.NODE_ENV === "production" && !isBuildPhase) {
59
- throw new Error(
60
- "BETTER_AUTH_SECRET is required in production: it signs the carried sign-up-agreement cookie.",
61
- );
62
- }
63
- return DEV_FALLBACK_SECRET;
64
- }
65
-
66
- function cookieName(): string {
67
- return process.env.NODE_ENV === "production" ? SECURE_COOKIE_NAME : DEV_COOKIE_NAME;
68
- }
69
-
70
- function emailHash(email: string): string {
71
- return createHash("sha256").update(email.trim().toLowerCase()).digest("hex");
72
- }
73
-
74
- function sign(payload: string): string {
75
- return createHmac("sha256", signUpAgreementsSecret()).update(payload).digest("hex");
76
- }
77
-
78
- interface CarriedPurpose {
79
- id: string;
80
- version: number;
81
- }
82
-
83
- function encodeCarriedAgreements(
84
- purposes: CarriedPurpose[],
85
- email: string,
86
- issuedAtMs: number,
87
- ): string {
88
- const purposesPart = purposes.map((p) => `${p.id}@${p.version}`).join(",");
89
- const payload = `${purposesPart}|${issuedAtMs}|${emailHash(email)}`;
90
- return `${payload}|${sign(payload)}`;
91
- }
92
-
93
- /**
94
- * Mints the carried-agreement cookie from the ticked `signUp` agreements,
95
- * read back from the registry for their CURRENT version — never trusting a
96
- * version the client might assert. Purposes the registry no longer marks
97
- * `signUp` are dropped. Never throws: a failed mint just means the account
98
- * home's first visit asks explicitly instead (D13's "Otherwise" card).
99
- */
100
- export async function rememberSignUpAgreementsAction(
101
- email: string,
102
- purposeIds: string[],
103
- ): Promise<void> {
104
- try {
105
- const config = SITE_CONSENT_CONFIG_DEFAULTS;
106
- const purposes: CarriedPurpose[] = [];
107
- for (const id of purposeIds) {
108
- const purpose = consentPurposeById(config, id);
109
- if (purpose?.signUp) purposes.push({ id: purpose.id, version: purpose.policyVersion });
110
- }
111
- if (purposes.length === 0) return;
112
-
113
- const value = encodeCarriedAgreements(purposes, email, Date.now());
114
- const cookieStore = await cookies();
115
- cookieStore.set(cookieName(), value, {
116
- httpOnly: true,
117
- secure: process.env.NODE_ENV === "production",
118
- sameSite: "lax",
119
- path: "/",
120
- maxAge: MAX_AGE_SECONDS,
121
- });
122
- } catch {
123
- // Convenience only — see file header.
124
- }
125
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * HTML snapshot regression tests for the downloads route →
3
- * @working-theory/page-renderer renderPage({ bodyMode: "mdx-body" }) migration.
4
- *
5
- * Two-commit strategy:
6
- * Commit 1: snapshots captured against pre-migration route (baseline).
7
- * Commit 2: route migrated; same snapshots verify diff=0.
8
- *
9
- * Routes covered:
10
- * - downloads/page.tsx (empty + populated — 2 snapshots)
11
- *
12
- * Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
13
- */
14
- import React from "react";
15
- import { render } from "@testing-library/react";
16
-
17
- // ── next-mdx-remote/rsc mock ──────────────────────────────────────────────────
18
- jest.mock("next-mdx-remote/rsc", () => ({
19
- MDXRemote: ({ source }: { source: string }): React.ReactElement =>
20
- React.createElement("div", { "data-testid": "mdx-rendered" }, source),
21
- }));
22
-
23
- // ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
24
- jest.mock("@working-theory/ui", () => ({
25
- // #7745: renderPage's mdx-body branch frames its head and body in the
26
- // section Container and renders the body through MDXContent + the shared map.
27
- Container: ({
28
- children,
29
- }: {
30
- children?: React.ReactNode;
31
- }): React.ReactElement => React.createElement("div", null, children),
32
- MDXContent: ({
33
- children,
34
- }: {
35
- children?: React.ReactNode;
36
- }): React.ReactElement => React.createElement(React.Fragment, null, children),
37
- getMDXComponents: (overrides?: Record<string, unknown>) => ({ ...overrides }),
38
- // #4446/#4447: renderPage's own fallback/empty-state title now composes
39
- // Heading (mobile type-budget classes, #4417) instead of a bare <h1>.
40
- Heading: ({
41
- children,
42
- level,
43
- }: {
44
- children: React.ReactNode;
45
- level: number;
46
- }): React.ReactElement => React.createElement(`h${level}`, null, children),
47
- SectionRenderer: ({ sections }: { sections: unknown[] }) =>
48
- React.createElement("div", {
49
- "data-testid": "section-renderer",
50
- "data-section-count": String(sections.length),
51
- }),
52
- }));
53
-
54
- import DownloadsPage from "../page";
55
-
56
- const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
57
-
58
- // ── DownloadsPage ─────────────────────────────────────────────────────────────
59
-
60
- describe("DownloadsPage — snapshot regression", () => {
61
- it("empty state renders deterministic HTML", async () => {
62
- const element = await DownloadsPage({
63
- params: Promise.resolve({ locale: "en" }),
64
- getMdxSource: getMdxSourceEmpty,
65
- });
66
- const { container } = render(element);
67
- expect(container.innerHTML).toMatchInlineSnapshot(
68
- `"<main id="main"><h1>Downloads</h1><p data-testid="section-empty-state">Downloads listing coming soon.</p></main>"`,
69
- );
70
- });
71
-
72
- it("populated state renders deterministic HTML", async () => {
73
- const getMdxSourcePopulated = jest.fn().mockResolvedValue({
74
- frontmatter: {
75
- type: "DownloadsList",
76
- slug: "downloads",
77
- title: "Downloads",
78
- description: "Downloadable assets and templates.",
79
- },
80
- body: "# Downloads\n\nBody.",
81
- });
82
-
83
- const element = await DownloadsPage({
84
- params: Promise.resolve({ locale: "en" }),
85
- getMdxSource: getMdxSourcePopulated,
86
- });
87
- const { container } = render(element);
88
- expect(container.innerHTML).toMatchInlineSnapshot(`
89
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Downloads","description":"Downloadable assets and templates.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"DigitalDocument"}]}}</script><main id="main"><div><h1>Downloads</h1><p data-testid="downloads-description" class="mt-block-tight text-lead">Downloadable assets and templates.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">
90
- Body.</div></article></div></div></div></main>"
91
- `);
92
- });
93
- });
@@ -1,37 +0,0 @@
1
- // Downloads list page. Reads DownloadsList MDX via renderPage and emits
2
- // a JSON-LD CollectionPage via the dispatcher. List-only — no detail route.
3
- import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
- import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
5
- import type { Metadata } from "next";
6
- import type { ReactElement } from "react";
7
-
8
- interface Props {
9
- params: Promise<{ locale: string }>;
10
- getMdxSource?: typeof defaultGetMdxSource;
11
- }
12
-
13
- export async function generateMetadata({
14
- params,
15
- }: Pick<Props, "params">): Promise<Metadata> {
16
- const { locale } = await params;
17
- return renderPageMetadata({
18
- type: "DownloadsList",
19
- slug: "downloads",
20
- locale,
21
- });
22
- }
23
-
24
- export default async function Page({
25
- params,
26
- getMdxSource,
27
- }: Props): Promise<ReactElement> {
28
- const { locale } = await params;
29
- return renderPage({
30
- type: "DownloadsList",
31
- slug: "downloads",
32
- locale,
33
- translationsNamespace: "downloads",
34
- bodyMode: "mdx-body",
35
- ...(getMdxSource !== undefined && { getMdxSource }),
36
- });
37
- }
@@ -1,139 +0,0 @@
1
- /**
2
- * subscribeNewsletterAction (#6730 D9) — the Server Action twin of the
3
- * `ailkSubscribeNewsletter` MCP tool.
4
- */
5
-
6
- import { subscribeNewsletterAction } from "../newsletter.actions.js";
7
-
8
- // `visitorForwardHeaders` stays real (#7394) so AC-9 compares the action's
9
- // `init` with the helper's own output.
10
- jest.mock("@working-theory/api-client", () => ({
11
- leads: { subscribe: jest.fn() },
12
- visitorForwardHeaders: jest.requireActual("@working-theory/api-client")
13
- .visitorForwardHeaders,
14
- }));
15
-
16
- // next/headers' `headers()` is request-scoped in Next; outside it, it is
17
- // mocked here the way flow.actions.test.ts does (#7394 AC-9).
18
- jest.mock("next/headers", () => ({
19
- __esModule: true,
20
- headers: jest.fn().mockResolvedValue({ get: () => null }),
21
- }));
22
-
23
- import { leads, visitorForwardHeaders } from "@working-theory/api-client";
24
- import { headers } from "next/headers";
25
- const mockSubscribe = leads.subscribe as jest.Mock;
26
- const mockHeaders = headers as jest.Mock;
27
-
28
- function formWith(email: string): FormData {
29
- const fd = new FormData();
30
- fd.append("email", email);
31
- fd.append("consent", "true");
32
- return fd;
33
- }
34
-
35
- afterEach(() => {
36
- jest.clearAllMocks();
37
- mockHeaders.mockResolvedValue({ get: () => null });
38
- });
39
-
40
- describe("subscribeNewsletterAction", () => {
41
- it("calls leads.subscribe with source newsletter and the address", async () => {
42
- mockSubscribe.mockResolvedValueOnce({ ok: true, body: { id: "subscr_1", persisted: "db" } });
43
-
44
- const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
45
-
46
- expect(result).toEqual({ kind: "success" });
47
- expect(mockSubscribe).toHaveBeenCalledWith(
48
- expect.objectContaining({ source: "newsletter", email: "ada@example.com" }),
49
- );
50
- });
51
-
52
- it("an invalid address → a field error, no API call", async () => {
53
- const result = await subscribeNewsletterAction(null, formWith("not-an-email"));
54
-
55
- expect(result).toMatchObject({ kind: "error", fieldErrors: { email: expect.any(String) } });
56
- expect(mockSubscribe).not.toHaveBeenCalled();
57
- });
58
-
59
- it("503 lead_capture_not_configured → unavailable", async () => {
60
- mockSubscribe.mockResolvedValueOnce({
61
- ok: false,
62
- status: 503,
63
- body: { error: "lead_capture_not_configured" },
64
- });
65
-
66
- const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
67
-
68
- expect(result).toEqual({ kind: "unavailable" });
69
- });
70
-
71
- it("any other failure → the generic error, never success", async () => {
72
- mockSubscribe.mockResolvedValueOnce({ ok: false, status: 503, body: { error: "persist_failed" } });
73
- expect(await subscribeNewsletterAction(null, formWith("a@b.co"))).toMatchObject({
74
- kind: "error",
75
- });
76
-
77
- mockSubscribe.mockRejectedValueOnce(new Error("ECONNREFUSED"));
78
- jest.spyOn(console, "error").mockImplementationOnce(() => undefined);
79
- expect(await subscribeNewsletterAction(null, formWith("a@b.co"))).toMatchObject({
80
- kind: "error",
81
- });
82
- });
83
-
84
- it("a filled honeypot → success, and nothing is sent", async () => {
85
- const fd = formWith("ada@example.com");
86
- fd.append("ailk_hp", "spam");
87
-
88
- expect(await subscribeNewsletterAction(null, fd)).toEqual({ kind: "success" });
89
- expect(mockSubscribe).not.toHaveBeenCalled();
90
- });
91
- });
92
-
93
- // #7394 AC-9 — a spoke forwards its visitor to the hub's leads rate limit.
94
- describe("subscribeNewsletterAction — visitor forwarding (#7394)", () => {
95
- const FORWARD_KEY = "0123456789abcdef".repeat(4); // gitleaks:allow — test fixture, not a credential (#7394)
96
- const ENV_KEYS = ["AILK_VISITOR_FORWARD_KEY", "AILK_API_URL"] as const;
97
- const priorEnv = new Map(ENV_KEYS.map((name) => [name, process.env[name]]));
98
-
99
- beforeEach(() => {
100
- process.env.AILK_VISITOR_FORWARD_KEY = FORWARD_KEY;
101
- process.env.AILK_API_URL = "https://hub.example";
102
- });
103
-
104
- afterAll(() => {
105
- for (const [name, value] of priorEnv) {
106
- if (value === undefined) delete process.env[name];
107
- else process.env[name] = value;
108
- }
109
- });
110
-
111
- it("passes the helper's two headers as init", async () => {
112
- const incoming = {
113
- get: (name: string) => (name === "x-forwarded-for" ? "203.0.113.5" : null),
114
- };
115
- mockHeaders.mockResolvedValue(incoming);
116
- mockSubscribe.mockResolvedValueOnce({ ok: true, body: { id: "subscr_1", persisted: "db" } });
117
-
118
- const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
119
-
120
- expect(result).toEqual({ kind: "success" });
121
- const expected = visitorForwardHeaders(incoming, FORWARD_KEY);
122
- expect(expected).toEqual({
123
- "x-ailk-visitor-ip": "203.0.113.5",
124
- "x-ailk-visitor-forward-key": FORWARD_KEY,
125
- });
126
- expect(mockSubscribe).toHaveBeenCalledWith(expect.any(Object), { headers: expected });
127
- });
128
-
129
- it("calls leads.subscribe with one argument when headers() throws, and still succeeds", async () => {
130
- mockHeaders.mockRejectedValue(new Error("outside a request scope"));
131
- mockSubscribe.mockResolvedValueOnce({ ok: true, body: { id: "subscr_1", persisted: "db" } });
132
-
133
- const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
134
-
135
- expect(result).toEqual({ kind: "success" });
136
- expect(mockSubscribe).toHaveBeenCalledTimes(1);
137
- expect(mockSubscribe.mock.calls[0]).toHaveLength(1);
138
- });
139
- });