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,268 @@
1
+ /**
2
+ * The sign-up agreements: their evidence, and the account-level reading
3
+ * (#8765, spec docs/specs/8765-sign-up-refuses-without-the-spec.md D2, D5).
4
+ *
5
+ * GET /v1/consent/sign-up?locale=
6
+ * Anonymous. `{ workspaceId, agreements: [{ purposeId,
7
+ * policyVersion, locale, statement, statementHash }] }` — one entry per
8
+ * `signUp` agreement of the site registry, its statement resolved by
9
+ * `resolveConsentStatement` for `locale` (default `en`) and hashed by
10
+ * `consentStatementHash`: the same evidence `recordPersonConsent`
11
+ * stamps, from the same functions. `Cache-Control: no-store`.
12
+ * GET /v1/consent/account?locale=
13
+ * The caller's sign-up agreements: `{ agreements: [{ id, statement,
14
+ * policyVersion, acceptedVersion, acceptedAt, current }], complete }`,
15
+ * folded from the caller's `user`-subject rows (`userPlatformRows`).
16
+ * `acceptedVersion`/`acceptedAt` come from the newest row when it is a
17
+ * grant, else `null`; `current` is a grant at the registry's version;
18
+ * `complete` is every entry current.
19
+ * POST /v1/consent/account
20
+ * `{ purposeIds, locale? }` — every id must be a `signUp` agreement
21
+ * (`404 purpose_unknown` with NOTHING written, all ids checked before
22
+ * any write); each is written through `recordPersonConsent` against
23
+ * subject `user` / the caller, no Person, source `account_settings`,
24
+ * with `sourceRef: spoke:<id>` for a hub credential (SEC-8765-A). A
25
+ * grant already at the current version adds nothing. Answers the GET
26
+ * body.
27
+ *
28
+ * The agreement belongs to the User account, a login shared by every site
29
+ * that signs in through this hub (C3), so the workspace is ALWAYS
30
+ * `resolveSiteWorkspaceId()` — the deployment's own site, where its `User`
31
+ * rows live — never a key, body or query value; `null` is
32
+ * `503 tenant_unresolved`. One row there answers the hub and every spoke.
33
+ *
34
+ * Callers. The evidence route: the web's own server, stamping the grant it
35
+ * writes inside Better Auth's sign-up transaction (`apps/web/lib/
36
+ * sign-up-agreements.ts`, D3, D4). Registered at the top level in its own
37
+ * rate-limited scope (`signupevidence`), outside every module gate; a call
38
+ * presenting a valid site key is counted per key there, any other per IP
39
+ * (#8805, `server.ts`) — the key never chooses the workspace. The
40
+ * account pair: `memberCaller` — the hub's own cookie session or a hub
41
+ * credential (the account layout's interstitial, D7); a personal `ApiToken`
42
+ * is `401 unauthorized`. There is no verified-email floor: the agreement is
43
+ * about the `User` row, and no Person is read or created. Registered in
44
+ * `authScope` unconditionally (auth and consent are never-gate surfaces),
45
+ * and both are in `HUB_SESSION_ROUTES`. Logs `{ event: "account-agreements",
46
+ * outcome }` only.
47
+ *
48
+ * MCP parity note: no MCP twin — the evidence route is part of the sign-up
49
+ * flow and the account pair is identity-bound; the anonymous MCP surface is
50
+ * identity-blind (tests/parity/README.md).
51
+ */
52
+ import {
53
+ accountAgreementsBodySchema,
54
+ accountAgreementsQuerySchema,
55
+ accountAgreementsResponseSchema,
56
+ consentSignUpQuerySchema,
57
+ consentSignUpResponseSchema,
58
+ resolveConsentStatement,
59
+ signUpAgreements,
60
+ type AccountAgreementsResponse,
61
+ type SiteConsentConfig,
62
+ } from "@working-theory/validation";
63
+ import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
64
+ import type { z } from "zod";
65
+
66
+ import { userPlatformRows } from "../../lib/consent-store.js";
67
+ import { resolveSiteWorkspaceId } from "../../lib/site.js";
68
+ import { memberCaller } from "../../middleware/auth.js";
69
+ import {
70
+ consentConfigForSite,
71
+ consentStatementHash,
72
+ recordPersonConsent,
73
+ } from "../../services/consent.js";
74
+
75
+ const EVENT = "account-agreements";
76
+
77
+ function refuse(
78
+ reply: FastifyReply,
79
+ status: number,
80
+ code: string,
81
+ message: string,
82
+ ) {
83
+ return reply.status(status).send({ ok: false, error: { code, message } });
84
+ }
85
+
86
+ function send<T extends z.ZodTypeAny>(
87
+ reply: FastifyReply,
88
+ schema: T,
89
+ data: unknown,
90
+ ) {
91
+ const parsed = schema.safeParse(data);
92
+ if (!parsed.success) {
93
+ return refuse(
94
+ reply,
95
+ 500,
96
+ "internal_error",
97
+ "Response failed contract validation",
98
+ );
99
+ }
100
+ return reply.send({ ok: true, data: parsed.data });
101
+ }
102
+
103
+ function log(request: FastifyRequest, outcome: string): void {
104
+ if (outcome === "saved" || outcome === "unchanged") {
105
+ request.log.info({ event: EVENT, outcome }, "[consent] account agreements");
106
+ } else {
107
+ request.log.warn({ event: EVENT, outcome }, "[consent] refused");
108
+ }
109
+ }
110
+
111
+ function refuseUnresolved(request: FastifyRequest, reply: FastifyReply) {
112
+ log(request, "tenant_unresolved");
113
+ return refuse(
114
+ reply,
115
+ 503,
116
+ "tenant_unresolved",
117
+ "This site cannot take this request.",
118
+ );
119
+ }
120
+
121
+ /** The caller's sign-up agreements in the site workspace (D5). */
122
+ async function accountReading(
123
+ workspaceId: string,
124
+ userId: string,
125
+ config: SiteConsentConfig,
126
+ locale: string,
127
+ ): Promise<AccountAgreementsResponse> {
128
+ const rows = await userPlatformRows(workspaceId, userId);
129
+ const newest = new Map(rows.map((row) => [row.purposeId, row]));
130
+ const agreements = signUpAgreements(config).map((purpose) => {
131
+ const row = newest.get(purpose.id);
132
+ const grant = row?.granted === true ? row : null;
133
+ return {
134
+ id: purpose.id,
135
+ statement: resolveConsentStatement(purpose, locale).statement,
136
+ policyVersion: purpose.policyVersion,
137
+ acceptedVersion: grant?.policyVersion ?? null,
138
+ acceptedAt: grant === null ? null : grant.at.toISOString(),
139
+ current: grant !== null && grant.policyVersion === purpose.policyVersion,
140
+ };
141
+ });
142
+ return { agreements, complete: agreements.every((a) => a.current) };
143
+ }
144
+
145
+ /** `GET /v1/consent/sign-up` — top level, its own rate-limited scope (D2). */
146
+ export const consentSignUpRoute: FastifyPluginAsync = async (fastify) => {
147
+ fastify.get("/v1/consent/sign-up", async (request, reply) => {
148
+ reply.header("Cache-Control", "no-store");
149
+ const query = consentSignUpQuerySchema.safeParse(request.query);
150
+ if (!query.success) {
151
+ return refuse(reply, 400, "invalid_input", "The request was not valid.");
152
+ }
153
+ const locale = query.data.locale ?? "en";
154
+
155
+ const workspaceId = await resolveSiteWorkspaceId();
156
+ if (workspaceId === null) return refuseUnresolved(request, reply);
157
+
158
+ const config = await consentConfigForSite(workspaceId);
159
+ const agreements = signUpAgreements(config).map((purpose) => {
160
+ const resolved = resolveConsentStatement(purpose, locale);
161
+ return {
162
+ purposeId: purpose.id,
163
+ policyVersion: purpose.policyVersion,
164
+ locale: resolved.locale,
165
+ statement: resolved.statement,
166
+ statementHash: consentStatementHash(resolved.statement),
167
+ };
168
+ });
169
+ return send(reply, consentSignUpResponseSchema, {
170
+ workspaceId,
171
+ agreements,
172
+ });
173
+ });
174
+ };
175
+
176
+ /** `GET`/`POST /v1/consent/account` — in `authScope`, unconditionally (D5). */
177
+ export const consentAccountRoutes: FastifyPluginAsync = async (fastify) => {
178
+ fastify.get("/v1/consent/account", async (request, reply) => {
179
+ const userId = memberCaller(request);
180
+ if (userId === null) {
181
+ log(request, "unauthorized");
182
+ return reply.status(401).send({ error: "unauthorized" });
183
+ }
184
+ const query = accountAgreementsQuerySchema.safeParse(request.query);
185
+ if (!query.success) {
186
+ log(request, "invalid_input");
187
+ return refuse(reply, 400, "invalid_input", "The request was not valid.");
188
+ }
189
+ const workspaceId = await resolveSiteWorkspaceId();
190
+ if (workspaceId === null) return refuseUnresolved(request, reply);
191
+
192
+ const config = await consentConfigForSite(workspaceId);
193
+ return send(
194
+ reply,
195
+ accountAgreementsResponseSchema,
196
+ await accountReading(
197
+ workspaceId,
198
+ userId,
199
+ config,
200
+ query.data.locale ?? "en",
201
+ ),
202
+ );
203
+ });
204
+
205
+ fastify.post("/v1/consent/account", async (request, reply) => {
206
+ const userId = memberCaller(request);
207
+ if (userId === null) {
208
+ log(request, "unauthorized");
209
+ return reply.status(401).send({ error: "unauthorized" });
210
+ }
211
+ const body = accountAgreementsBodySchema.safeParse(request.body);
212
+ if (!body.success) {
213
+ log(request, "invalid_input");
214
+ return refuse(reply, 400, "invalid_input", "The request was not valid.");
215
+ }
216
+ const locale = body.data.locale ?? "en";
217
+ const workspaceId = await resolveSiteWorkspaceId();
218
+ if (workspaceId === null) return refuseUnresolved(request, reply);
219
+
220
+ const config = await consentConfigForSite(workspaceId);
221
+ // Every id is checked before any write, so a refusal writes nothing.
222
+ const agreementIds = new Set(signUpAgreements(config).map((p) => p.id));
223
+ const purposeIds = [...new Set(body.data.purposeIds)];
224
+ if (!purposeIds.every((id) => agreementIds.has(id))) {
225
+ log(request, "purpose_unknown");
226
+ return refuse(
227
+ reply,
228
+ 404,
229
+ "purpose_unknown",
230
+ "That agreement can't be accepted here.",
231
+ );
232
+ }
233
+
234
+ let changed = false;
235
+ for (const purposeId of purposeIds) {
236
+ const result = await recordPersonConsent({
237
+ workspaceId,
238
+ personId: null,
239
+ purposeId,
240
+ granted: true,
241
+ source: "account_settings",
242
+ ...(request.hubSession !== undefined && {
243
+ sourceRef: `spoke:${request.hubSession.spokeId}`,
244
+ }),
245
+ locale,
246
+ config,
247
+ subject: { subjectType: "user", subjectId: userId },
248
+ });
249
+ if (!result.ok) {
250
+ log(request, result.code);
251
+ return refuse(
252
+ reply,
253
+ result.code === "purpose_unknown" ? 404 : 400,
254
+ result.code,
255
+ "That agreement can't be accepted here.",
256
+ );
257
+ }
258
+ changed ||= result.changed;
259
+ }
260
+
261
+ log(request, changed ? "saved" : "unchanged");
262
+ return send(
263
+ reply,
264
+ accountAgreementsResponseSchema,
265
+ await accountReading(workspaceId, userId, config, locale),
266
+ );
267
+ });
268
+ };
@@ -1,18 +1,26 @@
1
1
  /**
2
2
  * The consent routes (#8438, spec
3
- * docs/specs/8438-consent-registry-and-ledger-step-spec.md D5, D8, D9).
3
+ * docs/specs/8438-consent-registry-and-ledger-step-spec.md D5, D8, D9; #8765,
4
+ * spec docs/specs/8765-sign-up-refuses-without-the-spec.md D2, D5).
4
5
  *
5
6
  * cookie.ts POST /v1/consent/cookie — the banner's log
6
7
  * unsubscribe.ts POST /v1/consent/unsubscribe — a signed link's landing
8
+ * account.ts GET /v1/consent/sign-up — the sign-up agreements'
9
+ * evidence (anonymous)
10
+ * GET/POST /v1/consent/account — the caller's account-level
11
+ * sign-up agreements (cookie session or hub credential)
7
12
  * platform.ts GET/POST /v1/consent/platform — the Admin owner's
8
13
  * consents to Working Theory (multi-tenant module only)
9
14
  *
10
- * The first two are the never-gated privacy floor: registered at the top
11
- * level, each in its OWN rate-limited scope (`consent`, `unsubscribe`) so
12
- * filling one never blocks the other, and both on the forwarded-visitor
13
- * route list (`middleware/rate-limit.ts`). This barrel re-exports exactly
14
- * those two; the platform routes belong to the multi-tenant slice and are
15
- * loaded only through `server.ts`'s slice table.
15
+ * The first two and the evidence route are the never-gated floor: registered
16
+ * at the top level, each in its OWN rate-limited scope (`consent`,
17
+ * `unsubscribe`, `signupevidence`) so filling one never blocks another; the
18
+ * first two are also on the forwarded-visitor route list
19
+ * (`middleware/rate-limit.ts`). The account pair registers in `authScope`,
20
+ * unconditionally. This barrel re-exports those four plugins; the platform
21
+ * routes belong to the multi-tenant slice and are loaded only through
22
+ * `server.ts`'s slice table.
16
23
  */
24
+ export { consentAccountRoutes, consentSignUpRoute } from "./account.js";
17
25
  export { consentCookieRoute } from "./cookie.js";
18
26
  export { consentUnsubscribeRoute } from "./unsubscribe.js";
@@ -206,6 +206,42 @@ describe("GET /api/content/:type/:slug", () => {
206
206
  * established for #5831. `get-page.ts` itself is unedited (N3) — it
207
207
  * already accepted both types via `SCHEMA_ORG_TYPES` before this issue.
208
208
  */
209
+ // #8746 D2, AC-1 — the site origin (`_site.mdx` `baseUrl`) reaches the
210
+ // JobPosting builder, so the posting names where to apply.
211
+ describe("a JobPosting carries its ApplyAction when the site is known (#8746 AC-1)", () => {
212
+ const writePosting = () =>
213
+ fixture.writeFixturePage(
214
+ "JobPosting",
215
+ "content-strategist",
216
+ {
217
+ type: "JobPosting",
218
+ slug: "content-strategist",
219
+ title: "Content Strategist",
220
+ description: "Own the content pipeline.",
221
+ },
222
+ "# Content Strategist",
223
+ );
224
+
225
+ it("targets <baseUrl>/<locale>/careers/<slug>/apply", async () => {
226
+ writeSiteMdx(fixture.getContentRoot());
227
+ await writePosting();
228
+ const res = await app.inject({ method: "GET", url: "/v1/content/JobPosting/content-strategist" });
229
+ expect(res.statusCode).toBe(200);
230
+ const entity = JSON.parse(res.body).mainEntity as Record<string, unknown>;
231
+ expect(entity.potentialAction).toEqual({
232
+ "@type": "ApplyAction",
233
+ target: "https://example.com/en/careers/content-strategist/apply",
234
+ });
235
+ });
236
+
237
+ it("emits no ApplyAction when the deployment has no _site.mdx", async () => {
238
+ await writePosting();
239
+ const res = await app.inject({ method: "GET", url: "/v1/content/JobPosting/content-strategist" });
240
+ expect(res.statusCode).toBe(200);
241
+ expect("potentialAction" in (JSON.parse(res.body).mainEntity as Record<string, unknown>)).toBe(false);
242
+ });
243
+ });
244
+
209
245
  describe("parity with the adapter the MCP tool calls (D3)", () => {
210
246
  it.each(["BlogPosting", "DocsDetail"] as const)(
211
247
  "GET /v1/content/%s/:slug?include=body deep-equals adapter.getPageSource",