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
@@ -110,6 +110,26 @@ function — the page under test throws instead of taking a branch. Declare
110
110
  every predicate the pages call, binding the two reset ones to the same
111
111
  `jest.fn()` when the case does not need them to disagree.
112
112
 
113
+ **Sign-up agreements are refused and written on the server (#8765).**
114
+ `lib/sign-up-agreements.ts` is a Better Auth plugin plus two database hooks
115
+ that `auth.ts` installs. While `isSignUpOpen()`, `POST /api/auth/sign-up/email`
116
+ without every sign-up agreement in its `agreements` body key answers
117
+ `400 AGREEMENTS_REQUIRED` before the email lookup (so the answer never depends
118
+ on whether the address is taken), and `503 AGREEMENTS_UNAVAILABLE` when the
119
+ API's `GET /v1/consent/sign-up` evidence cannot be read (60 s fresh, 10 min
120
+ stale-on-error, one cache entry per enabled locale). An accepted sign-up — or
121
+ an OAuth sign-up whose state carries the card's agreements — writes one
122
+ `sign_up` grant per agreement through the auth client's transaction
123
+ (`prismaAdapter(…, { transaction: true })`), so the `User`, its `Account` and
124
+ the grants commit together or not at all; this hook is the ledger's only
125
+ writer outside `apps/api`'s `services/consent.ts`, and it writes only rows of
126
+ a user it is creating. A user created without agreements (a first social
127
+ sign-in from `/sign-in`) has none, and the account interstitial asks them
128
+ (`(authed)/account/layout.tsx`, which fails closed on any read but a 2xx or an
129
+ older hub's 404). The carried `__Host-ailk-signup-agreements` cookie is
130
+ retired. Policy links on every agreement surface come from
131
+ `lib/consent-policies.ts`'s `consentPoliciesFor(locale)`.
132
+
113
133
  ## Contact page
114
134
 
115
135
  The contact page (`apps/web/app/[locale]/contact/page.tsx`) renders the
@@ -123,10 +143,27 @@ the site's contact email, never a form that would report a message as sent
123
143
  when it reached nobody. A failed status read counts as accepting, so the
124
144
  form renders and its submit reports honestly.
125
145
 
146
+ **Every visible fact on the form comes from the magnet (#8847).** The page
147
+ reads the magnet through `lib/in-page-magnet.ts`, the in-page form shape:
148
+ its field step's `copy.title` is the page's H1 and `<title>` and its
149
+ `copy.body` the line under it (`framedSources` writes both into the first
150
+ `contact-form` section, and into the frontmatter title when that section
151
+ leads), and its `ending` is the success message (`{name}` filled from the
152
+ posted name). The section's own `submitLabel` and `successMessage` apply
153
+ only to its own fields. When the hub cannot be read the seeded
154
+ `contactFormFlowConfig` renders, so a fresh deploy looks the same. Under
155
+ the form one muted line reads "Prefer email?" (`contact.preferEmail`) and
156
+ the `_site.mdx` contact email, absent when the site has none. The default
157
+ Contact form's statement is the two-link "I accept the Terms and Privacy
158
+ Policy." (`CONTACT_FORM_TERMS_STATEMENT`); sign-up keeps the three-link
159
+ `ACCOUNT_TERMS_STATEMENT`.
160
+
126
161
  The form's fields come from the workspace's **Contact form magnet** (#8661):
127
- every workspace is seeded one, live at slug `contact`, and the page reads it
128
- on every request (`force-dynamic`) through the public flow read,
129
- `flows.get("contact")` — the API resolves the workspace (the site alias
162
+ every workspace is seeded one, live at slug `contact`, and the page is rendered
163
+ on every request (`force-dynamic`) and reads it through the public flow read,
164
+ `flows.get("contact")`, shared across requests for 15 seconds per server
165
+ instance (#8731) — so a burst of views is one hub read, and an Admin edit shows
166
+ within 15 seconds. The capture-status read is shared the same way. The API resolves the workspace (the site alias
130
167
  keyless, a spoke by its key), so the page names none. `contact-form-magnet.ts`
131
168
  maps the config's first step onto the block's fields, terms statement and
132
169
  submit label. The order is: a usable magnet (its first step has `name` and
@@ -135,11 +172,44 @@ declares any, else the built-in form, which is the seeded config itself. Any
135
172
  failed read (absent, draft, a Free build with no flow route, a throw) is the
136
173
  fallback, never an error. A magnet-driven post carries a hidden
137
174
  `source: "flow:contact"`, so it is an Inbox submission of the Contact form
138
- with every answer shown; a fallback post stays `web`. The terms statement
175
+ with every answer shown; a fallback post is `contact-form` (#8747). The terms statement
139
176
  links Terms, Privacy Policy and Working Agreement from a page-built
140
177
  `policies` map, and `captureLeadAction` refuses a post without
141
- `consent=true` (the intake excepted) and keeps only the sources `web`,
142
- `conversational-intake` and `flow:contact`.
178
+ `consent=true` whatever its source (#8730 D2 — the hidden `source` picks the
179
+ stamp, never the gate) and keeps only the contact sources
180
+ (`CONTACT_LEAD_SOURCES`: `contact-form`, `conversational-intake`,
181
+ `flow:contact`). An accepted post sends `consent: true` to the API, which
182
+ refuses a contact lead without it (`400 consent_required`, shown as the same
183
+ field error) and records `payload.consent` itself.
184
+
185
+ The action writes through `src/lib/lead-write.ts` (#8747), the site's lead
186
+ writer: `payload.site` is `AILK_LEAD_SITE` (none when unset) and
187
+ `payload.page` is `contact`, both spread last so no form field overrides them,
188
+ and no `siteId` is sent. Before the write it runs a per-visitor limit
189
+ (`src/lib/rate-limit.ts`, 30 posts per 10 minutes, per instance; skipped when
190
+ no client address is readable), because the hub sees only this site's egress
191
+ address. An API that cannot be reached — a
192
+ thrown submit, a 502 or a 504 — renders the contact email, like the 503
193
+ `lead_capture_not_configured`.
194
+
195
+ **The account link (#8757 D8, D9).** When the Contact form magnet is set to
196
+ `invite` in Admin, the API's 201 carries `next: { completion: "invite" }` and
197
+ the action answers `{ kind: "success", next: { href, label } }`: the success
198
+ alert gains "Track this in your account" (`contact.trackInAccount`, testid
199
+ `contact-success-next`), built by `lib/account-invite.ts` from
200
+ `SIGN_IN_PROVIDER` — `site` links `/sign-in?next=<locale>/account&email=…`,
201
+ `hub` links `/api/hub-session/sign-in?next=/account&email=…`, `none` (or an
202
+ email `accountEmailHintSchema` refuses) gives no link. Anything else,
203
+ `checkout` included (Phase 1), is exactly `{ kind: "success" }`. The link
204
+ signs no one in: `/sign-in` reads one valid `email` as the card's starting
205
+ value (`AuthCard`'s `defaultEmail`), the spoke's
206
+ `/api/hub-session/sign-in` forwards it as `login_hint`, and the hub's
207
+ authorize route carries a valid hint only onto its no-session `/sign-in`
208
+ redirect, never into a code, cookie or continuation ref (a repeated
209
+ `login_hint` is `400 invalid_request`). `email` and `login_hint` are in the
210
+ telemetry scrub lists (`lib/telemetry-scrub.ts`, `packages/analytics`).
211
+
212
+ **The same link on the magnet walk and /careers (#8771).** The careers apply page's action (`/careers/<slug>/apply` on a site without the walk, #8849) reads the API's `next` the same way (`apply.actions.ts`): `invite` returns the link and `contact.trackInAccount`, anything else — or a throw after the write — exactly `{ kind: "success" }`, so an error never invites a duplicate application. The walk (`/flows/<slug>`, `FlowStepperClient`) tracks the first `mapsTo: "email"` answer it submitted; on a completing response with `next: { completion: "invite" }` it asks the server action `accountInviteLinkAction` (the builder reads the generated topology, so it never runs in the client) and shows the link inside `FlowStepper`'s finished card (its `completedAction` slot), or — for a flow with a `thankYou` block — writes one `sessionStorage` item, `ailk:account-invite`, that `/thanks/<slug>`'s `AccountInviteLink` reads once and removes (the page passes it to `renderPage` as `trailing`, so it sits inside `<main>`). Each link sits in a `data-sentry-block` element (Session Replay records `href`s); no walk URL ever carries the email.
143
213
 
144
214
  A `contact-form` section in the `column` variant (the minimalism design's
145
215
  Contact page) renders one centred heading, one line and the form, and the
@@ -151,12 +221,298 @@ Send disabled until the box is ticked.
151
221
  The chat-style intake is not mounted by default.
152
222
  `ConversationalIntakeWrapper.tsx` remains for a site that prefers it: it hosts
153
223
  `@working-theory/ui`'s `ConversationalIntake` block (contract in
154
- `packages/ui/src/blocks/conversational-intake/README.md`), submits through the
155
- same `captureLeadAction`, and on Skip or a failed submit swaps to
224
+ `packages/ui/src/blocks/conversational-intake/README.md`), asks for the terms
225
+ in its own last turn (#8730 D2), submits through the same
226
+ `captureLeadAction` with `consent=true` only when the accept pill was tapped,
227
+ and on a declined terms turn, Skip or a failed submit swaps to
156
228
  `LeadCaptureForm`, prefilled with what was already captured. The canonical
157
229
  embedding for it is behind the hero CTA's lightbox gate
158
230
  (`packages/ui/src/sections/FormLightbox.tsx`, #4577), not a whole-page mount.
159
231
 
232
+ ## Newsletter, unsubscribe and the footer signup
233
+
234
+ The workspace's **Newsletter magnet** (#8847) — seeded live at slug
235
+ `newsletter` in every workspace — is the only source of `/newsletter`,
236
+ `/unsubscribe` and the footer signup; none of their copy is in page code,
237
+ content or the message catalog. All three read it through
238
+ `lib/in-page-magnet.ts`'s `readServedMagnet("newsletter")`, shared for 15
239
+ seconds per instance like /contact's read.
240
+
241
+ - **`/newsletter`** renders on every request. The magnet's step title is the
242
+ H1 and `<title>`, its body the line under it, and the standard
243
+ `NewsletterFormInline` (visible Email label, the consent row above the
244
+ field, the consent field's help line, Subscribe, and the magnet's ending in
245
+ place of the form) goes into the page's first `contact-form` section. With
246
+ the hub unreadable the seeded `newsletterFlowConfig` renders; with capture
247
+ off (`GET /v1/leads/capture`'s `newsletter`) the slot is `ContactFallback`
248
+ with the `_site.mdx` email. A page with no `contact-form` section keeps its
249
+ pre-#8847 composition.
250
+ - **The signup.** `subscribeNewsletterAction` refuses a post without
251
+ `consent=true`, then re-reads the magnet and submits its step through
252
+ `flows.submitStep` — the step key, the email field and the consent key
253
+ (`terms.newsletter`) all from the server's read, never the post — so the
254
+ hub writes the lead, the `newsletter` consent row and the welcome email.
255
+ Without a flow route (a Free build) it posts `leads.subscribe`, as before.
256
+ It lives in `lib/newsletter.actions.ts`, not under `app/[locale]/newsletter/`,
257
+ because the footer imports it and a scaffold with the `newsletter` surface
258
+ off drops that route directory.
259
+ - **`/unsubscribe`** takes its title, line, button and done message from the
260
+ magnet's `unsubscribe` block (strings only across the island edge); the
261
+ already, invalid and unavailable lines stay in the `unsubscribe` catalog.
262
+ - **The footer** fills `Footer`'s `subscribe` slot with the compact form
263
+ (stacked, `sr-only` label) only when the magnet is served and capture is
264
+ on; the built-in config never fills it.
265
+
266
+ ## Careers
267
+
268
+ Three linked pages, each built from the `content/<locale>/JobPosting/*.mdx`
269
+ records (#8849):
270
+
271
+ - **`/careers`** renders in sections mode, the /alternatives pattern: one
272
+ generated `items` section first, a card per posting (newest `publishedAt`
273
+ first), then the seed's declared sections, so the card grid and the
274
+ `CollectionPage`'s `ItemList` (`ListItem`s with `position` and `url`) agree.
275
+ The section's `filter` gives a search box and employment-type and location
276
+ selects; the facets read the postings' first-class `employmentType` and
277
+ `location` frontmatter, never `schema.mainEntity`. Every posting stays in the
278
+ server HTML. Every visible string is a `careers` message.
279
+ - **`/careers/<slug>`** keeps its chrome and ends with an Apply button
280
+ (`job-apply-cta`, `Button` size `big`) to `/careers/<slug>/apply`; its
281
+ `ApplyAction` targets the same URL. It makes no hub read.
282
+ - **`/careers/<slug>/apply`** is the `signal-collectors` walk where the module
283
+ is present (§ Magnet pages), else the Free apply page,
284
+ `careers/[slug]/[action]/page.tsx`: the posting's title over the one-step
285
+ `LeadCaptureForm` bound to `apply.actions.ts` with the slug as `job`,
286
+ `noindex, follow` and full-screen. Next.js serves the static `apply`
287
+ segment first, the `[slug]/[detail]` idiom; every other action 404s. Lead
288
+ capture is never-gate, so every tier takes an application. Both apply pages
289
+ read the posting's title through `lib/posting-title.ts`.
290
+
291
+ ## Meeting pages
292
+
293
+ The scheduling module's booking pages (#8843) read every meeting fact from
294
+ the workspace — no content file, env var or deploy sits between a Meetings or
295
+ Cal.com edit and the page.
296
+
297
+ - **One builder.** `lib/meetings.tsx` reads `GET /v1/schedule/meetings`
298
+ through `schedule.meetings()`, re-parses the body with `MeetingListSchema`
299
+ and shares the result per server instance for 15 seconds (concurrent and
300
+ failed reads too, the contact page's #8731 shape). Any failure is
301
+ `{ ok: false }`. It builds the index cards (`meetingCardsSection`: `items`
302
+ tiles, two columns, `gapAbove: "tight"`, one card per bookable meeting with
303
+ the hub's title and description and a "Book <n> min" button) and the
304
+ meeting page's booker (`meetingPageSections`: `scheduler-embed`, provider
305
+ `cal`, month view). Every visible string is a `schedule` message.
306
+ - **`/meet` and `/private`** both return `MeetingIndex`
307
+ (`app/[locale]/meet/meeting-index.tsx` — beside the route, because it
308
+ renders the paid `BookingProfileHeader`, which `architecture.yaml` admits
309
+ only there): the back link alone (`Nav` `bar="bare"` `bareMark={false}`),
310
+ the header's avatar and one H1, then the cards of that visibility. One
311
+ meeting redirects to `/meet/<slug>`; none, or a failed read, shows the
312
+ site's contact email — never a 404 or a 500. `/private` is
313
+ `noindex, nofollow`, has no seed and no nav entry: private means unlisted,
314
+ not access-controlled.
315
+ - **`/meet/<slug>`** is one centred H1 ("Schedule a <title> meeting"),
316
+ Cal.com's inline embed from the hub's Cal.com link, and "← All meeting
317
+ types". A Live meeting that cannot be booked, or a failed read for a valid
318
+ slug, shows the contact email; an unknown slug or a dotted one is a 404. A
319
+ `BookCall/<slug>.mdx` seed gives only the URL, the JSON-LD inputs and the
320
+ fallback title — its sections are never rendered here.
321
+ - **Chrome.** `lib/modules.generated.ts`'s `SCHEDULING_PAGES` (written by
322
+ create-ailk from `apps/web/package.json`, never copied by `update`) and
323
+ `lib/auth-routes.ts`'s `isBookingPath` make the locale layout drop the
324
+ marketing shell on `/meet`, `/meet/<slug>` and `/private`; `/meet/confirmed`
325
+ and a site without the module keep it. The consent banner stays.
326
+ - **The CSP (D12).** `security-headers.config.ts` exports `SCHEDULING_CSP`
327
+ (Cal.com's app origin in `script-src` and `connect-src`; `frame-src`
328
+ `'self'` and the same origin) and a frozen `SCHEDULING_SECURITY_HEADERS`.
329
+ `middleware.ts` applies it instead of `DEFAULT_SECURITY_HEADERS` only when
330
+ `SCHEDULING_PAGES && isBookingPath(pathname, enabledLocales)`; sign-in,
331
+ account, checkout, `/meet/confirmed` and every site without the module keep
332
+ the default policy. `architecture.yaml`'s `cal\.com` rule excludes the
333
+ config, the middleware and their two tests for this reason.
334
+
335
+ ## Magnet pages
336
+
337
+ Every multi-step magnet is a page pair (#8840), rendered by one module,
338
+ `app/[locale]/flows/[slug]/magnet-page.tsx`:
339
+
340
+ - **`/<magnet>`** — the indexable landing, inside the site's Nav and Footer: a
341
+ centred hero of the flow record's `landing` copy (title, line, muted meta
342
+ line, button to `/<magnet>/start`; absent keys fall back to step 1's title
343
+ and the `flow` messages) and the `Assessment` page type's `WebApplication`
344
+ JSON-LD.
345
+ - **`/<magnet>/start`** — the walk, `noindex, follow`, full-screen with the
346
+ back arrow alone (`NavBackLink`) above step 1. `lib/magnet-routes.ts`'s
347
+ `isMagnetWalkPath` is what the locale layout drops the Nav and Footer on:
348
+ any path ending in `start`, plus `/careers/apply` and
349
+ `/careers/<posting>/apply`. The cookie consent banner stays.
350
+ - **The ending.** A flow with an `ending` ends on it, never on `thankYou`:
351
+ `FlowStepperClient` picks the outcome from the walk's answers
352
+ (`selectFlowEndingOutcome`) — a screen in the finished card with `{name}`
353
+ filled, or a redirect to a site path — and re-checks every destination
354
+ against `flowSitePathSchema` before it renders or navigates.
355
+ - **The hub is down.** Both pages read the flow through one `flows.get`,
356
+ shared per instance for 15 seconds per slug; a refused connection or a 5xx
357
+ shows `ContactFallback` with the site's contact email (both pages
358
+ `noindex`), and a 404 is `notFound()`. `/careers/apply` exists apart from
359
+ its flow, so it passes `renderMagnetStart` a `title`: a 404, a refused
360
+ connection or a 5xx all render the contact email under that `<h1>`
361
+ (#9219).
362
+ - **The pairs.** `/flows/<slug>` is the canonical pair. `/careers/apply` and
363
+ `/careers/<posting>/apply` walk the Job application magnet (the second
364
+ titles step 1 with the posting and sends its slug as the hidden `job`).
365
+ Any other pair is written by `create-ailk add-route magnet --path <path>
366
+ --flow <slug>`: two short pages carrying the `Standard magnet page
367
+ (#8840)` marker, which `lib/__tests__/structured-data.test.tsx` exempts
368
+ from its `renderPage` check. A site adds a magnet by binding a pair to a
369
+ slug, never by copying stepper code.
370
+ - **The recommender's landing (#9071).** A recommending flow with an offer
371
+ carrying `lookupKey` (an Offerings recommender, such as the standard
372
+ `recommender` magnet the Admin's Offerings page opens) never renders its
373
+ results page: the results permalink
374
+ (`flows/[slug]/results/[payload]/page.tsx`) redirects a completed walk to
375
+ `/<locale>/pricing?plan=<plan>` while the first fit's lookup key is in the
376
+ current shown set, else to `/<locale>/pricing` with no badge (no fit, a key
377
+ no longer shown, any failed read, or no answer within 5 seconds). The
378
+ shown-set read is `offerings.shown()`, `/pricing`'s own, shared per
379
+ instance for 15 seconds; `?plan=` carries the key's `{plan}` part, the only
380
+ value `/pricing` reads. A flow with no such offer, or whose first fit
381
+ carries none, renders the results page as before, with no read.
382
+ - **The deliverable pages (#8848).** A flow whose served config carries a
383
+ `deliverable` block (a `file`, `video`, `page` or `external` payload, gated
384
+ or not) has one route, `/reports/<slug>`, rendered by
385
+ `app/[locale]/reports/deliverable-page.tsx`; both halves of every other pair
386
+ redirect a deliverable flow there. Gated: the page is a centred hero of the
387
+ block's copy with the back arrow to `/reports` and a button to the walk at
388
+ `/reports/<slug>/start`, and the walk's finished card shows the deliverable
389
+ after the ending screen — the reader link `/reports/<slug>?g=<grant>` (in a
390
+ `data-sentry-block` element), an inline player, a site link or a link out;
391
+ a redirect ending never runs when the response carries one, and a file walk
392
+ that minted no grant says the link is in the email. Ungated: the reader over
393
+ the site's own `/reports/<slug>/file` route handler (it reads the hub's
394
+ `GET /v1/deliverables/open/:slug` with the spoke key and the visitor
395
+ forward, 30 reads per visitor per 10 minutes — skipped when no client
396
+ address is readable — the five file headers passed through), a player, a redirect to the site page, or a button out. Every
397
+ page under `/reports/<slug>` is chrome-free (`isMagnetWalkPath`); `/reports`
398
+ keeps its chrome. The reader's Share is built from the page URL, never the
399
+ bytes or `?g=`. A hub read that fails falls back to the slug's `ReportPage`
400
+ content. With none, #8848 D6 holds for a configured hub: a 5xx or a refused
401
+ connection shows the contact email (`noindex`). A site with no hub to ask —
402
+ the read answers 503 `flows_not_configured`, or no API origin
403
+ (`AILK_API_URL`, `NEXT_PUBLIC_AILK_API_URL`, `NEXT_PUBLIC_API_URL`) is set —
404
+ reads `not-configured` (set once in `readMagnet`), and the slug 404s, as
405
+ does its `/start` (#9219). `/downloads` and `/downloads/<slug>` are
406
+ permanent redirects into `/reports` (signal-collectors files).
407
+
408
+ ## Pricing page
409
+
410
+ The pricing page (`apps/web/app/[locale]/pricing/page.tsx`) renders the
411
+ workspace's Offerings shown set from Stripe (#8927). Before rendering it
412
+ reads `GET /v1/content/shown-offerings` through `offerings.shown()` from
413
+ `@working-theory/api-client`, renders on every request (`force-dynamic`),
414
+ and shares the read across requests for 15 seconds per server instance, the
415
+ contact page's window and reason: a product switched off disappears within
416
+ 15 seconds, with no deploy. The API resolves the workspace (a spoke by its
417
+ key), so the page names none. It renders one of three pages:
418
+
419
+ - **Unconnected** — the route answers `unconnected`, or the deployment runs
420
+ in production with none of the API origin variables `getApiBase()` reads:
421
+ today's MDX page through `renderPage`, byte for byte, `?plan=` matching
422
+ authored plan keys as #8842 made it.
423
+ - **A current shown set** — `lib/offerings-pricing.ts` maps it to a plans
424
+ fragment and an add-ons fragment. The first `pricing` section that is not
425
+ `addons` becomes the Stripe plans, and the first `addons` one the Stripe
426
+ add-ons, each keeping only its authored section copy (eyebrow, headline,
427
+ description and layout knobs), never an authored plan or price. Every other
428
+ `pricing` section is removed; a fragment with no section to replace goes
429
+ after a leading hero. `?plan=` moves the recommendation only for a
430
+ `{plan}`-grammar value (`LOOKUP_KEY_PLAN_PATTERN`), so `?plan=Growth` keeps
431
+ the workspace's own. Copy comes from the `pricingOfferings` namespace.
432
+ - **Anything else** — an expired or empty shown set, `empty`, a non-2xx, an
433
+ unknown body or a throw: every `pricing` section gives way to one
434
+ `contact-form` section holding `ContactFallback` with the site's contact
435
+ email (`readSiteContactEmail()`), in the `contact` namespace's copy. Never
436
+ an error page, and no `Offer` is published.
437
+
438
+ Stripe text reaches the page only as React text and JSON-LD only through
439
+ `renderPage`'s serializer; the title and meta stay the MDX page's.
440
+
441
+ Every Stripe card links `/api/offerings/checkout/<lookup_key>`, which
442
+ `app/api/offerings/checkout/[lookupKey]/route.ts` serves (#9051). Only a
443
+ buyer's page load starts anything: a request that is not a top-level
444
+ navigation (`Sec-Fetch-Mode: navigate`, `Sec-Fetch-Dest: document`), a
445
+ `HEAD`, a prefetch, an RSC fetch or a cross-site load is refused before any
446
+ call. A key `parseLookupKey` refuses, a site without the `commerce` claim, and
447
+ production with no API origin are refused locally too. The route then reads
448
+ the shown set, shared per instance for 15 seconds like the page's own read,
449
+ and goes on only when the site is connected and that set shows the key, so an
450
+ unconnected site never reaches the API's env path. Each visitor address gets 5
451
+ starts per 10 minutes per instance. The route POSTs `/v1/checkout/sessions`
452
+ through `checkout.createSession` with the visitor's `x-forwarded-for`, success
453
+ URL `/purchase/success?session_id={CHECKOUT_SESSION_ID}` and cancel URL
454
+ `/pricing`, both on the origin of `resolveBaseUrl()` and never the request's
455
+ host, and answers a `303` to the `https:` Checkout URL the API returns. Every
456
+ refusal is a `303` to `/pricing` with no body, `Cache-Control: no-store` and
457
+ one `[offerings-checkout] refused reason=<reason>` warning; no response or
458
+ log line carries the key. It sends `OSS_API_INTERNAL_SECRET` as
459
+ `x-internal-auth` only from a site that is not a spoke and only to an
460
+ `https:` or loopback API, so the hub's own `apps/web` must hold the API's
461
+ value when the API sets it. A spoke never holds that secret: it sells only
462
+ when its hub lists the spoke's origin in `AILK_TRUSTED_ORIGINS` and does not
463
+ set `OSS_API_INTERNAL_SECRET`, and otherwise every spoke buy returns to
464
+ `/pricing`.
465
+
466
+ An existing scaffold adopts the new page by bringing
467
+ `lib/offerings-pricing.ts`, `app/api/offerings/checkout/[lookupKey]/route.ts`
468
+ (it sits under `app/api/`, outside the route registry `sync-routes` copies),
469
+ the `pricingOfferings` namespace and an
470
+ `@working-theory/api-client` release that exports `offerings` with it, and
471
+ needs an API (its own `apps/api`, or a spoke's hub) that serves
472
+ `GET /v1/content/shown-offerings` (#8924): an older API answers that path
473
+ non-2xx, which renders the contact email in place of the authored pricing.
474
+
475
+ ## Spoke checkout
476
+
477
+ **A spoke sells through the Offerings buy route, and only through it.**
478
+ `app/api/offerings/checkout/[lookupKey]/route.ts` (§ Pricing page) is the one
479
+ selling path on every site: the seller's Stripe account is held once per
480
+ Organization on the hub, and the hub authorizes each buy against the
481
+ workspace's shown set. The spoke's legacy env-key checkout — its own buy route
482
+ (`app/api/checkout/[offering]/route.ts`), its Stripe webhook route
483
+ (`app/api/webhooks/stripe/route.ts`), `lib/checkout.ts` (the fulfilment) and
484
+ the generated checkout e2e workflow — is retired (#9142). `create-ailk
485
+ sync-routes --apply` removes them from an existing spoke when they are still
486
+ the copies create-ailk wrote, and names any it leaves.
487
+
488
+ What a spoke that carries `commerce` still gets (#8748), kept current by
489
+ `create-ailk update`; a hub, or a spoke without `commerce`, never gets them:
490
+
491
+ - **The two return pages.** `app/[locale]/checkout/thank-you/page.tsx` and
492
+ `app/[locale]/checkout/unavailable/page.tsx`. Both call `notFound()` unless
493
+ `lib/topology.generated.ts`'s `TOPOLOGY` is `"spoke"`, so this monorepo's
494
+ hub-shaped `apps/web` serves neither.
495
+ - **One data file.** `lib/checkout-config.ts` is the site's stamp,
496
+ `checkoutConfig.site`, and nothing else: no import and no code.
497
+ `create-ailk update` never rewrites it.
498
+ - **One package reader.** The thank-you page may import
499
+ `@working-theory/spoke-checkout` for `readPaidCheckoutEmail` alone
500
+ (`architecture.yaml`, #8757 D13): it reads `session_id`, and for a paid
501
+ session of this site under 24 hours old shows "Open your account" with the
502
+ buyer's email (`accountInviteHref`), else the same link with no email. Each
503
+ visitor gets 10 reads per 10 minutes (`src/lib/rate-limit.ts`); the page
504
+ sends no referrer, renders dynamically and never touches a cookie. The read
505
+ admits the spoke's `STRIPE_SECRET_KEY` from `process.env`; that key never
506
+ sells.
507
+ - **The billing action (a spoke that signs in through its hub).** create-ailk
508
+ renders `app/[locale]/(authed)/account/billing/spoke-billing.action.ts`,
509
+ which builds the package's billing reads from `checkoutConfig.site` and
510
+ `resolveBaseUrl()`; the spoke's own `architecture.yaml` admits it as the
511
+ package's other importer.
512
+ - **No `Idempotency-Key`.** No `apps/web` file sends one
513
+ (`tests/architecture/idempotency-key-producers.test.ts`): the fulfilment that
514
+ did is retired with the legacy checkout.
515
+
160
516
  ## The same-origin relay (`/v1/*`)
161
517
 
162
518
  A visitor's browser never calls the hub. Lead capture, newsletter signup, callback, scheduling and the commerce invoice call post to this site's own `/v1/...` address, and `app/v1/[...path]/route.ts` hands each request to `lib/relay.ts` (`handleRelay`), which forwards it server-side through `@working-theory/api-client`'s `relayToApi` with the site's own `AILK_SPOKE_KEY` when it has one (#7644). The browser never holds the key, so a hub can refuse unknown callers (#7633) without taking a site's forms down.
@@ -183,4 +539,32 @@ A visitor's browser never calls the hub. Lead capture, newsletter signup, callba
183
539
  - Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
184
540
  - Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
185
541
 
186
- Last updated: 2026-10-14 — § Contact page: the Contact form magnet read, the fallback order and the `column` look (#8661).
542
+ Last updated: 2026-10-19 — § Magnet pages: `/careers/apply` renders the contact email under its own `<h1>` when its flow is absent or the hub cannot answer; `readMagnet`'s `not-configured` kind 404s an unknown `/reports/<slug>` (and its `/start`) on a site with no hub to ask, while a configured hub that is down keeps #8848 D6's contact email (#9219) — verified against `app/[locale]/flows/[slug]/magnet-page.tsx`, `app/[locale]/careers/apply/page.tsx`, `app/[locale]/reports/deliverable-page.tsx` and `app/[locale]/reports/[slug]/{page,start/page}.tsx`.
543
+
544
+ Prior: 2026-10-18 — § Spoke checkout: the Offerings buy route is the one selling path, and the legacy env-key buy route, webhook route, `lib/checkout.ts` and checkout e2e workflow are retired (#9142) — verified against `app/api/offerings/checkout/[lookupKey]/route.ts`, `app/[locale]/checkout/*/page.tsx`, `lib/checkout-config.ts` and create-ailk's `spoke-checkout.ts` and `sync-routes.ts`.
545
+
546
+ Prior: 2026-10-18 — § Magnet pages: the recommender's landing on `/pricing?plan=` (#9071) — verified against `app/[locale]/flows/[slug]/results/[payload]/page.tsx` and its `landing.test.tsx`.
547
+
548
+ Prior: 2026-10-18 — § Magnet pages: the deliverable pages at `/reports/<slug>`, their ending, the file proxy and the `/downloads` redirects (#8848) — verified against `app/[locale]/reports/{deliverable-page.tsx,[slug]/page.tsx,[slug]/start/page.tsx,[slug]/file/route.ts}`, `app/[locale]/flows/[slug]/{magnet-page,FlowStepperClient}.tsx`, `app/[locale]/downloads/` and `lib/magnet-routes.ts`.
549
+
550
+ Prior: 2026-10-18 — § Contact page and § Newsletter, unsubscribe and the footer signup: the in-page form shape — every visible fact from the Contact form and Newsletter magnets (#8847) — verified against `lib/in-page-magnet.ts`, `app/[locale]/{contact,newsletter,unsubscribe}/page.tsx`, `lib/newsletter.actions.ts` and `app/[locale]/layout.tsx`.
551
+
552
+ Prior: 2026-10-18 — § Careers: the searchable /careers, the posting's Apply button and the Free apply page (#8849) — verified against `app/[locale]/careers/{page,[slug]/page,[slug]/[action]/page}.tsx` and `lib/posting-title.ts`; § Pricing page: the Offerings buy route `app/api/offerings/checkout/[lookupKey]/route.ts` starts the hosted Checkout for a shown key and returns every refusal to `/pricing`, and the spoke precondition (#9051) — verified against the route, its suite and `lib/offerings-pricing.ts`.
553
+
554
+ Prior: 2026-10-18 — § Meeting pages: the booking pages carry the Cal.com CSP (`SCHEDULING_CSP`, applied by `middleware.ts` on booking paths only) and `scheduler-embed` forwards `gapAbove` (#8843 D12, D13) — verified against `security-headers.config.ts`, `middleware.ts` and `packages/ui/src/sections/SchedulerEmbed.tsx`.
555
+
556
+ Prior: 2026-10-18 — § Meeting pages: the booking pages read the workspace's meetings through one builder, the chrome-free booking branch and its module flag (#8843) — verified against `lib/meetings.tsx`, `app/[locale]/meet/`, `app/[locale]/private/page.tsx`, `lib/auth-routes.ts` and `app/[locale]/layout.tsx`.
557
+
558
+ Prior: 2026-10-18 — § Magnet pages: the standard magnet page pair, its walk predicate, ending, hub-down fallback and `add-route magnet` (#8840) — verified against `app/[locale]/flows/[slug]/magnet-page.tsx`, `FlowStepperClient.tsx`, `lib/magnet-routes.ts`, `app/[locale]/layout.tsx` and `app/[locale]/careers/{apply,[slug]/apply}/page.tsx`.
559
+
560
+ Prior: 2026-10-18 — § Pricing page: `/pricing` renders the Offerings shown set, the contact email when there is none to sell, and today's page for an unconnected site (#8927) — verified against `app/[locale]/pricing/page.tsx` and `lib/offerings-pricing.ts`.
561
+
562
+ Prior: 2026-10-16 — § Spoke checkout: the purchase lead's `termsAccepted` key, the stamp of the terms the buyer accepted, from which the hub writes the checkout consent grant (#8778) — verified against `lib/checkout.ts` and `packages/spoke-checkout/src/webhook.ts`.
563
+
564
+ Prior: 2026-10-16 — § Spoke checkout: the thank-you page reads the paid session's email for "Open your account" (#8757 D13, #8800) — verified against `app/[locale]/checkout/thank-you/page.tsx` and `packages/spoke-checkout/src/paid-email.ts`.
565
+
566
+ Prior: 2026-10-15 — § The `(auth)` route group: sign-up agreements are refused and written on the server, the carried cookie is retired, and the account interstitial asks a user with no current grant (#8765) — verified against `lib/sign-up-agreements.ts`, `auth.ts`, `lib/consent-policies.ts` and `app/[locale]/(authed)/account/layout.tsx`.
567
+
568
+ Prior: 2026-10-15 — § Spoke checkout: the standard spoke checkout files, the one data file, the 404 and the fulfilment's one-key rule (#8748) — verified against `app/api/checkout/[offering]/route.ts`, `app/api/webhooks/stripe/route.ts`, `app/[locale]/checkout/*/page.tsx`, `lib/checkout.ts` and `lib/checkout-config.ts`.
569
+
570
+ Prior: 2026-10-15 — § Contact page: the account link an `invite` magnet adds, and the sign-in email hint (#8757).
@@ -24,10 +24,14 @@ const catalog = msgs as unknown as Record<string, Record<string, string>>;
24
24
  // getTranslations({ namespace, locale }) — renderPage (post-migration)
25
25
  export const getTranslations = async (
26
26
  arg: string | { namespace?: string; locale?: string },
27
- ): Promise<(key: string) => string> => {
27
+ ): Promise<((key: string) => string) & { has: (key: string) => boolean }> => {
28
28
  const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
29
29
  const messages = catalog[ns];
30
- return (key: string): string => messages?.[key] ?? `${ns}.${key}`;
30
+ const t = (key: string): string => messages?.[key] ?? `${ns}.${key}`;
31
+ // next-intl's `t.has` — an optional catalog key (#8790's prospect line) is
32
+ // present only when the site added it.
33
+ t.has = (key: string): boolean => messages?.[key] !== undefined;
34
+ return t;
31
35
  };
32
36
 
33
37
  export const getRequestConfig = (fn: unknown): unknown => fn;
@@ -141,9 +141,9 @@ describe("the (auth) layout", () => {
141
141
  expect(document.querySelector(".ailk-footer")).not.toBeInTheDocument();
142
142
  });
143
143
 
144
- it("renders the brand mark and the slim footer's three links", async () => {
144
+ it("renders no brand row (the mark is in each card, #8790 D3) and the slim footer's three links", async () => {
145
145
  await renderLayout();
146
- expect(screen.getByTestId("auth-shell-brand")).toBeInTheDocument();
146
+ expect(screen.queryByTestId("auth-shell-brand")).not.toBeInTheDocument();
147
147
  const footer = screen.getByTestId("auth-shell-footer");
148
148
  expect(footer.querySelectorAll("a")).toHaveLength(3);
149
149
  expect(
@@ -193,9 +193,10 @@ describe("the moved sign-in card", () => {
193
193
  expect(screen.queryByTestId("auth-forgot")).not.toBeInTheDocument();
194
194
  });
195
195
 
196
- it("renders no logo inside the card — the mark is the shell's", async () => {
196
+ it("renders the site's mark inside the card, not in the shell (#8790 D3)", async () => {
197
197
  await renderPage(SignInPage);
198
- expect(screen.queryByTestId("auth-card-logo")).not.toBeInTheDocument();
198
+ expect(screen.getByTestId("auth-card-logo")).toBeInTheDocument();
199
+ expect(screen.queryByTestId("auth-shell-brand")).not.toBeInTheDocument();
199
200
  });
200
201
  });
201
202
 
@@ -32,12 +32,14 @@ import {
32
32
  type AuthSubmitData,
33
33
  } from "@working-theory/ui/blocks";
34
34
  import { createAuthClient } from "better-auth/react";
35
- import type { ReactElement } from "react";
35
+ import type { ReactElement, ReactNode } from "react";
36
36
  import { useState } from "react";
37
37
 
38
38
  const authClient = createAuthClient();
39
39
 
40
40
  export interface ForgotPasswordCardProps {
41
+ /** The site's mark, centred in the card at 48px (#8790 D3). */
42
+ logo?: ReactNode;
41
43
  /** Absolute same-origin URL of `/reset-password`, resolved server-side. */
42
44
  resetUrl: string;
43
45
  /** Href for the card footer's "Back to sign in". */
@@ -53,6 +55,7 @@ export function ForgotPasswordCard({
53
55
  signInHref,
54
56
  labels,
55
57
  errorLabels,
58
+ logo,
56
59
  }: ForgotPasswordCardProps): ReactElement {
57
60
  const [sent, setSent] = useState(false);
58
61
  const [sentTo, setSentTo] = useState("");
@@ -85,6 +88,7 @@ export function ForgotPasswordCard({
85
88
  return (
86
89
  <div className="flex flex-col gap-4" data-testid="forgot-password-page">
87
90
  <AuthCard
91
+ {...(logo !== undefined ? { logo } : {})}
88
92
  intent={sent ? "sent" : "forgot"}
89
93
  signInHref={signInHref}
90
94
  labels={labels}
@@ -40,6 +40,7 @@ import type { ReactElement } from "react";
40
40
  import { resetSendConfigured } from "../../../../lib/auth-env";
41
41
 
42
42
  import { ForgotPasswordCard } from "./forgot-password-card";
43
+ import { siteLogo } from "../../../../lib/site-logo";
43
44
 
44
45
  interface Props {
45
46
  params: Promise<{ locale: string }>;
@@ -100,6 +101,7 @@ export default async function ForgotPasswordPage({
100
101
 
101
102
  return (
102
103
  <ForgotPasswordCard
104
+ logo={siteLogo()}
103
105
  resetUrl={resetUrl}
104
106
  signInHref={signInHref}
105
107
  labels={labels}
@@ -24,7 +24,6 @@ import { headers } from "next/headers";
24
24
  import { getTranslations } from "next-intl/server";
25
25
  import type { ReactElement, ReactNode } from "react";
26
26
 
27
- import { siteLogo } from "../../../lib/site-logo";
28
27
 
29
28
  interface Props {
30
29
  children: ReactNode;
@@ -51,19 +50,9 @@ export default async function AuthLayout({
51
50
 
52
51
  return (
53
52
  <AuthShell
54
- brand={
55
- /*
56
- * The mark is clamped to 28px the way `BrandLogo` clamps it inside
57
- * Nav: `siteLogo()` emits an <img> with a height ATTRIBUTE and no
58
- * width, and an SVG with no intrinsic size then stretches to the
59
- * container — it rendered 48 x 350 here before the clamp. The shell
60
- * owns the row's geometry; the site owns its mark's size.
61
- */
62
- <div className="inline-flex h-7 items-center [&>img]:h-full [&>img]:w-auto [&>img]:object-contain">
63
- {siteLogo()}
64
- </div>
65
- }
66
- brandHref={localeHref(locale, "/")}
53
+ // No brand row (#8790 D3): every auth card now carries the site's mark
54
+ // centred at 48px (`AuthCard`'s `logo` slot), so the shell's top-start
55
+ // corner is free for a page's own affordance (the sign-in's back link).
67
56
  footerLinks={footerLinks}
68
57
  languageSwitcher={
69
58
  <LocaleSwitcherNav
@@ -39,6 +39,7 @@ import type { ReactElement } from "react";
39
39
  import { resetSendConfigured } from "../../../../lib/auth-env";
40
40
 
41
41
  import { ResetPasswordCard } from "./reset-password-card";
42
+ import { siteLogo } from "../../../../lib/site-logo";
42
43
 
43
44
  interface Props {
44
45
  params: Promise<{ locale: string }>;
@@ -122,6 +123,7 @@ export default async function ResetPasswordPage({
122
123
 
123
124
  return (
124
125
  <ResetPasswordCard
126
+ logo={siteLogo()}
125
127
  token={token}
126
128
  signInHref={signInHref}
127
129
  labels={labels}
@@ -23,12 +23,14 @@ import {
23
23
  type AuthSubmitData,
24
24
  } from "@working-theory/ui/blocks";
25
25
  import { createAuthClient } from "better-auth/react";
26
- import type { ReactElement } from "react";
26
+ import type { ReactElement, ReactNode } from "react";
27
27
  import { useState } from "react";
28
28
 
29
29
  const authClient = createAuthClient();
30
30
 
31
31
  export interface ResetPasswordCardProps {
32
+ /** The site's mark, centred in the card at 48px (#8790 D3). */
33
+ logo?: ReactNode;
32
34
  /** The `?token=` Better Auth redirected back with. May be empty. */
33
35
  token: string;
34
36
  /** Where a completed reset lands. */
@@ -60,6 +62,7 @@ export function ResetPasswordCard({
60
62
  labels,
61
63
  errorLabels,
62
64
  navigate = defaultNavigate,
65
+ logo,
63
66
  }: ResetPasswordCardProps): ReactElement {
64
67
  const [errors, setErrors] = useState<{
65
68
  password?: string;
@@ -104,6 +107,7 @@ export function ResetPasswordCard({
104
107
  return (
105
108
  <div className="flex flex-col gap-4" data-testid="reset-password-page">
106
109
  <AuthCard
110
+ {...(logo !== undefined ? { logo } : {})}
107
111
  intent="reset"
108
112
  signInHref={signInHref}
109
113
  labels={labels}