create-ailk 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/component-catalog.json +186 -8
  2. package/component-catalog.md +9 -7
  3. package/dist/add-module.js +20 -14
  4. package/dist/add-route.d.ts +55 -1
  5. package/dist/add-route.js +294 -12
  6. package/dist/cli.js +27 -10
  7. package/dist/derive-architecture.d.ts +12 -3
  8. package/dist/derive-architecture.js +20 -8
  9. package/dist/design-package.js +7 -2
  10. package/dist/lib/design-checks.js +4 -2
  11. package/dist/lib/env-catalog.js +5 -5
  12. package/dist/lib/npm-auth.d.ts +9 -0
  13. package/dist/lib/npm-auth.js +48 -0
  14. package/dist/lib/scaffold-render-sweep.d.ts +3 -2
  15. package/dist/lib/scaffold-render-sweep.js +5 -8
  16. package/dist/module-architecture.js +99 -9
  17. package/dist/paid-sections.d.ts +21 -2
  18. package/dist/paid-sections.js +69 -7
  19. package/dist/programmatic.js +19 -5
  20. package/dist/seed-flows.d.ts +4 -1
  21. package/dist/seed-flows.js +50 -4
  22. package/dist/site-chrome.d.ts +8 -0
  23. package/dist/site-chrome.js +17 -1
  24. package/dist/spoke-checkout.d.ts +65 -41
  25. package/dist/spoke-checkout.js +208 -430
  26. package/dist/starter-grid.d.ts +0 -3
  27. package/dist/starter-grid.js +24 -14
  28. package/dist/surfaces.js +52 -26
  29. package/dist/sync-routes.d.ts +38 -1
  30. package/dist/sync-routes.js +187 -13
  31. package/dist/template-record.d.ts +4 -3
  32. package/dist/template-record.js +8 -4
  33. package/dist/topology.js +25 -1
  34. package/dist/update.js +15 -0
  35. package/package.json +3 -3
  36. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  37. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  38. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  39. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  41. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  43. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  46. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  48. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  49. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  50. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  51. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  52. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  53. package/templates/.env.example +9 -7
  54. package/templates/.github/workflows/ci.yml +13 -0
  55. package/templates/README.md +1 -0
  56. package/templates/_gitignore +4 -0
  57. package/templates/apps/api/.env.example +27 -0
  58. package/templates/apps/api/CLAUDE.md +87 -17
  59. package/templates/apps/api/package.json +1 -0
  60. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  61. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  62. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  63. package/templates/apps/api/src/config/index.ts +109 -0
  64. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  65. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  66. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  67. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  68. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  69. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  70. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  71. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  72. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  73. package/templates/apps/api/src/lib/stripe.ts +74 -4
  74. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  75. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  77. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  78. package/templates/apps/api/src/middleware/auth.ts +13 -6
  79. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  80. package/templates/apps/api/src/openapi/spec.ts +33 -3
  81. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  82. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  83. package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
  84. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  85. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
  86. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  87. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  88. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  89. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  90. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  91. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  92. package/templates/apps/api/src/routes/consent/account.ts +268 -0
  93. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  94. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  95. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  96. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  97. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  98. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  99. package/templates/apps/api/src/routes/content/index.ts +16 -2
  100. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  101. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  102. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  103. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  104. package/templates/apps/api/src/routes/leads/index.ts +164 -7
  105. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  106. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  111. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  112. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  113. package/templates/apps/api/src/routes/webhooks/stripe.ts +245 -36
  114. package/templates/apps/api/src/server.ts +163 -46
  115. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  116. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
  117. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  118. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1383 -133
  119. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  120. package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
  121. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
  122. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  123. package/templates/apps/api/src/services/consent.ts +95 -0
  124. package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
  125. package/templates/apps/api/src/services/entitlements.ts +157 -1
  126. package/templates/apps/api/src/services/lead-routing.ts +431 -47
  127. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  128. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
  129. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  130. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
  131. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  132. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
  133. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
  134. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  135. package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
  136. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
  137. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  138. package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
  139. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  140. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  141. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  142. package/templates/apps/api/src/services/person.ts +164 -17
  143. package/templates/apps/api/src/services/plan-offerings.ts +125 -0
  144. package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
  145. package/templates/apps/mcp/CLAUDE.md +22 -14
  146. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  147. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  148. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  149. package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
  150. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  151. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  152. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  153. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  154. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  155. package/templates/apps/mcp/src/server.ts +5 -0
  156. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  157. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  158. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  159. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  160. package/templates/apps/web/.env.example +24 -31
  161. package/templates/apps/web/CLAUDE.md +393 -9
  162. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  163. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  164. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  165. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  166. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  167. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  168. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  169. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  170. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  171. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  172. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  173. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  174. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  175. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  176. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  177. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  178. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  179. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  180. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  181. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  182. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  183. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  184. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  185. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  186. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  187. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  188. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  189. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  190. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  191. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  192. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  193. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  194. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  195. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  196. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
  197. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
  198. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
  199. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -0
  200. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  201. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  202. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +141 -0
  203. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
  204. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
  205. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  206. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  207. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  208. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  209. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  210. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  211. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  212. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
  213. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  214. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
  215. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
  216. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
  217. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
  218. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  219. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
  220. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  221. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  222. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
  223. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  224. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  225. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  226. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  227. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  228. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  229. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  230. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  231. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
  232. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  233. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  234. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  235. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  236. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  237. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  238. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  239. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  240. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  241. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  242. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  243. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  244. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  245. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  246. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  247. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  248. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  249. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  250. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  251. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  252. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  253. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  254. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  255. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  256. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  257. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  258. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  259. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  260. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  261. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  262. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  263. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  264. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  265. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  266. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  267. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  268. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  269. package/templates/apps/web/auth.ts +28 -2
  270. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  271. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  272. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  273. package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
  274. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  275. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
  276. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  277. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  278. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +101 -25
  279. package/templates/apps/web/lib/account-invite.ts +37 -0
  280. package/templates/apps/web/lib/auth-routes.ts +36 -2
  281. package/templates/apps/web/lib/checkout-config.ts +16 -0
  282. package/templates/apps/web/lib/consent-policies.ts +22 -0
  283. package/templates/apps/web/lib/deck-page.ts +7 -0
  284. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  285. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  286. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  287. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  288. package/templates/apps/web/lib/magnet-routes.ts +42 -0
  289. package/templates/apps/web/lib/modules.generated.ts +9 -0
  290. package/templates/apps/web/lib/newsletter.actions.ts +202 -0
  291. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  292. package/templates/apps/web/lib/posting-title.ts +17 -0
  293. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  294. package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
  295. package/templates/apps/web/lib/site-chrome.ts +2 -0
  296. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  297. package/templates/apps/web/messages/en.json +71 -20
  298. package/templates/apps/web/middleware.ts +12 -2
  299. package/templates/apps/web/package.json +1 -0
  300. package/templates/apps/web/security-headers.config.ts +17 -0
  301. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  302. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  303. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  304. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  305. package/templates/content/en/BlogList/blog.mdx +4 -71
  306. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  307. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  308. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  309. package/templates/content/en/BookCall/start-project.mdx +1 -17
  310. package/templates/content/en/ContactPage/contact.mdx +6 -63
  311. package/templates/content/en/HomePage/home.mdx +3 -21
  312. package/templates/database/CHANGELOG.md +69 -0
  313. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  314. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  315. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  316. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  317. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  318. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  319. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  320. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  321. package/templates/database/__tests__/seed.test.ts +64 -1
  322. package/templates/database/auth/schema.prisma +4 -0
  323. package/templates/database/config/schema.prisma +91 -0
  324. package/templates/database/inbox/schema.prisma +17 -2
  325. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  326. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  327. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  328. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  329. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  330. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  331. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  332. package/templates/database/package.json +1 -1
  333. package/templates/database/seed.ts +62 -3
  334. package/versions.json +35 -35
  335. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  336. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
  337. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  338. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  339. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  340. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  341. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  342. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -0,0 +1,449 @@
1
+ /**
2
+ * Sign-up agreements, refused and recorded on the server (#8765 D3, D4, spec
3
+ * docs/specs/8765-sign-up-refuses-without-the-spec.md).
4
+ *
5
+ * THREE PIECES, ONE FILE. `auth.ts` installs all three:
6
+ *
7
+ * signUpAgreementsPlugin() a Better Auth plugin. While `isSignUpOpen()`,
8
+ * `POST /sign-up/email` without every sign-up
9
+ * agreement in its `agreements` body key is
10
+ * refused `400 AGREEMENTS_REQUIRED` — BEFORE
11
+ * the handler's user lookup, so the answer is
12
+ * the same for a new and a taken address
13
+ * (NFR2). The OAuth callback only warms the
14
+ * evidence cache, outside the transaction.
15
+ * withAuthTransactionCapture() wraps the client the Prisma adapter holds,
16
+ * so the transaction client Better Auth's
17
+ * sign-up transaction runs on is reachable
18
+ * from a database hook.
19
+ * signUpAgreementHooks() two database hooks: the user hook decides
20
+ * whether this request's new user carries
21
+ * agreements; the account hook writes one
22
+ * `sign_up` grant per agreement on the
23
+ * captured transaction client, so the User,
24
+ * its Account and the grants commit together
25
+ * or not at all (NFR1).
26
+ *
27
+ * THE EVIDENCE IS THE API'S. Each grant's statement, hash and version come
28
+ * from `GET /v1/consent/sign-up` — built by the same functions `apps/api`'s
29
+ * `services/consent.ts` stamps with — never from this app's copy of the
30
+ * registry. It is cached per process: fresh for 60 s, the last good entry
31
+ * served for up to 10 minutes while a refresh fails, at most one entry per
32
+ * enabled locale (SEC-8765-D). The read carries this site's `AILK_SPOKE_KEY`
33
+ * (the api-client attaches it), which the hub counts against the key's own
34
+ * budget rather than this instance's egress IP (#8805). No usable entry →
35
+ * `503 AGREEMENTS_UNAVAILABLE`: no user is created without evidence. The database
36
+ * hooks run INSIDE the transaction and never call the network; they read the
37
+ * cache only (SEC-8765-I).
38
+ *
39
+ * THE SECOND LEDGER WRITER. `services/consent.ts` writes every other row.
40
+ * This hook writes only `sign_up` rows for a user it is creating in the same
41
+ * transaction, so `consent.ts`'s advisory lock and write-only-on-change rule
42
+ * have nothing to guard here. It reaches the database through the auth
43
+ * client apps/web already holds as its one documented exception
44
+ * (`lib/prisma-for-auth.ts`), and imports no `@working-theory/database`
45
+ * value.
46
+ *
47
+ * NOT EVERY NEW USER HAS A GRANT (SEC-8765-C). A first social sign-in from
48
+ * `/sign-in`, or the `idToken` branch, carries no agreements; Better Auth
49
+ * still creates the user, and the account interstitial asks before the
50
+ * account loads.
51
+ *
52
+ * `better-auth/api` is imported lazily, inside the hooks: it is ESM-only, and
53
+ * the CommonJS Jest suites that build `createAuth()` mock only the
54
+ * `better-auth` modules `auth.ts` itself imports. Precedent for the file's
55
+ * shape: `lib/auth-email.ts`.
56
+ */
57
+
58
+ import { AsyncLocalStorage } from "node:async_hooks";
59
+
60
+ import { consent } from "@working-theory/api-client";
61
+ import {
62
+ consentSignUpResponseSchema,
63
+ SIGN_UP_AGREEMENTS_FIELD,
64
+ signUpAgreementsSchema,
65
+ type ConsentSignUpResponse,
66
+ } from "@working-theory/validation";
67
+ import type { BetterAuthOptions, BetterAuthPlugin } from "better-auth";
68
+
69
+ import { isSignUpOpen } from "./auth-env";
70
+
71
+ const SIGN_UP_PATH = "/sign-up/email";
72
+ const CALLBACK_PREFIX = "/callback/";
73
+ const DEFAULT_LOCALE = "en";
74
+ const FRESH_MS = 60_000;
75
+ const STALE_MS = 10 * 60_000;
76
+ /** The longest one evidence read may hold a sign-up or a sign-in. */
77
+ const READ_TIMEOUT_MS = 2_000;
78
+
79
+ // ── The evidence cache ──────────────────────────────────────────────────────
80
+
81
+ interface CacheEntry {
82
+ evidence: ConsentSignUpResponse;
83
+ fetchedAt: number;
84
+ }
85
+
86
+ const cache = new Map<string, CacheEntry>();
87
+ const refreshing = new Map<string, Promise<ConsentSignUpResponse | null>>();
88
+ let clock: () => number = Date.now;
89
+
90
+ /** Test seam: empties the per-process cache and sets the clock it reads. */
91
+ export function resetSignUpEvidenceCache(now: () => number = Date.now): void {
92
+ cache.clear();
93
+ refreshing.clear();
94
+ clock = now;
95
+ }
96
+
97
+ /**
98
+ * A body or state `locale` counts only when it is one of the site's enabled
99
+ * locales — the list `middleware.ts` routes. Anything else reads as `en`, so
100
+ * the cache holds at most one entry per enabled locale (SEC-8765-D).
101
+ */
102
+ function normaliseLocale(raw: unknown): string {
103
+ return typeof raw === "string" && enabledLocales().includes(raw)
104
+ ? raw
105
+ : DEFAULT_LOCALE;
106
+ }
107
+
108
+ function enabledLocales(): string[] {
109
+ const enabled = (process.env.NEXT_PUBLIC_ENABLED_LOCALES ?? DEFAULT_LOCALE)
110
+ .split(",")
111
+ .map((locale) => locale.trim())
112
+ .filter((locale) => locale.length > 0);
113
+ return enabled.length > 0 ? enabled : [DEFAULT_LOCALE];
114
+ }
115
+
116
+ /** One read of the API's evidence. Never throws: a failure is `null`. */
117
+ async function fetchEvidence(
118
+ locale: string,
119
+ ): Promise<ConsentSignUpResponse | null> {
120
+ // Bounded, so a slow API never holds a sign-up or an OAuth callback: a
121
+ // timeout is a failed read (sign-up fails closed; the interstitial asks an
122
+ // OAuth user later).
123
+ let timer: ReturnType<typeof setTimeout> | undefined;
124
+ const timedOut = new Promise<null>((resolve) => {
125
+ timer = setTimeout(() => resolve(null), READ_TIMEOUT_MS);
126
+ });
127
+ try {
128
+ const res = await Promise.race([
129
+ consent.signUpEvidence({ locale }),
130
+ timedOut,
131
+ ]);
132
+ if (res === null || !res.ok) return null;
133
+ const parsed = consentSignUpResponseSchema.safeParse(res.body);
134
+ if (!parsed.success) return null;
135
+ cache.set(locale, { evidence: parsed.data, fetchedAt: clock() });
136
+ return parsed.data;
137
+ } catch {
138
+ return null;
139
+ } finally {
140
+ clearTimeout(timer);
141
+ }
142
+ }
143
+
144
+ /** Concurrent misses for one locale share one read. */
145
+ function refresh(locale: string): Promise<ConsentSignUpResponse | null> {
146
+ const pending = refreshing.get(locale);
147
+ if (pending) return pending;
148
+ const read = fetchEvidence(locale).finally(() => refreshing.delete(locale));
149
+ refreshing.set(locale, read);
150
+ return read;
151
+ }
152
+
153
+ /** The cached entry while it may still be served (≤ 10 min old), no network. */
154
+ function cachedEvidence(locale: string): ConsentSignUpResponse | null {
155
+ const entry = cache.get(locale);
156
+ return entry && clock() - entry.fetchedAt <= STALE_MS ? entry.evidence : null;
157
+ }
158
+
159
+ /** Fresh evidence, refreshing when older than 60 s; stale on a failed refresh. */
160
+ async function readEvidence(
161
+ locale: string,
162
+ ): Promise<ConsentSignUpResponse | null> {
163
+ const entry = cache.get(locale);
164
+ if (entry && clock() - entry.fetchedAt < FRESH_MS) return entry.evidence;
165
+ return (await refresh(locale)) ?? cachedEvidence(locale);
166
+ }
167
+
168
+ /** Whether `raw` names every purpose the evidence asks for. */
169
+ function coversEvidence(
170
+ raw: unknown,
171
+ evidence: ConsentSignUpResponse,
172
+ ): boolean {
173
+ const parsed = signUpAgreementsSchema.safeParse(raw);
174
+ if (!parsed.success) return false;
175
+ return evidence.agreements.every((agreement) =>
176
+ parsed.data.includes(agreement.purposeId),
177
+ );
178
+ }
179
+
180
+ async function apiError(
181
+ status: "BAD_REQUEST" | "SERVICE_UNAVAILABLE",
182
+ code: "AGREEMENTS_REQUIRED" | "AGREEMENTS_UNAVAILABLE",
183
+ message: string,
184
+ ): Promise<Error> {
185
+ const { APIError } = await import("better-auth/api");
186
+ return new APIError(status, { code, message });
187
+ }
188
+
189
+ const agreementsRequired = (): Promise<Error> =>
190
+ apiError(
191
+ "BAD_REQUEST",
192
+ "AGREEMENTS_REQUIRED",
193
+ "Accept the agreements to create an account.",
194
+ );
195
+
196
+ const agreementsUnavailable = (): Promise<Error> =>
197
+ apiError(
198
+ "SERVICE_UNAVAILABLE",
199
+ "AGREEMENTS_UNAVAILABLE",
200
+ "The sign-up agreements could not be read. Try again in a moment.",
201
+ );
202
+
203
+ /** A request body, as the hooks see it: never trusted, read by key. */
204
+ function bodyField(body: unknown, key: string): unknown {
205
+ return typeof body === "object" && body !== null
206
+ ? Reflect.get(body, key)
207
+ : undefined;
208
+ }
209
+
210
+ // ── D3 — the plugin ─────────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Refuses `POST /sign-up/email` without the agreements (D3), and warms the
214
+ * evidence cache on the OAuth callback so the user hook below finds it
215
+ * without a network call inside the transaction (SEC-8765-I).
216
+ */
217
+ export function signUpAgreementsPlugin(): BetterAuthPlugin {
218
+ return {
219
+ id: "sign-up-agreements",
220
+ hooks: {
221
+ before: [
222
+ {
223
+ matcher: (context) => context.path === SIGN_UP_PATH,
224
+ handler: async (context) => {
225
+ // Closed sign-up refuses in the handler itself (`disableSignUp`).
226
+ if (!isSignUpOpen()) return;
227
+ const body: unknown = context.body;
228
+ const evidence = await readEvidence(
229
+ normaliseLocale(bodyField(body, "locale")),
230
+ );
231
+ if (!evidence) throw await agreementsUnavailable();
232
+ if (evidence.agreements.length === 0) return;
233
+ if (
234
+ !coversEvidence(
235
+ bodyField(body, SIGN_UP_AGREEMENTS_FIELD),
236
+ evidence,
237
+ )
238
+ ) {
239
+ throw await agreementsRequired();
240
+ }
241
+ },
242
+ },
243
+ {
244
+ matcher: (context) =>
245
+ context.path?.startsWith(CALLBACK_PREFIX) ?? false,
246
+ // Warms only, and only while sign-up is open. The state — and so
247
+ // the locale the card sent — is parsed inside the handler, so every
248
+ // enabled locale is warmed; each is read at most once a minute and
249
+ // each read is bounded. Never refuses.
250
+ handler: async () => {
251
+ if (!isSignUpOpen()) return;
252
+ await Promise.all(
253
+ enabledLocales().map((locale) => readEvidence(locale)),
254
+ );
255
+ },
256
+ },
257
+ ],
258
+ },
259
+ };
260
+ }
261
+
262
+ // ── D4 — the transaction capture ────────────────────────────────────────────
263
+
264
+ const capturedTx = new AsyncLocalStorage<unknown>();
265
+
266
+ /**
267
+ * Wraps the client the Prisma adapter holds. Its `$transaction(fn, options)`
268
+ * calls the client's own `$transaction` and runs `fn(tx)` inside an
269
+ * `AsyncLocalStorage` holding `tx`; every other member is the client's own.
270
+ * The capture never changes what a transaction does: options pass through
271
+ * unchanged, and the array form is delegated as is.
272
+ *
273
+ * A client with no `$transaction` member (a test double) gets `fn(client)` —
274
+ * what the adapter did for it before `transaction: true` — and no `tx` is
275
+ * captured, so a sign-up with agreements fails closed on it.
276
+ */
277
+ export function withAuthTransactionCapture<Client extends object>(
278
+ client: Client,
279
+ ): Client {
280
+ const $transaction = (
281
+ fn: unknown,
282
+ ...options: unknown[]
283
+ ): Promise<unknown> => {
284
+ const own: unknown = Reflect.get(client, "$transaction");
285
+ if (typeof fn !== "function") {
286
+ return Promise.resolve(
287
+ typeof own === "function"
288
+ ? Reflect.apply(own, client, [fn, ...options])
289
+ : undefined,
290
+ );
291
+ }
292
+ if (typeof own !== "function") {
293
+ return Promise.resolve().then(() =>
294
+ Reflect.apply(fn, undefined, [client]),
295
+ );
296
+ }
297
+ const run = (tx: unknown): unknown =>
298
+ capturedTx.run(tx, () => Reflect.apply(fn, undefined, [tx]));
299
+ return Promise.resolve(Reflect.apply(own, client, [run, ...options]));
300
+ };
301
+
302
+ return new Proxy(client, {
303
+ get: (target, member) =>
304
+ member === "$transaction" ? $transaction : Reflect.get(target, member),
305
+ });
306
+ }
307
+
308
+ // ── D4 — the database hooks ─────────────────────────────────────────────────
309
+
310
+ /** The one delegate method the account hook calls on the transaction client. */
311
+ interface ConsentGrantWriter {
312
+ consentGrant: {
313
+ createMany(args: { data: ConsentGrantRow[] }): Promise<unknown>;
314
+ };
315
+ }
316
+
317
+ interface ConsentGrantRow {
318
+ siteId: string;
319
+ workspaceId: string;
320
+ subjectType: "user";
321
+ subjectId: string;
322
+ personId: null;
323
+ purposeId: string;
324
+ granted: true;
325
+ source: "sign_up";
326
+ sourceRef: null;
327
+ policyVersion: number;
328
+ locale: string;
329
+ statement: string;
330
+ statementHash: string;
331
+ }
332
+
333
+ function isConsentGrantWriter(tx: unknown): tx is ConsentGrantWriter {
334
+ if (typeof tx !== "object" || tx === null) return false;
335
+ const delegate: unknown = Reflect.get(tx, "consentGrant");
336
+ return (
337
+ typeof delegate === "object" &&
338
+ delegate !== null &&
339
+ typeof Reflect.get(delegate, "createMany") === "function"
340
+ );
341
+ }
342
+
343
+ type DatabaseHooks = NonNullable<BetterAuthOptions["databaseHooks"]>;
344
+ type HookContext = Parameters<
345
+ NonNullable<
346
+ NonNullable<NonNullable<DatabaseHooks["user"]>["create"]>["before"]
347
+ >
348
+ >[1];
349
+
350
+ /**
351
+ * The evidence the user being created agreed to, or `null` when none is to be
352
+ * recorded. Reads the cache only — the transaction is already open.
353
+ */
354
+ async function agreedEvidence(
355
+ context: NonNullable<HookContext>,
356
+ ): Promise<ConsentSignUpResponse | null> {
357
+ const path = typeof context.path === "string" ? context.path : "";
358
+
359
+ if (path === SIGN_UP_PATH) {
360
+ if (!isSignUpOpen()) return null;
361
+ const evidence = cachedEvidence(
362
+ normaliseLocale(bodyField(context.body, "locale")),
363
+ );
364
+ // The entry aged out between the plugin and here: fail closed.
365
+ if (!evidence) throw await agreementsUnavailable();
366
+ if (evidence.agreements.length === 0) return null;
367
+ // The plugin checked the body against the entry it read; a refresh since
368
+ // may have changed it, so the evidence recorded is checked again here.
369
+ if (
370
+ !coversEvidence(
371
+ bodyField(context.body, SIGN_UP_AGREEMENTS_FIELD),
372
+ evidence,
373
+ )
374
+ ) {
375
+ throw await agreementsRequired();
376
+ }
377
+ return evidence;
378
+ }
379
+
380
+ if (path.startsWith(CALLBACK_PREFIX)) {
381
+ const { getOAuthState } = await import("better-auth/api");
382
+ const state: unknown = await getOAuthState().catch(() => null);
383
+ const evidence = cachedEvidence(
384
+ normaliseLocale(bodyField(state, "locale")),
385
+ );
386
+ if (!evidence || evidence.agreements.length === 0) return null;
387
+ return coversEvidence(bodyField(state, SIGN_UP_AGREEMENTS_FIELD), evidence)
388
+ ? evidence
389
+ : null;
390
+ }
391
+
392
+ return null;
393
+ }
394
+
395
+ /**
396
+ * The two database hooks (D4). They share one request's context object —
397
+ * Better Auth hands both the endpoint context it runs the handler in — and
398
+ * that object keys the `WeakMap` between them.
399
+ */
400
+ export function signUpAgreementHooks(): DatabaseHooks {
401
+ const pending = new WeakMap<object, ConsentSignUpResponse>();
402
+
403
+ return {
404
+ user: {
405
+ create: {
406
+ // Changes nothing on the user; only remembers what it agreed to.
407
+ before: async (_user, context) => {
408
+ if (!context) return;
409
+ const evidence = await agreedEvidence(context);
410
+ if (evidence) pending.set(context, evidence);
411
+ },
412
+ },
413
+ },
414
+ account: {
415
+ create: {
416
+ // Set only in a request creating a user, whose first account this is.
417
+ before: async (account, context) => {
418
+ if (!context) return;
419
+ const evidence = pending.get(context);
420
+ if (!evidence) return;
421
+ pending.delete(context);
422
+ const tx = capturedTx.getStore();
423
+ if (!isConsentGrantWriter(tx)) {
424
+ throw new Error(
425
+ "sign-up agreements: no transaction client captured; refusing to create the user without its grants",
426
+ );
427
+ }
428
+ await tx.consentGrant.createMany({
429
+ data: evidence.agreements.map((agreement) => ({
430
+ siteId: evidence.workspaceId,
431
+ workspaceId: evidence.workspaceId,
432
+ subjectType: "user",
433
+ subjectId: account.userId,
434
+ personId: null,
435
+ purposeId: agreement.purposeId,
436
+ granted: true,
437
+ source: "sign_up",
438
+ sourceRef: null,
439
+ policyVersion: agreement.policyVersion,
440
+ locale: agreement.locale,
441
+ statement: agreement.statement,
442
+ statementHash: agreement.statementHash,
443
+ })),
444
+ });
445
+ },
446
+ },
447
+ },
448
+ };
449
+ }
@@ -16,6 +16,7 @@
16
16
  // - innerPages: "bare" → every non-home page renders the bare header and
17
17
  // the one-bar footer.
18
18
  // - deckPresentation → the deck home presents slide by slide: fitted slides, slide anchors, the mark follows the ground
19
+ // - deckAnchors: "bare" → the presenting deck names its slides #<id> instead of #slide-<id>
19
20
  // - colorMode: "light" → the site is pinned to light and shows no theme
20
21
  // switch; dark appears only as a deck ground.
21
22
  // - chromeStyle: "minimal" → the deck's closing slide is the wordmark,
@@ -31,6 +32,7 @@ export interface SiteChrome {
31
32
  home: "default" | "deck";
32
33
  innerPages?: "bare";
33
34
  deckPresentation?: true;
35
+ deckAnchors?: "bare";
34
36
  colorMode?: "light";
35
37
  chromeStyle?: "minimal";
36
38
  closingFooterPaths?: readonly string[];
@@ -9,10 +9,14 @@
9
9
  // `/purchase/success?session_id=cs_…` is the SAME class of bearer capability
10
10
  // — the exact defect #5562 fixed for `?g=`, recurring on a second surface.
11
11
  //
12
+ // `email` and `login_hint` joined by #8757 (D9, SEC-8757-C): an account link
13
+ // carries the submitter's address as `/sign-in?email=…` and the hub's
14
+ // authorize step as `login_hint=…` — personal data GA and Sentry must not get.
15
+ //
12
16
  // Mirrored (duplicated, not shared) in packages/analytics/src/scrub.ts,
13
17
  // since apps/* must not import @working-theory/* packages — keep both lists
14
18
  // in sync (#5613).
15
- export const TOKEN_QUERY_PARAMS = ["g", "session_id"];
19
+ export const TOKEN_QUERY_PARAMS = ["g", "session_id", "email", "login_hint"];
16
20
 
17
21
  // Sentry navigation breadcrumbs (`data.to`/`data.from`) are relative
18
22
  // (`path?search`, no origin) rather than the absolute URLs GA and
@@ -94,7 +94,27 @@
94
94
  },
95
95
  "careers": {
96
96
  "heading": "Careers",
97
- "emptyState": "Career listings coming soon."
97
+ "emptyState": "Career listings coming soon.",
98
+ "applicationSuccess": "Thank you for applying! We have your application and will be in touch.",
99
+ "apply": "Apply for this role",
100
+ "openPositions": "Open positions",
101
+ "viewRole": "View role",
102
+ "employmentTypeFullTime": "Full-time",
103
+ "employmentTypePartTime": "Part-time",
104
+ "employmentTypeContractor": "Contract",
105
+ "employmentTypeTemporary": "Temporary",
106
+ "employmentTypeIntern": "Internship",
107
+ "employmentTypeVolunteer": "Volunteer",
108
+ "employmentTypePerDiem": "Per diem",
109
+ "employmentTypeOther": "Other",
110
+ "filterSearch": "Search roles",
111
+ "filterType": "Type",
112
+ "filterAllTypes": "All types",
113
+ "filterLocation": "Location",
114
+ "filterAllLocations": "All locations",
115
+ "filterEmpty": "No open positions match your search.",
116
+ "filterCountOne": "1 open position",
117
+ "filterCountOther": "{count} open positions"
98
118
  },
99
119
  "case-studies": {
100
120
  "heading": "Case Studies",
@@ -125,7 +145,9 @@
125
145
  "sending": "Sending…",
126
146
  "sendMessage": "Send message",
127
147
  "successMessage": "Thank you for reaching out! We will be in touch shortly.",
128
- "consentStatement": "I accept the [Terms](terms), [Privacy Policy](privacy), and [Working Agreement](working_agreement).",
148
+ "trackInAccount": "Track this in your account",
149
+ "consentStatement": "I accept the [Terms](terms) and [Privacy Policy](privacy).",
150
+ "preferEmail": "Prefer email?",
129
151
  "consentRequired": "Please accept the terms to send your message.",
130
152
  "genericError": "Something went wrong, please try again.",
131
153
  "unavailableHeading": "Get in touch",
@@ -140,8 +162,12 @@
140
162
  "namePrompt": "What's your name?",
141
163
  "messageAck": "Good to meet you.",
142
164
  "messagePrompt": "What are you hoping to get done?",
143
- "emailAck": "Got it — last one.",
165
+ "emailAck": "Got it.",
144
166
  "emailPrompt": "What's the best email to reach you at?",
167
+ "consentAck": "Thanks — one last thing.",
168
+ "consentPrompt": "Do you accept our Terms and Privacy Policy? They're linked at the foot of every page.",
169
+ "consentAccept": "Yes, I accept",
170
+ "consentDecline": "Not yet",
145
171
  "skipMessage": "No worries — here's our form instead.",
146
172
  "completionMessage": "Thanks! We'll be in touch shortly.",
147
173
  "submitError": "We couldn't send that automatically — please review and send below."
@@ -179,6 +205,9 @@
179
205
  "selectAllThatApply": "Select all that apply",
180
206
  "getStarted": "Get Started",
181
207
  "landingIntro": "A few short questions, then your result — it takes about a minute.",
208
+ "unavailableHeading": "This isn't available right now",
209
+ "unavailableBody": "Email us and we'll help.",
210
+ "unavailableNoEmail": "Please try again later.",
182
211
  "bookCall": "Book a Call",
183
212
  "emailMePlan": "Save plan",
184
213
  "pairingTitle": "Better together",
@@ -259,6 +288,15 @@
259
288
  "heading": "Pricing",
260
289
  "emptyState": "Pricing page content coming soon."
261
290
  },
291
+ "pricingOfferings": {
292
+ "cta": "Get started",
293
+ "recommended": "Recommended",
294
+ "recommendedForYou": "Recommended for you",
295
+ "addonsHeadline": "Add-ons",
296
+ "monthly": "{amount}/mo",
297
+ "annual": "{amount}/yr",
298
+ "oneTime": "{amount} one-time"
299
+ },
262
300
  "purchasePolicy": {
263
301
  "heading": "Purchase Policy",
264
302
  "emptyState": "Purchase policy content coming soon."
@@ -312,7 +350,8 @@
312
350
  },
313
351
  "checkoutThankYou": {
314
352
  "title": "Thank you",
315
- "body": "Your payment went through. We'll email you a receipt and the next steps."
353
+ "body": "Your payment went through. We'll email you a receipt and the next steps.",
354
+ "openAccount": "Open your account"
316
355
  },
317
356
  "checkoutUnavailable": {
318
357
  "title": "Checkout isn't available right now",
@@ -328,6 +367,14 @@
328
367
  "heading": "Book time with me",
329
368
  "description": "Pick a time that works for you.",
330
369
  "headline": "Pick a time that works",
370
+ "bookLength": "Book {minutes} min",
371
+ "meetingHeading": "Schedule a {title} meeting",
372
+ "meetingHeadingFallback": "Schedule a meeting",
373
+ "allMeetingTypes": "← All meeting types",
374
+ "backToSite": "Back",
375
+ "unavailableHeading": "Book a time by email",
376
+ "unavailableBody": "Email {email} to book a time.",
377
+ "unavailableNoEmail": "Online booking isn't available right now.",
331
378
  "confirmed": {
332
379
  "title": "You're booked",
333
380
  "description": "Your call is confirmed and the calendar invite is on its way.",
@@ -377,7 +424,12 @@
377
424
  },
378
425
  "reports": {
379
426
  "heading": "Reports",
380
- "emptyState": "Reports coming soon."
427
+ "emptyState": "Reports coming soon.",
428
+ "gateIntro": "Tell us where to send it, and the report opens right away.",
429
+ "gateCta": "Get the report",
430
+ "openFile": "Open the report",
431
+ "fileByEmail": "The link is on its way to your email.",
432
+ "openExternal": "Open it"
381
433
  },
382
434
  "waitlistDashboard": {
383
435
  "gate": {
@@ -499,8 +551,7 @@
499
551
  "signIn": {
500
552
  "title": "Sign in",
501
553
  "description": "Sign in to your account.",
502
- "heading": "Sign in to your account",
503
- "subhead": "Welcome back. Choose how you'd like to continue.",
554
+ "heading": "Welcome back",
504
555
  "notConfiguredSubhead": "Sign-in is not configured for this site yet. Set a provider's environment variables to enable it.",
505
556
  "ssoPrefix": "Sign in with",
506
557
  "google": "Google",
@@ -520,7 +571,14 @@
520
571
  "emailLinkSentBody": "If an account uses that address, a sign-in link is on its way. It works once and expires in 5 minutes.",
521
572
  "emailLinkFailed": "We couldn't send a sign-in link. Try again.",
522
573
  "emailLinkRateLimited": "Too many requests. Wait a minute, then try again.",
523
- "emailLinkExpired": "That sign-in link has expired or was already used. Request a new one."
574
+ "emailLinkExpired": "That sign-in link has expired or was already used. Request a new one.",
575
+ "useEmailLink": "Email me a sign-in link instead",
576
+ "emailPlaceholder": "you@example.com",
577
+ "backTo": "Back to {site}",
578
+ "acknowledgement": "You acknowledge that you read and agree to our",
579
+ "termsLink": "Terms of Service",
580
+ "privacyLink": "Privacy Policy",
581
+ "workingAgreementLink": "Working Agreement"
524
582
  },
525
583
  "auth": {
526
584
  "signUpTitle": "Create an account",
@@ -546,6 +604,7 @@
546
604
  "signUpFooterPrompt": "Already have an account?",
547
605
  "signUpFooterLink": "Sign in",
548
606
  "signUpAgreementsLegend": "Agreements",
607
+ "signUpAgreementsRequired": "Accept the agreements to create an account.",
549
608
  "signUpNotConfiguredSubhead": "Sign-up is not configured for this site yet. Set a provider's environment variables to enable it.",
550
609
  "signUpFailed": "We couldn't create your account. Check your details and try again.",
551
610
  "providerFailed": "We couldn't start sign-up with that provider. Try again.",
@@ -852,7 +911,9 @@
852
911
  "agreements": {
853
912
  "titleNew": "Please accept our terms",
854
913
  "titleUpdated": "We've updated our terms",
914
+ "lead": "To keep using your account, accept the agreements below.",
855
915
  "accept": "Accept",
916
+ "signOut": "Sign out",
856
917
  "error": "We couldn't save that. Try again."
857
918
  },
858
919
  "gate": {
@@ -862,7 +923,8 @@
862
923
  "homeLink": "Back to the site",
863
924
  "hubHeadline": "Sign in to your account",
864
925
  "hubDescription": "You sign in on our main site, then come straight back here.",
865
- "hubSignIn": "Sign in"
926
+ "hubSignIn": "Sign in",
927
+ "title": "Sign in"
866
928
  },
867
929
  "profile": {
868
930
  "email": "Email",
@@ -1138,18 +1200,7 @@
1138
1200
  "heading": "Features",
1139
1201
  "emptyState": "Features coming soon."
1140
1202
  },
1141
- "newsletter": {
1142
- "emailLabel": "Email",
1143
- "submit": "Subscribe",
1144
- "sending": "Subscribing…",
1145
- "success": "You're subscribed. Thanks for signing up!",
1146
- "consentStatement": "I accept the [Terms](terms)."
1147
- },
1148
1203
  "unsubscribe": {
1149
- "title": "Unsubscribe",
1150
- "lead": "Stop these emails?",
1151
- "button": "Unsubscribe",
1152
- "done": "You're unsubscribed from {description}.",
1153
1204
  "already": "You were already unsubscribed.",
1154
1205
  "invalid": "This link isn't valid.",
1155
1206
  "unavailable": "Unsubscribing isn't available right now."