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
@@ -45,6 +45,7 @@ import {
45
45
  import {
46
46
  getPageSections,
47
47
  pageToJsonLd,
48
+ siteOrigin,
48
49
  type JsonLdObject,
49
50
  } from "@working-theory/page-renderer";
50
51
  import {
@@ -110,6 +111,13 @@ if (process.env.AILK_CONTENT_ROOT === undefined) {
110
111
  process.env.AILK_CONTENT_ROOT = CONTENT_ROOT;
111
112
  }
112
113
 
114
+ // A page that checks its own locale (`lib/locale-guard`, the meeting page)
115
+ // reads the enabled list next.config.mjs bakes in; pin it to the locale this
116
+ // file renders, as a built app would carry it.
117
+ if (process.env.NEXT_PUBLIC_ENABLED_LOCALES === undefined) {
118
+ process.env.NEXT_PUBLIC_ENABLED_LOCALES = DEFAULT_LOCALE;
119
+ }
120
+
113
121
  // Mirrors the locale-directory rule in tools/content-compile and the adapter.
114
122
  const LOCALE_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
115
123
 
@@ -194,11 +202,8 @@ function extractJsonLdBlocks(container: HTMLElement): JsonLdObject[] {
194
202
  }
195
203
 
196
204
  const SITE_IDENTITY = readSiteIdentity(CONTENT_ROOT);
197
- const SITE_ORIGIN = (
198
- process.env.NEXT_PUBLIC_SITE_URL ??
199
- process.env.MARKETING_WEB_BASE_URL ??
200
- "http://localhost:3000"
201
- ).replace(/\/+$/, "");
205
+ // The resolver's own origin chain (#8797 D5), so this check cannot drift from it.
206
+ const SITE_ORIGIN = siteOrigin(SITE_IDENTITY);
202
207
 
203
208
  /**
204
209
  * The sitewide identity trio every rendered page carries when the site's own
@@ -297,12 +302,24 @@ describe("structured data — every content page builds valid JSON-LD", () => {
297
302
  * exempt route whose page.tsx does render through `renderPage` fails too,
298
303
  * because the reason given for it is false.
299
304
  */
300
- // Literal, not imported: apps/web does not depend on create-ailk (the import
301
- // boundary). Mirrors packages/create-ailk/src/spoke-checkout.ts's
302
- // SPOKE_CHECKOUT_PAGE_MARKER; packages/create-ailk/__tests__/
303
- // spoke-checkout.test.ts pins the two in sync (#8513 AC-5).
304
- const CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
305
+ // The marker both standard spoke checkout pages carry in their header comment
306
+ // (apps/web/app/[locale]/checkout/{thank-you,unavailable}/page.tsx, #8748 D5).
307
+ const CHECKOUT_PAGE_MARKER = "Standard spoke checkout page (#8748)";
308
+ // The marker create-ailk rendered before #8748. A spoke scaffolded then keeps
309
+ // its rendered pages through `create-ailk update` (they are not in its
310
+ // record), so its updated copy of this test must still exempt them.
311
+ const LEGACY_CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
305
312
  const CHECKOUT_PAGE_NOINDEX_LINE = "robots: { index: false, follow: false }";
313
+ // #8840 D13 — the marker every standard magnet page carries in its header
314
+ // comment (`apps/web/app/[locale]/flows/[slug]/magnet-page.tsx`), and the
315
+ // renderer it must import. A magnet page renders no `renderPage` call this
316
+ // harness can read, and its landing's JSON-LD comes from the hub's record, so
317
+ // a page carrying both is exempt at ANY route: `create-ailk add-route magnet`
318
+ // writes a pair at a path the site picks. Keyed on the file, as #8513 is.
319
+ const MAGNET_PAGE_MARKER = "Standard magnet page (#8840)";
320
+ const MAGNET_RENDERER_IMPORT = "flows/[slug]/magnet-page";
321
+ const MAGNET_PAGE_REASON =
322
+ "standard magnet page (#8840) — rendered by flows/[slug]/magnet-page; its landing's JSON-LD comes from the hub's flow record";
306
323
 
307
324
  const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
308
325
  route: string;
@@ -313,21 +330,22 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
313
330
  * entry naming no strings applies to its route unconditionally, as every
314
331
  * entry did before. Keys the exemption on the file, not the topology seam:
315
332
  * a spoke can carry a hand-written page at this route, and a site can
316
- * edit the generated one — either is checked like any other route.
333
+ * edit the generated one — either is checked like any other route. A
334
+ * nested list is any one of its strings.
317
335
  */
318
- requires?: readonly string[];
336
+ requires?: ReadonlyArray<string | readonly string[]>;
319
337
  }> = [
320
338
  // Fumadocs routes: JSON-LD is emitted by @working-theory/fumadocs-schema
321
339
  // from the fumadocs `.source` collection, which is not built in this
322
340
  // harness. Not checked here.
323
341
  {
324
342
  route: "blog",
325
- reason: "fumadocs route — emits via @working-theory/fumadocs-schema",
343
+ reason: "fumadocs route — its .source collection is not built in this harness",
326
344
  subtree: true,
327
345
  },
328
346
  {
329
347
  route: "docs",
330
- reason: "fumadocs route — emits via @working-theory/fumadocs-schema",
348
+ reason: "fumadocs route — its .source collection is not built in this harness",
331
349
  subtree: true,
332
350
  },
333
351
  // Hub routes that enumerate a detail type with no list page type of their
@@ -337,6 +355,10 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
337
355
  route: "how-to",
338
356
  reason: "hand-composed listing; no HowTo list type exists",
339
357
  },
358
+ {
359
+ route: "careers/[slug]/[action]",
360
+ reason: "the Free apply form for a posting; noindex, no JSON-LD (#8849 D7)",
361
+ },
340
362
  {
341
363
  route: "software",
342
364
  reason: "hand-composed listing; no SoftwareProduct list type exists",
@@ -365,11 +387,23 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
365
387
  reason: "interactive flow engine, API-backed",
366
388
  subtree: true,
367
389
  },
368
- { route: "meet", reason: "booking surface, API-backed", subtree: true },
390
+ // #8843 D7 — the meeting page `meet/[slug]` composes its own body from a
391
+ // `BookCall` seed (`getMdxSource` + `pageToJsonLd`), so it is checked like
392
+ // any route; the two indexes and the confirmation page are not pages.
393
+ { route: "meet", reason: "booking index, API-backed (#8843 D8)" },
394
+ { route: "meet/confirmed", reason: "booking confirmation, API-backed" },
395
+ { route: "private", reason: "private booking index, API-backed and noindex (#8843 D8)" },
369
396
  {
370
397
  route: "unsubscribe",
371
398
  reason: "app-shaped landing, API-backed; no content page behind it",
372
399
  },
400
+ // #8848 D12 — `/downloads` and `/downloads/<slug>` are permanent redirects
401
+ // into `/reports`; a redirect renders no page.
402
+ {
403
+ route: "downloads",
404
+ reason: "permanent redirect into /reports (#8848 D12); renders no page",
405
+ subtree: true,
406
+ },
373
407
  // #6084 — both purchase return pages read a Checkout Session (or read
374
408
  // nothing, for cancel) and render via SectionRenderer directly; neither
375
409
  // is a content-adapter page, and the paid render's own schema.org node
@@ -388,22 +422,28 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
388
422
  reason: "auth surfaces, not content pages",
389
423
  subtree: true,
390
424
  },
391
- // #8513 — a spoke carrying commerce gets two generated checkout return
392
- // pages (create-ailk #7838): noindex, rendered through SectionRenderer
393
- // directly, no JSON-LD. Exempt only when the page on disk still carries
394
- // both the generated marker and the noindex line (see `requires` above) —
395
- // a hand-written or edited page at either route is checked like any other.
425
+ // #8513 — the two standard spoke checkout return pages (#8748 D5):
426
+ // noindex, rendered through SectionRenderer directly, no JSON-LD. Exempt
427
+ // only when the page on disk still carries both the standard's marker and
428
+ // the noindex line (see `requires` above) — a hand-written or edited page
429
+ // at either route is checked like any other.
396
430
  {
397
431
  route: "checkout/thank-you",
398
432
  reason:
399
- "spoke checkout return page (create-ailk #7838) — noindex, renders sections only, no JSON-LD",
400
- requires: [CHECKOUT_PAGE_MARKER, CHECKOUT_PAGE_NOINDEX_LINE],
433
+ "spoke checkout return page (#8748) — noindex, renders sections only, no JSON-LD",
434
+ requires: [
435
+ [CHECKOUT_PAGE_MARKER, LEGACY_CHECKOUT_PAGE_MARKER],
436
+ CHECKOUT_PAGE_NOINDEX_LINE,
437
+ ],
401
438
  },
402
439
  {
403
440
  route: "checkout/unavailable",
404
441
  reason:
405
- "spoke checkout return page (create-ailk #7838) — noindex, renders sections only, no JSON-LD",
406
- requires: [CHECKOUT_PAGE_MARKER, CHECKOUT_PAGE_NOINDEX_LINE],
442
+ "spoke checkout return page (#8748) — noindex, renders sections only, no JSON-LD",
443
+ requires: [
444
+ [CHECKOUT_PAGE_MARKER, LEGACY_CHECKOUT_PAGE_MARKER],
445
+ CHECKOUT_PAGE_NOINDEX_LINE,
446
+ ],
407
447
  },
408
448
  ];
409
449
 
@@ -431,6 +471,9 @@ function walkRoutes(dir: string, out: RouteEntry[] = []): RouteEntry[] {
431
471
  }
432
472
 
433
473
  function exemption(route: string, src: string): string | undefined {
474
+ if (src.includes(MAGNET_PAGE_MARKER) && src.includes(MAGNET_RENDERER_IMPORT)) {
475
+ return MAGNET_PAGE_REASON;
476
+ }
434
477
  const entry = NO_STRUCTURED_DATA_ROUTES.find(
435
478
  (e) =>
436
479
  route === e.route ||
@@ -439,7 +482,9 @@ function exemption(route: string, src: string): string | undefined {
439
482
  if (entry === undefined) return undefined;
440
483
  if (
441
484
  entry.requires !== undefined &&
442
- !entry.requires.every((s) => src.includes(s))
485
+ !entry.requires.every((s) =>
486
+ typeof s === "string" ? src.includes(s) : s.some((any) => src.includes(any)),
487
+ )
443
488
  ) {
444
489
  return undefined;
445
490
  }
@@ -645,6 +690,12 @@ describe("structured data — exemption(route, src) keys the generated checkout
645
690
  expect(exemption("checkout/unavailable", NO_NOINDEX_SRC)).toBeUndefined();
646
691
  });
647
692
 
693
+ it("still exempts a page create-ailk rendered before #8748, which update leaves in place", () => {
694
+ const legacy = `// The checkout thank-you page — ${LEGACY_CHECKOUT_PAGE_MARKER}.\nexport async function generateMetadata() {\n return { ${CHECKOUT_PAGE_NOINDEX_LINE} };\n}\n`;
695
+ expect(exemption("checkout/thank-you", legacy)).toContain("noindex");
696
+ expect(exemption("checkout/unavailable", legacy)).toContain("noindex");
697
+ });
698
+
648
699
  it("does not exempt a neighboring checkout route or an unrelated route", () => {
649
700
  expect(exemption("checkout/other", GENERATED_SRC)).toBeUndefined();
650
701
  expect(exemption("about", GENERATED_SRC)).toBeUndefined();
@@ -661,6 +712,31 @@ describe("structured data — exemption(route, src) keys the generated checkout
661
712
  });
662
713
  });
663
714
 
715
+ // ── 2b. exemption(route, src) — the standard magnet pages (#8840 D13) ────
716
+
717
+ describe("structured data — exemption(route, src) keys a standard magnet page on the file (#8840)", () => {
718
+ // The import line is assembled, not written out, so this file's own text
719
+ // carries no import of a path a scaffold's copy of it may not ship.
720
+ const IMPORT_LINE = ["import { magnetLanding } from ", JSON.stringify(`../${MAGNET_RENDERER_IMPORT}`), ";"].join("");
721
+ const PAIR_SRC = `// Generated by create-ailk add-route magnet.\n// ${MAGNET_PAGE_MARKER} — the landing of the /diagnostic magnet pair.\n${IMPORT_LINE}\n`;
722
+
723
+ it("exempts a page carrying the marker and the renderer import, at any route", () => {
724
+ expect(exemption("diagnostic", PAIR_SRC)).toBe(MAGNET_PAGE_REASON);
725
+ expect(exemption("diagnostic/start", PAIR_SRC)).toBe(MAGNET_PAGE_REASON);
726
+ expect(exemption("careers/writer", PAIR_SRC)).toBe(MAGNET_PAGE_REASON);
727
+ });
728
+
729
+ it("checks a page carrying the marker but no import of the renderer like any other", () => {
730
+ const noImport = `// ${MAGNET_PAGE_MARKER}\nexport default function Page() { return null; }\n`;
731
+ expect(exemption("diagnostic", noImport)).toBeUndefined();
732
+ });
733
+
734
+ it("checks a page importing the renderer without the marker like any other", () => {
735
+ const noMarker = `${IMPORT_LINE}\n`;
736
+ expect(exemption("diagnostic", noMarker)).toBeUndefined();
737
+ });
738
+ });
739
+
664
740
  // ── 3. The two defect shapes that shipped, pinned ───────────────────────────
665
741
  //
666
742
  // These prove the checks above reach the validator rule that rejects each
@@ -0,0 +1,37 @@
1
+ /**
2
+ * account-invite.ts — the /contact thank-you's account link (#8757 D8): the
3
+ * path that starts sign-in on this site with the submitter's email prefilled
4
+ * and lands on their account. It signs no one in; the email is only the
5
+ * sign-in form's starting value (NFR2).
6
+ *
7
+ * Built from the generated sign-in provider (`SIGN_IN_PROVIDER`):
8
+ * - `site` — this site's own `/sign-in`, locale-prefixed like every page
9
+ * (`localeHref`), with `next` = the locale's `/account` and `email`;
10
+ * - `hub` — a hub-sign-in spoke starts at `/api/hub-session/sign-in`
11
+ * (`accountInvitePath("hub", …)`), which hands the hint to the hub;
12
+ * - `none` — no sign-in, so no link.
13
+ * An email the hint schema refuses (`accountEmailHintSchema`) gives no link.
14
+ * The query is built with `URLSearchParams` (SEC-8757-L).
15
+ */
16
+ import { localeHref } from "@working-theory/ui";
17
+ import { accountEmailHintSchema, accountInvitePath } from "@working-theory/validation";
18
+
19
+ import { SIGN_IN_PROVIDER } from "./topology.generated";
20
+
21
+ export function accountInviteHref(locale: string, email: string): string | null {
22
+ const hint = accountEmailHintSchema.safeParse(email);
23
+ if (!hint.success) return null;
24
+ switch (SIGN_IN_PROVIDER) {
25
+ case "site": {
26
+ const query = new URLSearchParams({
27
+ next: localeHref(locale, "/account"),
28
+ email: hint.data,
29
+ });
30
+ return `${localeHref(locale, "/sign-in")}?${query.toString()}`;
31
+ }
32
+ case "hub":
33
+ return accountInvitePath("hub", hint.data);
34
+ default:
35
+ return null;
36
+ }
37
+ }
@@ -12,6 +12,12 @@
12
12
  * signed-in account area — `/account` and everything under it — which the
13
13
  * layout also renders without the marketing chrome.
14
14
  *
15
+ * `isBookingPath()` (#8843 D10) is the same predicate for the scheduling
16
+ * module's booking pages — `/meet`, `/meet/<slug>` and `/private`, never
17
+ * `/meet/confirmed` — which the layout renders chrome-free only where the
18
+ * module is installed (`lib/modules.generated.ts`). This module imports
19
+ * nothing, so edge code can import it too.
20
+ *
15
21
  * No Next.js import — pure functions, asserted directly by their own suite.
16
22
  */
17
23
 
@@ -62,6 +68,21 @@ export function isAccountPath(
62
68
  return firstSegment(pathname, locales) === "account";
63
69
  }
64
70
 
71
+ /**
72
+ * Whether `pathname` addresses a booking page: `/private`, `/meet`, or
73
+ * `/meet/<slug>` for any slug but `confirmed` (the booker's confirmation
74
+ * page, which keeps the marketing chrome). The same prefix rule and the same
75
+ * fail-safe direction as `isAuthPath()`.
76
+ */
77
+ export function isBookingPath(
78
+ pathname: string | undefined | null,
79
+ locales: readonly string[],
80
+ ): boolean {
81
+ const [first, second] = segments(pathname, locales);
82
+ if (first === "private") return true;
83
+ return first === "meet" && second !== "confirmed";
84
+ }
85
+
65
86
  /**
66
87
  * The first path segment after an enabled-locale prefix, or `undefined` for
67
88
  * an absent or empty pathname.
@@ -70,7 +91,18 @@ function firstSegment(
70
91
  pathname: string | undefined | null,
71
92
  locales: readonly string[],
72
93
  ): string | undefined {
73
- if (!pathname) return undefined;
94
+ return segments(pathname, locales)[0];
95
+ }
96
+
97
+ /**
98
+ * The path's segments after an enabled-locale prefix (empty for an absent or
99
+ * empty pathname). A trailing slash adds no segment.
100
+ */
101
+ function segments(
102
+ pathname: string | undefined | null,
103
+ locales: readonly string[],
104
+ ): string[] {
105
+ if (!pathname) return [];
74
106
 
75
107
  // Drop a query string / fragment — `x-pathname` carries the path, but a
76
108
  // caller passing a full request path should get the same verdict.
@@ -80,5 +112,7 @@ function firstSegment(
80
112
  ? path.replace(new RegExp(`^/(${locales.join("|")})(?=/|$)`), "")
81
113
  : path;
82
114
 
83
- return bare.split("/")[1];
115
+ const parts = bare.split("/").slice(1);
116
+ if (parts.at(-1) === "") parts.pop();
117
+ return parts;
84
118
  }
@@ -0,0 +1,16 @@
1
+ // checkout-config.ts — this spoke's checkout stamp, as data (#7838, #8748).
2
+ //
3
+ // Written once by create-ailk and yours from then on: `create-ailk update`
4
+ // never rewrites it. Keep it to data: no import, and no code. A spoke sells
5
+ // through its hub's Offerings buy route, /api/offerings/checkout/<lookup_key>
6
+ // (#9142); the thank-you page and, on a spoke that signs in through its hub,
7
+ // the billing action read `site` from here.
8
+ //
9
+ // site The paid-session stamp the thank-you page checks
10
+ // (`metadata.site`) before it prefills "Open your account". Never
11
+ // "default". The create-ailk writer stamps the line below.
12
+ const SITE = "example-site";
13
+
14
+ export const checkoutConfig = {
15
+ site: SITE,
16
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * consent-policies.ts — the one policy map an agreement surface links
3
+ * through (#8765 D9, spec docs/specs/8765-sign-up-refuses-without-the-spec.md).
4
+ *
5
+ * A consent statement names its links by key (`[Terms](terms)`); the keys
6
+ * resolve against the registry's `policies` map. This localizes that map, so
7
+ * the sign-up page, the account interstitial and the Privacy tab link Terms,
8
+ * Privacy Policy and Working Agreement to the same three pages `/contact`
9
+ * builds inline (#8761 decision, C6).
10
+ */
11
+ import { localeHref } from "@working-theory/ui";
12
+ import { SITE_CONSENT_CONFIG_DEFAULTS } from "@working-theory/validation";
13
+
14
+ /** The registry's policy map with each path localized for `locale`. */
15
+ export function consentPoliciesFor(locale: string): Record<string, string> {
16
+ return Object.fromEntries(
17
+ Object.entries(SITE_CONSENT_CONFIG_DEFAULTS.policies).map(([key, path]) => [
18
+ key,
19
+ localeHref(locale, path),
20
+ ]),
21
+ );
22
+ }
@@ -37,6 +37,7 @@ import { siteChrome, type SiteChrome } from "./site-chrome";
37
37
  export const DECK_ROUTES = [
38
38
  "/about",
39
39
  "/alternatives",
40
+ "/careers",
40
41
  "/contact",
41
42
  "/faq",
42
43
  "/launch-service",
@@ -55,6 +56,8 @@ export const DECK_ROUTES = [
55
56
  type Chrome = SiteChrome & {
56
57
  deckGrounds?: DeckPageInputs["deckGrounds"];
57
58
  deckPresentation?: true;
59
+ /** #8773 — present only when the design chooses bare slide anchors. */
60
+ deckAnchors?: "bare";
58
61
  };
59
62
  const chrome: Chrome = siteChrome;
60
63
 
@@ -180,6 +183,10 @@ async function inputsFor(ctx: DeckPageContext): Promise<DeckPageInputs | null> {
180
183
  return {
181
184
  ...(chrome.deckGrounds !== undefined ? { deckGrounds: chrome.deckGrounds } : {}),
182
185
  deckPresentation: chrome.deckPresentation === true,
186
+ // #8773 — a page that presents names its slides as the design chose.
187
+ ...(chrome.deckPresentation === true && chrome.deckAnchors === "bare"
188
+ ? { deckAnchorStyle: "bare" as const }
189
+ : {}),
183
190
  };
184
191
  }
185
192
 
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The shape a deliverable grant id must match before anything spends a hub
3
+ * call on it (#9125). One definition, read by the report page
4
+ * (`app/[locale]/reports/[slug]/page.tsx`, `resolveGrantId`) and its file
5
+ * route (`app/[locale]/reports/[slug]/file/route.ts`), so the page and the
6
+ * bytes can never disagree on which ids are worth probing.
7
+ *
8
+ * A grant id is minted as `dgrant_<uuidv7>` (database/inbox/schema.prisma).
9
+ * This is deliberately a charset-and-length check, not a `dgrant_` prefix
10
+ * match: the API stays the authority on which ids are real, and a future id
11
+ * scheme should not need an edit here. 128 is a generous ceiling over today's
12
+ * `dgrant_<uuid>`.
13
+ *
14
+ * Deliberately base-tree, not slice-owned: a keyed scaffold's
15
+ * signal-collectors bundle installs the page and route that import it, so it
16
+ * must already be in every scaffold.
17
+ */
18
+ export const GRANT_ID_SHAPE = /^[A-Za-z0-9_-]{1,128}$/;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * HubSignInGate (#7894 D10) as #8790 AC-1/AC-2 shape it: the site's inner-page
3
+ * header (back arrow + mark), a label-size heading, the one hub sign-in
4
+ * action, and a prospect line only when the site's messages carry it.
5
+ */
6
+ import { render, screen } from "@testing-library/react";
7
+ import en from "../../../messages/en.json";
8
+ import { HubSignInGate } from "../sign-in-gate";
9
+
10
+ type Gate = Record<string, string>;
11
+ let gateMessages: Gate;
12
+
13
+ jest.mock("next-intl/server", () => ({
14
+ __esModule: true,
15
+ getTranslations: jest.fn(async () => {
16
+ const t = (key: string) => gateMessages[key] ?? `copy:${key}`;
17
+ t.has = (key: string) => key in gateMessages;
18
+ return t;
19
+ }),
20
+ }));
21
+
22
+ const mockConfig = jest.fn();
23
+ // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; hubSessionConfig() is a pure env read
24
+ jest.mock("../config", () => ({
25
+ __esModule: true,
26
+ hubSessionConfig: () => mockConfig(),
27
+ }));
28
+
29
+ // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; siteLogo() reads the site's brand files
30
+ jest.mock("../../site-logo", () => ({
31
+ __esModule: true,
32
+ siteLogo: () => (
33
+ <img src="/logomark.svg" alt="Site" data-testid="site-mark" />
34
+ ),
35
+ }));
36
+
37
+ async function renderGate(pathname?: string): Promise<void> {
38
+ const requestHeaders = new Headers();
39
+ if (pathname) requestHeaders.set("x-pathname", pathname);
40
+ render(await HubSignInGate({ locale: "en", requestHeaders }));
41
+ }
42
+
43
+ beforeEach(() => {
44
+ gateMessages = { ...(en.account.gate as Gate) };
45
+ mockConfig.mockReset().mockReturnValue({ clientId: "spoke_1" });
46
+ });
47
+
48
+ describe("<HubSignInGate/> (#8790 AC-1)", () => {
49
+ it("wears the site's inner-page header: a back arrow to the home and the mark", async () => {
50
+ await renderGate("/account");
51
+ const header = screen.getByTestId("account-sign-in-gate-header");
52
+ const back = screen.getByTestId("nav-back-link");
53
+ expect(header).toContainElement(back);
54
+ expect(back).toHaveAttribute("href", "/");
55
+ expect(back).toHaveAccessibleName(en.account.gate.homeLink);
56
+ expect(screen.getByTestId("account-sign-in-gate-mark")).toContainElement(
57
+ screen.getByTestId("site-mark"),
58
+ );
59
+ });
60
+
61
+ it("sets the heading at the label size and offers the one sign-in action", async () => {
62
+ await renderGate("/account/settings");
63
+ const heading = screen.getByRole("heading", { level: 1 });
64
+ expect(heading).toHaveTextContent(en.account.gate.hubHeadline);
65
+ expect(heading).toHaveClass("text-heading-2");
66
+ expect(screen.getByTestId("account-hub-sign-in")).toHaveAttribute(
67
+ "href",
68
+ "/api/hub-session/sign-in?next=%2Faccount%2Fsettings",
69
+ );
70
+ });
71
+
72
+ it("renders no prospect line by default (the shipped messages carry no prospect keys)", async () => {
73
+ await renderGate();
74
+ expect(screen.queryByTestId("account-sign-in-gate-prospect")).toBeNull();
75
+ });
76
+ });
77
+
78
+ describe("<HubSignInGate/> prospect line (#8790 AC-2)", () => {
79
+ it("renders the line and its link when the site fills all three keys", async () => {
80
+ gateMessages.prospectPrompt = "New here?";
81
+ gateMessages.prospectLink = "See if we fit";
82
+ gateMessages.prospectHref = "/fit";
83
+ await renderGate();
84
+ const line = screen.getByTestId("account-sign-in-gate-prospect");
85
+ expect(line).toHaveTextContent("New here? See if we fit.");
86
+ expect(screen.getByRole("link", { name: "See if we fit" })).toHaveAttribute(
87
+ "href",
88
+ "/fit",
89
+ );
90
+ });
91
+
92
+ it("renders nothing when any of the three keys is absent", async () => {
93
+ gateMessages.prospectPrompt = "New here?";
94
+ gateMessages.prospectLink = "See if we fit";
95
+ await renderGate();
96
+ expect(screen.queryByTestId("account-sign-in-gate-prospect")).toBeNull();
97
+ });
98
+
99
+ it("keeps the not-configured state when the hub is not configured", async () => {
100
+ mockConfig.mockReturnValue(null);
101
+ await renderGate();
102
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
103
+ en.account.gate.emptyHeadline,
104
+ );
105
+ expect(screen.getByTestId("account-sign-in-gate-home")).toHaveAttribute(
106
+ "href",
107
+ "/",
108
+ );
109
+ expect(screen.queryByTestId("account-hub-sign-in")).toBeNull();
110
+ });
111
+ });