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,6 +1,6 @@
1
1
  /**
2
2
  * HubSignInGate — the signed-out account gate of a spoke that signs in
3
- * through its hub (#7894 D10).
3
+ * through its hub (#7894 D10; #8790 AC-1/AC-2).
4
4
  *
5
5
  * A Server Component, rendered by the `(authed)` layout through the generated
6
6
  * gate seam (`signed-out-gate.generated.tsx`, its `signIn: "hub"` render). It
@@ -9,16 +9,26 @@
9
9
  * link is a plain anchor (the `Link` primitive), so nothing prefetches the
10
10
  * route and starts a flow the visitor did not ask for.
11
11
  *
12
+ * #8790 — the gate reads as one of the site's inner pages, not a bare
13
+ * full-height stop: the site's inner-page header (the back arrow at the start
14
+ * edge, the mark centred — the shape `Nav bar="bare"` draws, composed here
15
+ * from the same `NavBackLink` and `siteLogo()`), a heading at the label size
16
+ * (`heading-2`), and an optional quiet prospect line a site turns on by
17
+ * adding `account.gate.prospectPrompt` + `prospectLink` + `prospectHref`
18
+ * to its messages — absent, the shipped default, renders nothing.
19
+ * The document title is the account page's job (`generateMetadata` there
20
+ * reads the session and names this state "Sign in · <site>").
21
+ *
12
22
  * Not configured (`hubSessionConfig()` is `null`: a client id, secret or
13
23
  * session secret missing), it renders the account area's existing "sign-in
14
24
  * isn't available yet" state, whose one action leads back to the site.
15
25
  */
16
-
17
26
  import { localeHref } from "@working-theory/ui";
27
+ import { NavBackLink } from "@working-theory/ui/chrome";
18
28
  import { Button, Heading, Link, Text } from "@working-theory/ui/primitives";
19
29
  import { getTranslations } from "next-intl/server";
20
30
  import type { ReactElement } from "react";
21
-
31
+ import { siteLogo } from "../site-logo";
22
32
  import { hubSessionConfig } from "./config";
23
33
  import { HUB_SESSION_FALLBACK_PATH } from "./session";
24
34
 
@@ -40,40 +50,86 @@ export async function HubSignInGate({
40
50
  const next =
41
51
  requestHeaders.get("x-pathname") ??
42
52
  localeHref(locale, HUB_SESSION_FALLBACK_PATH);
53
+ const home = localeHref(locale, "/");
54
+ // The three prospect keys are optional extensions of the typed catalog (the
55
+ // shipped en.json carries none — every leaf must be a non-empty string), so
56
+ // presence is the switch; hence the cast on `has`.
57
+ type Key = Parameters<typeof t.has>[0];
58
+ const has = (key: string): boolean => t.has(key as Key);
59
+ const prospect =
60
+ has("prospectPrompt") && has("prospectLink") && has("prospectHref")
61
+ ? {
62
+ prompt: t("prospectPrompt" as Key),
63
+ linkLabel: t("prospectLink" as Key),
64
+ href: t("prospectHref" as Key),
65
+ }
66
+ : null;
43
67
 
44
68
  return (
45
69
  <div
46
- className="flex min-h-screen flex-col items-center justify-center gap-4 py-section text-center"
70
+ className="flex min-h-screen flex-col"
47
71
  data-testid="account-sign-in-gate"
48
72
  >
49
- <Heading level={1} visual={4}>
50
- {configured ? t("hubHeadline") : t("emptyHeadline")}
51
- </Heading>
52
- <Text variant="body-sm" tone="muted" className="max-w-xs">
53
- {configured ? t("hubDescription") : t("emptyDescription")}
54
- </Text>
55
- {configured ? (
56
- // The primary action: 44px on a phone, 36px from md up.
57
- <Button asChild className="max-md:min-h-11">
58
- <Link
59
- href={`${HUB_SIGN_IN_ROUTE}?next=${encodeURIComponent(next)}`}
60
- variant="bare"
61
- data-testid="account-hub-sign-in"
62
- >
63
- {t("hubSignIn")}
64
- </Link>
65
- </Button>
66
- ) : (
67
- <Button asChild variant="secondary" className="max-md:min-h-11">
68
- <Link
69
- href={localeHref(locale, "/")}
70
- variant="bare"
71
- data-testid="account-sign-in-gate-home"
72
- >
73
- {t("homeLink")}
74
- </Link>
75
- </Button>
76
- )}
73
+ {/* The site's inner-page header: back arrow at the start, the mark centred (#8790 AC-1). */}
74
+ <header
75
+ className="grid h-16 grid-cols-[1fr_auto_1fr] items-center px-5 md:px-8"
76
+ data-testid="account-sign-in-gate-header"
77
+ >
78
+ <div className="justify-self-start">
79
+ <NavBackLink href={home} label={t("homeLink")} />
80
+ </div>
81
+ <Link
82
+ href={home}
83
+ variant="unstyled"
84
+ className="inline-flex h-7 items-center [&>img]:h-full [&>img]:w-auto [&>img]:object-contain"
85
+ data-testid="account-sign-in-gate-mark"
86
+ >
87
+ {siteLogo()}
88
+ </Link>
89
+ <div />
90
+ </header>
91
+ <div className="flex flex-1 flex-col items-center justify-center gap-4 py-section text-center">
92
+ <Heading level={1} visual={2}>
93
+ {configured ? t("hubHeadline") : t("emptyHeadline")}
94
+ </Heading>
95
+ <Text variant="body-sm" tone="muted" className="max-w-xs">
96
+ {configured ? t("hubDescription") : t("emptyDescription")}
97
+ </Text>
98
+ {configured ? (
99
+ // The primary action: 44px on a phone, 36px from md up.
100
+ <Button asChild className="max-md:min-h-11">
101
+ <Link
102
+ href={`${HUB_SIGN_IN_ROUTE}?next=${encodeURIComponent(next)}`}
103
+ variant="bare"
104
+ data-testid="account-hub-sign-in"
105
+ >
106
+ {t("hubSignIn")}
107
+ </Link>
108
+ </Button>
109
+ ) : (
110
+ <Button asChild variant="secondary" className="max-md:min-h-11">
111
+ <Link
112
+ href={home}
113
+ variant="bare"
114
+ data-testid="account-sign-in-gate-home"
115
+ >
116
+ {t("homeLink")}
117
+ </Link>
118
+ </Button>
119
+ )}
120
+ {configured && prospect ? (
121
+ // A wrapping div carries the hook: `Text` forwards no data-* attributes.
122
+ <div data-testid="account-sign-in-gate-prospect">
123
+ <Text as="p" variant="body-sm" tone="muted">
124
+ {prospect.prompt}{" "}
125
+ <Link href={prospect.href} variant="inline">
126
+ {prospect.linkLabel}
127
+ </Link>
128
+ .
129
+ </Text>
130
+ </div>
131
+ ) : null}
132
+ </div>
77
133
  </div>
78
134
  );
79
135
  }
@@ -0,0 +1,416 @@
1
+ /**
2
+ * in-page-magnet.ts — the in-page form shape (#8847 D6, D15).
3
+ *
4
+ * A contact, quote or newsletter magnet renders from its hub record inside a
5
+ * chrome-on page: the page holds layout only, and every visible fact on the
6
+ * form (heading, sentence, fields, consent, button, ending) comes from the
7
+ * magnet's served config. /contact, /newsletter, /unsubscribe and the
8
+ * footer signup all read through here.
9
+ *
10
+ * - `sharedHubRead` shares a hub read across requests for HUB_READ_TTL_MS
11
+ * (#8731, moved here from /contact): a visitor view carries no forwarding
12
+ * headers, so on a spoke every view would otherwise count against the
13
+ * spoke server's one per-IP limit at the hub. An Admin edit shows within
14
+ * that window.
15
+ * - `readServedMagnet(slug)` is the public flow read; any throw or non-2xx
16
+ * (absent, draft, a Free build with no flow route) is `null`.
17
+ * - `inPageView(config, locale, form)` maps a config's one field step. The
18
+ * form kind comes from the CALLER, never the config or the slug (C1): a
19
+ * served config carries no `kind`.
20
+ * - `framedSources` makes the page's first `contact-form` section carry the
21
+ * magnet's title and body, and — when that section leads the page — the
22
+ * frontmatter title too, so `renderPage`'s title carve-out gives the
23
+ * section the `<h1>` and `renderPageMetadata` gives `<title>` the same text.
24
+ * - `renderInPageMagnet` is the slotted surface for a slug (D15): the form
25
+ * bound to its action, or `ContactFallback` when capture is off.
26
+ *
27
+ * Pure apart from the two reads; no React state.
28
+ */
29
+ import { flows, leads } from "@working-theory/api-client";
30
+ import type {
31
+ getMdxSource as defaultGetMdxSource,
32
+ getPageSections as defaultGetPageSections,
33
+ } from "@working-theory/page-renderer";
34
+ import {
35
+ ContactFallback,
36
+ LeadCaptureForm,
37
+ type LeadCaptureFormText,
38
+ localeHref,
39
+ NewsletterFormInline,
40
+ } from "@working-theory/ui";
41
+ import {
42
+ flowLeadSource,
43
+ flowSitePathSchema,
44
+ resolveLocalizedString,
45
+ type FlowConfig,
46
+ } from "@working-theory/validation";
47
+ import { createElement, type ReactElement } from "react";
48
+
49
+ import {
50
+ contactFormFromConfig,
51
+ type ContactFormView,
52
+ } from "../app/[locale]/contact/contact-form-magnet";
53
+ import { captureLeadAction } from "../app/[locale]/contact/contact.actions";
54
+
55
+ import { consentPoliciesFor } from "./consent-policies";
56
+ import { subscribeNewsletterAction } from "./newsletter.actions";
57
+
58
+
59
+ export { fillEndingName } from "@working-theory/validation";
60
+
61
+ // ─── Shared hub reads (#8731) ──────────────────────────────────────────────────
62
+
63
+ /**
64
+ * How long a hub read is shared across requests (#8731). Short, so an Admin
65
+ * edit to the magnet shows within seconds; long enough that a burst of views
66
+ * makes one read of each, not one per view.
67
+ */
68
+ export const HUB_READ_TTL_MS = 15_000;
69
+
70
+ const hubReads = new Map<string, { at: number; read: Promise<unknown> }>();
71
+
72
+ /**
73
+ * How long a page waits on one hub read before treating it as failed. The
74
+ * root layout reads the newsletter magnet for the footer on every page, so a
75
+ * hub that accepts the connection and stalls must not hold the site.
76
+ */
77
+ export const HUB_READ_DEADLINE_MS = 2_000;
78
+
79
+ /** `read`'s result, or `fallback` once HUB_READ_DEADLINE_MS has passed. */
80
+ function withDeadline<T>(read: Promise<T>, fallback: T): Promise<T> {
81
+ let timer: ReturnType<typeof setTimeout> | undefined;
82
+ const late = new Promise<T>((resolve) => {
83
+ timer = setTimeout(() => resolve(fallback), HUB_READ_DEADLINE_MS);
84
+ });
85
+ return Promise.race([read, late]).finally(() => clearTimeout(timer));
86
+ }
87
+
88
+ /**
89
+ * `read()`'s result, shared per instance for HUB_READ_TTL_MS. Concurrent calls
90
+ * share one in-flight read. A read that settles to `null` (any failed read,
91
+ * see the readers below) is shared the same way, so a burst against an
92
+ * unreachable hub is not a burst of retries.
93
+ */
94
+ export function sharedHubRead<T>(key: string, read: () => Promise<T>): Promise<T> {
95
+ const now = Date.now();
96
+ const hit = hubReads.get(key);
97
+ if (hit && now - hit.at < HUB_READ_TTL_MS) return hit.read as Promise<T>;
98
+ const fresh = read();
99
+ hubReads.set(key, { at: now, read: fresh });
100
+ return fresh;
101
+ }
102
+
103
+ export type CaptureStatus = Extract<
104
+ Awaited<ReturnType<typeof leads.captureStatus>>,
105
+ { ok: true }
106
+ >["body"]["data"];
107
+
108
+ /** The API's capture status, or `null` for any failed read. */
109
+ export function readCaptureStatus(): Promise<CaptureStatus | null> {
110
+ return sharedHubRead("capture-status", () =>
111
+ withDeadline(
112
+ (async () => {
113
+ try {
114
+ const res = await leads.captureStatus();
115
+ return res.ok && res.body.ok === true ? res.body.data : null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ })(),
120
+ null,
121
+ ),
122
+ );
123
+ }
124
+
125
+ /**
126
+ * The served magnet's config at `slug`, or `null` for any failed read: a
127
+ * non-2xx (absent, draft, a build with no flow route), a throw, or no answer
128
+ * within HUB_READ_DEADLINE_MS. The
129
+ * property access sits inside the `try`, so a client with no `flows` is a
130
+ * failed read too.
131
+ */
132
+ export function readServedMagnet(slug: string): Promise<FlowConfig | null> {
133
+ return sharedHubRead(`magnet:${slug}`, () =>
134
+ withDeadline(
135
+ (async () => {
136
+ try {
137
+ const res = await flows.get(slug);
138
+ return res.ok ? res.body : null;
139
+ } catch {
140
+ return null;
141
+ }
142
+ })(),
143
+ null,
144
+ ),
145
+ );
146
+ }
147
+
148
+ // ─── The view ──────────────────────────────────────────────────────────────────
149
+
150
+ /** Which form a caller asks for (C1). */
151
+ export type InPageForm = "contact" | "newsletter";
152
+
153
+ /** The magnet's `ending.default`, resolved and localized (#8841 D1 shape). */
154
+ export type InPageEnding =
155
+ | {
156
+ kind: "screen";
157
+ headline: string;
158
+ body?: string;
159
+ ctas?: { label: string; href: string }[];
160
+ }
161
+ | { kind: "redirect"; href: string };
162
+
163
+ /** A newsletter magnet's one step, as `NewsletterFormInline` renders it. */
164
+ export interface NewsletterFormView {
165
+ kind: "newsletter";
166
+ /** The `email` field's name — the key the step submit sends the address under. */
167
+ emailField: string;
168
+ label: string;
169
+ placeholder?: string;
170
+ /** The step's first consent field: its name, purposes, statement and help line. */
171
+ consent?: { field: string; purposes: string[]; statement: string; help?: string };
172
+ submitLabel?: string;
173
+ }
174
+
175
+ export interface InPageView {
176
+ stepKey: string;
177
+ title: string;
178
+ body?: string;
179
+ /** Absent when the config has no `ending` and the caller gave no fallback line. */
180
+ ending?: InPageEnding;
181
+ form: ContactFormView | NewsletterFormView;
182
+ }
183
+
184
+ type FieldStep = Extract<FlowConfig["steps"][number], { type: "field" }>;
185
+
186
+ /** The field types a visitor types or picks a value into. */
187
+ const ENTRY_TYPES = new Set(["text", "email", "tel", "url", "textarea", "select", "address"]);
188
+
189
+ function newsletterForm(
190
+ step: FieldStep,
191
+ resolve: (value: Record<string, string>) => string,
192
+ ): NewsletterFormView | null {
193
+ const email = step.fields.find((f) => f.inputType === "email" && f.mapsTo === "email");
194
+ if (email === undefined) return null;
195
+ // Any other field a visitor fills in is not a one-field signup.
196
+ if (step.fields.some((f) => f !== email && ENTRY_TYPES.has(f.inputType))) return null;
197
+ const consent = step.fields.find((f) => f.inputType === "consent");
198
+ return {
199
+ kind: "newsletter",
200
+ emailField: email.name,
201
+ label: resolve(email.copy.label),
202
+ ...(email.copy.placeholder !== undefined && { placeholder: resolve(email.copy.placeholder) }),
203
+ ...(consent !== undefined && {
204
+ consent: {
205
+ field: consent.name,
206
+ purposes: consent.purposes ?? [],
207
+ statement: resolve(consent.copy.label),
208
+ ...(consent.copy.help !== undefined && { help: resolve(consent.copy.help) }),
209
+ },
210
+ }),
211
+ ...(step.copy.cta !== undefined && { submitLabel: resolve(step.copy.cta) }),
212
+ };
213
+ }
214
+
215
+ /**
216
+ * A config's one field step as an in-page form of the kind the caller names,
217
+ * or `null` when it is not one: its first step is not a `field` step, a
218
+ * newsletter config has more than one step or a field other than its email
219
+ * and consent, or a contact config lacks `name` and `email`. A config with no
220
+ * `ending` ends on `fallbackHeadline` (the page's catalog success line), or
221
+ * has no `ending` when the caller gives none, so the form keeps its own.
222
+ */
223
+ export function inPageView(
224
+ config: FlowConfig,
225
+ locale: string,
226
+ form: InPageForm,
227
+ fallbackHeadline?: string,
228
+ ): InPageView | null {
229
+ const step = config.steps[0];
230
+ if (step?.type !== "field") return null;
231
+ const resolve = (value: Record<string, string>): string =>
232
+ resolveLocalizedString(value, locale, config.defaultLocale);
233
+
234
+ let view: ContactFormView | NewsletterFormView | null;
235
+ if (form === "newsletter") {
236
+ view = config.steps.length === 1 ? newsletterForm(step, resolve) : null;
237
+ } else {
238
+ view = contactFormFromConfig(config, locale);
239
+ }
240
+ if (view === null) return null;
241
+
242
+ // Every destination is re-checked against the site-path grammar before it
243
+ // is rendered or navigated to, as the walk does (#8840): a stored row that
244
+ // bypassed the write-side check never becomes an off-site link.
245
+ const onSite = (href: string): boolean => flowSitePathSchema.safeParse(href).success;
246
+ const outcome = config.ending?.default;
247
+ const fallback: InPageEnding | undefined =
248
+ fallbackHeadline === undefined ? undefined : { kind: "screen", headline: fallbackHeadline };
249
+ let ending: InPageEnding | undefined;
250
+ if (outcome === undefined) {
251
+ ending = fallback;
252
+ } else if (outcome.kind === "redirect") {
253
+ ending = onSite(outcome.href) ? { kind: "redirect", href: localeHref(locale, outcome.href) } : fallback;
254
+ } else {
255
+ const ctas = (outcome.ctas ?? []).filter((cta) => onSite(cta.href));
256
+ ending = {
257
+ kind: "screen",
258
+ headline: resolve(outcome.headline),
259
+ ...(outcome.body !== undefined && { body: resolve(outcome.body) }),
260
+ ...(ctas.length > 0 && {
261
+ ctas: ctas.map((cta) => ({
262
+ label: resolve(cta.label),
263
+ href: localeHref(locale, cta.href),
264
+ })),
265
+ }),
266
+ };
267
+ }
268
+
269
+ return {
270
+ stepKey: step.key,
271
+ title: resolve(step.copy.title),
272
+ ...(step.copy.body !== undefined && { body: resolve(step.copy.body) }),
273
+ ...(ending !== undefined && { ending }),
274
+ form: view,
275
+ };
276
+ }
277
+
278
+ // ─── The page frame ────────────────────────────────────────────────────────────
279
+
280
+ type Loaders = {
281
+ getMdxSource: typeof defaultGetMdxSource;
282
+ getPageSections: typeof defaultGetPageSections;
283
+ };
284
+ type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
285
+
286
+ /**
287
+ * The page's first `contact-form` section takes `headline = view.title` and
288
+ * `description = view.body` (deleted when there is none). When that section
289
+ * is the first non-alert section, the frontmatter `title` is `view.title`
290
+ * too. With no `contact-form` section, both loaders pass through unchanged.
291
+ */
292
+ export function framedSources({
293
+ getMdxSource,
294
+ getPageSections,
295
+ view,
296
+ }: Loaders & { view: Pick<InPageView, "title" | "body"> }): Loaders {
297
+ const framedSections = async (frontmatter: unknown): Promise<Section[]> => {
298
+ const sections = await getPageSections(frontmatter);
299
+ const at = sections.findIndex((s) => s.type === "contact-form");
300
+ if (at < 0) return sections;
301
+ return sections.map((section, i) => {
302
+ if (i !== at || section.type !== "contact-form") return section;
303
+ const data = { ...section.data, headline: view.title };
304
+ if (view.body !== undefined) data.description = view.body;
305
+ else delete data.description;
306
+ return { ...section, data };
307
+ });
308
+ };
309
+ const framedSource: typeof defaultGetMdxSource = async (type, slug, locale) => {
310
+ const source = await getMdxSource(type, slug, locale);
311
+ if (source === null) return source;
312
+ const sections = await getPageSections(source.frontmatter);
313
+ const leading = sections.find((s) => s.type !== "alert");
314
+ if (leading?.type !== "contact-form") return source;
315
+ return { ...source, frontmatter: { ...source.frontmatter, title: view.title } };
316
+ };
317
+ return { getMdxSource: framedSource, getPageSections: framedSections };
318
+ }
319
+
320
+ // ─── The slotted surface (D15) ─────────────────────────────────────────────────
321
+
322
+ export interface NewsletterSignupOptions {
323
+ locale: string;
324
+ fallbackEmail?: string;
325
+ /** The footer's compact form: stacked, the label `sr-only`, the placeholder naming the field. */
326
+ compact?: boolean;
327
+ className?: string;
328
+ }
329
+
330
+ /**
331
+ * The newsletter magnet's signup: the standard `NewsletterFormInline` bound to
332
+ * `subscribeNewsletterAction`, every word from the view. On /newsletter the
333
+ * email label is visible and the field and Subscribe share a row from `md`;
334
+ * the footer's compact form stacks, with the label `sr-only`.
335
+ */
336
+ export function newsletterSignup(
337
+ view: InPageView & { form: NewsletterFormView },
338
+ { locale, fallbackEmail, compact = false, className }: NewsletterSignupOptions,
339
+ ): ReactElement {
340
+ const { form, ending } = view;
341
+ const placeholder = form.placeholder ?? (compact ? form.label : undefined);
342
+ return createElement(NewsletterFormInline, {
343
+ formAction: subscribeNewsletterAction,
344
+ emailLabel: form.label,
345
+ ...(compact ? { layout: "stacked" as const } : { label: form.label }),
346
+ ...(placeholder !== undefined && { placeholder }),
347
+ ...(form.consent !== undefined && {
348
+ consentStatement: form.consent.statement,
349
+ policies: consentPoliciesFor(locale),
350
+ ...(form.consent.help !== undefined && { helpText: form.consent.help }),
351
+ }),
352
+ ...(form.submitLabel !== undefined && { submitLabel: form.submitLabel }),
353
+ // A redirect ending has no in-place form; the block's own success line stands.
354
+ ...(ending?.kind === "screen" && {
355
+ ending: { headline: ending.headline, ...(ending.body !== undefined && { body: ending.body }) },
356
+ }),
357
+ ...(fallbackEmail !== undefined && { fallbackEmail }),
358
+ ...(className !== undefined && { className }),
359
+ });
360
+ }
361
+
362
+ export interface RenderInPageMagnetInput {
363
+ slug: string;
364
+ locale: string;
365
+ /** The page's view of the magnet (`inPageView`), mapped for the caller's form. */
366
+ view: InPageView;
367
+ /** The `contact` catalog's form copy (`contactFormText`). */
368
+ text: LeadCaptureFormText;
369
+ accepting: boolean;
370
+ fallbackEmail?: string;
371
+ }
372
+
373
+ /**
374
+ * The surface a page slots for an in-page magnet: the form bound to its
375
+ * action, or `ContactFallback` when capture is off. A `contact` form posts
376
+ * through `captureLeadAction` stamped `flowLeadSource(slug)`; the action keeps
377
+ * only the contact sources (`CONTACT_LEAD_SOURCES`), so a quote magnet's post
378
+ * lands as `contact-form` until that list admits its slug. A `newsletter`
379
+ * form posts through `subscribeNewsletterAction`.
380
+ */
381
+ export function renderInPageMagnet({
382
+ slug,
383
+ locale,
384
+ view,
385
+ text,
386
+ accepting,
387
+ fallbackEmail,
388
+ }: RenderInPageMagnetInput): ReactElement {
389
+ if (!accepting) {
390
+ return createElement(ContactFallback, {
391
+ email: fallbackEmail,
392
+ text: {
393
+ heading: text.unavailableHeading,
394
+ body: text.unavailableBody,
395
+ noEmail: text.unavailableNoEmail,
396
+ },
397
+ });
398
+ }
399
+ const form = view.form;
400
+ if ("kind" in form) {
401
+ return newsletterSignup({ ...view, form }, { locale, ...(fallbackEmail !== undefined && { fallbackEmail }) });
402
+ }
403
+ return createElement(LeadCaptureForm, {
404
+ action: captureLeadAction,
405
+ text: form.consentStatement ? { ...text, consentStatement: form.consentStatement } : text,
406
+ termsHref: localeHref(locale, "/terms"),
407
+ policies: consentPoliciesFor(locale),
408
+ fallbackEmail,
409
+ fields: form.fields,
410
+ submitLabel: form.submitLabel,
411
+ ending: view.ending,
412
+ source: flowLeadSource(slug),
413
+ look: "column",
414
+ consentGate: "on-submit",
415
+ });
416
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The magnet walk's path predicate (#8840 D10, spec
3
+ * docs/specs/8840-a-default-full-screen-magnet-spec.md).
4
+ *
5
+ * A magnet's walk renders full-screen: `app/[locale]/layout.tsx` drops the
6
+ * Nav and the Footer for it, as it does for the auth and account paths, and
7
+ * keeps the cookie consent banner (a visitor can arrive straight at a walk).
8
+ * A walk is `/<magnet>/start` — any path whose last segment is `start` — or
9
+ * one of the two careers apply walks, `/careers/apply` and
10
+ * `/careers/<posting>/apply`, each bare or under an enabled-locale prefix
11
+ * (the `auth-routes.ts` rule). #8843's meeting pages add their paths here
12
+ * rather than writing a second predicate. #8848 D9 adds every page under
13
+ * `/reports/<slug>` — a gated landing, an ungated reader and the grant
14
+ * reader alike, since the path cannot tell them apart; `/reports` itself
15
+ * keeps its chrome.
16
+ *
17
+ * Fail-safe direction, as `isAuthPath()`: an absent or empty pathname
18
+ * answers `false`, so a missing `x-pathname` never strips a page's chrome.
19
+ *
20
+ * Core, not a module slice: the layout every scaffold carries imports it, and
21
+ * it names no paid code. No Next.js import — a pure function.
22
+ */
23
+ export function isMagnetWalkPath(
24
+ pathname: string | undefined | null,
25
+ locales: readonly string[],
26
+ ): boolean {
27
+ if (!pathname) return false;
28
+ const path = pathname.split(/[?#]/)[0] ?? "";
29
+ const bare =
30
+ locales.length > 0
31
+ ? path.replace(new RegExp(`^/(${locales.join("|")})(?=/|$)`), "")
32
+ : path;
33
+ const segments = bare.split("/").filter((segment) => segment.length > 0);
34
+ if (segments.length === 0) return false;
35
+ if (segments[segments.length - 1] === "start") return true;
36
+ if (segments[0] === "reports" && segments.length === 2) return true;
37
+ return (
38
+ segments[0] === "careers" &&
39
+ segments[segments.length - 1] === "apply" &&
40
+ (segments.length === 2 || segments.length === 3)
41
+ );
42
+ }
@@ -0,0 +1,9 @@
1
+ // modules.generated.ts — which module pages this site carries.
2
+ //
3
+ // GENERATED by create-ailk from this app's package.json dependencies (#8843).
4
+ // Do not edit: every scaffold, `create-ailk add-module` and
5
+ // `create-ailk update` rewrites it. It imports nothing, so edge code may
6
+ // read it as well as the locale layout.
7
+
8
+ /** The scheduling module's booking pages (/meet, /private) exist. */
9
+ export const SCHEDULING_PAGES: boolean = false;