create-ailk 0.22.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 (238) hide show
  1. package/component-catalog.json +149 -5
  2. package/component-catalog.md +7 -6
  3. package/dist/add-module.js +11 -9
  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 +7 -5
  8. package/dist/derive-architecture.js +9 -32
  9. package/dist/lib/design-checks.js +4 -2
  10. package/dist/lib/env-catalog.js +0 -4
  11. package/dist/lib/npm-auth.d.ts +9 -0
  12. package/dist/lib/npm-auth.js +48 -0
  13. package/dist/lib/scaffold-render-sweep.d.ts +3 -2
  14. package/dist/lib/scaffold-render-sweep.js +5 -8
  15. package/dist/module-architecture.js +98 -10
  16. package/dist/paid-sections.d.ts +21 -2
  17. package/dist/paid-sections.js +69 -7
  18. package/dist/seed-flows.d.ts +4 -1
  19. package/dist/seed-flows.js +50 -4
  20. package/dist/spoke-checkout.d.ts +38 -35
  21. package/dist/spoke-checkout.js +66 -229
  22. package/dist/starter-grid.d.ts +0 -3
  23. package/dist/starter-grid.js +24 -14
  24. package/dist/surfaces.js +33 -12
  25. package/dist/sync-routes.d.ts +38 -1
  26. package/dist/sync-routes.js +184 -11
  27. package/dist/template-record.d.ts +1 -1
  28. package/dist/template-record.js +3 -2
  29. package/dist/update.js +10 -0
  30. package/package.json +3 -3
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  32. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  35. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  37. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  38. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  39. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  40. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  41. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  42. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  43. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  44. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  45. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  46. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  47. package/templates/.env.example +9 -7
  48. package/templates/.github/workflows/ci.yml +13 -0
  49. package/templates/README.md +1 -0
  50. package/templates/_gitignore +4 -0
  51. package/templates/apps/api/.env.example +27 -0
  52. package/templates/apps/api/CLAUDE.md +77 -16
  53. package/templates/apps/api/package.json +1 -0
  54. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  55. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  56. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  57. package/templates/apps/api/src/config/index.ts +109 -0
  58. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  59. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  60. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  61. package/templates/apps/api/src/lib/stripe.ts +74 -4
  62. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  63. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  64. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  65. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  66. package/templates/apps/api/src/openapi/spec.ts +10 -7
  67. package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
  68. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  69. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
  70. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  71. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  72. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  73. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  74. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
  75. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  76. package/templates/apps/api/src/routes/consent/account.ts +4 -2
  77. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
  78. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  79. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  80. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  81. package/templates/apps/api/src/routes/content/index.ts +16 -2
  82. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  83. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  84. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  85. package/templates/apps/api/src/routes/leads/index.ts +17 -14
  86. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  87. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  88. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  89. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
  90. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  91. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  92. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  93. package/templates/apps/api/src/routes/webhooks/stripe.ts +91 -17
  94. package/templates/apps/api/src/server.ts +140 -51
  95. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  96. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
  97. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  98. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
  99. package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
  100. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
  101. package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
  102. package/templates/apps/api/src/services/entitlements.ts +157 -1
  103. package/templates/apps/api/src/services/lead-routing.ts +113 -58
  104. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
  105. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  106. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
  107. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  108. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
  109. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
  110. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  111. package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
  112. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
  113. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  114. package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
  115. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  116. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  117. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  118. package/templates/apps/api/src/services/person.ts +124 -9
  119. package/templates/apps/api/src/services/plan-offerings.ts +96 -30
  120. package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
  121. package/templates/apps/mcp/CLAUDE.md +22 -14
  122. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  123. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  124. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  125. package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
  126. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  127. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  128. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  129. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  130. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  131. package/templates/apps/mcp/src/server.ts +5 -0
  132. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  133. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  134. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  135. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  136. package/templates/apps/web/.env.example +10 -31
  137. package/templates/apps/web/CLAUDE.md +315 -42
  138. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
  139. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
  140. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
  141. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  142. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
  143. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
  144. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
  145. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
  146. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  147. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  148. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
  149. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
  150. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
  151. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  152. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
  153. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
  154. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +6 -2
  155. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
  156. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
  157. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  158. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  159. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  160. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  161. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  162. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
  163. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  164. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  165. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  166. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  167. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  168. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  169. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  170. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  171. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  172. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  173. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  174. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  175. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  176. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  177. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  178. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  179. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  180. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  181. package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
  182. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  183. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
  184. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  185. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  186. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
  187. package/templates/apps/web/lib/auth-routes.ts +36 -2
  188. package/templates/apps/web/lib/checkout-config.ts +9 -20
  189. package/templates/apps/web/lib/deck-page.ts +1 -0
  190. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  191. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  192. package/templates/apps/web/lib/magnet-routes.ts +42 -0
  193. package/templates/apps/web/lib/modules.generated.ts +9 -0
  194. package/templates/apps/web/lib/newsletter.actions.ts +202 -0
  195. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  196. package/templates/apps/web/lib/posting-title.ts +17 -0
  197. package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
  198. package/templates/apps/web/messages/en.json +49 -15
  199. package/templates/apps/web/middleware.ts +12 -2
  200. package/templates/apps/web/security-headers.config.ts +17 -0
  201. package/templates/content/en/BlogList/blog.mdx +4 -71
  202. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  203. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  204. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  205. package/templates/content/en/BookCall/start-project.mdx +1 -17
  206. package/templates/content/en/ContactPage/contact.mdx +6 -63
  207. package/templates/content/en/HomePage/home.mdx +3 -21
  208. package/templates/database/CHANGELOG.md +31 -0
  209. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  210. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  211. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  212. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  213. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  214. package/templates/database/__tests__/seed.test.ts +31 -1
  215. package/templates/database/auth/schema.prisma +4 -0
  216. package/templates/database/config/schema.prisma +91 -0
  217. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  218. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  219. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  220. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  221. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  222. package/templates/database/package.json +1 -1
  223. package/templates/database/seed.ts +30 -2
  224. package/versions.json +35 -35
  225. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  226. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  227. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  228. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  229. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  230. package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
  231. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
  232. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
  233. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
  234. package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
  235. package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
  236. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  237. package/templates/apps/web/lib/checkout.ts +0 -93
  238. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -143,6 +143,21 @@ the site's contact email, never a form that would report a message as sent
143
143
  when it reached nobody. A failed status read counts as accepting, so the
144
144
  form renders and its submit reports honestly.
145
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
+
146
161
  The form's fields come from the workspace's **Contact form magnet** (#8661):
147
162
  every workspace is seeded one, live at slug `contact`, and the page is rendered
148
163
  on every request (`force-dynamic`) and reads it through the public flow read,
@@ -194,7 +209,7 @@ redirect, never into a code, cookie or continuation ref (a repeated
194
209
  `login_hint` is `400 invalid_request`). `email` and `login_hint` are in the
195
210
  telemetry scrub lists (`lib/telemetry-scrub.ts`, `packages/analytics`).
196
211
 
197
- **The same link on the magnet walk and /careers (#8771).** The `/careers/<slug>` job page's action 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.
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.
198
213
 
199
214
  A `contact-form` section in the `column` variant (the minimalism design's
200
215
  Contact page) renders one centred heading, one line and the form, and the
@@ -214,51 +229,289 @@ and on a declined terms turn, Skip or a failed submit swaps to
214
229
  embedding for it is behind the hero CTA's lightbox gate
215
230
  (`packages/ui/src/sections/FormLightbox.tsx`, #4577), not a whole-page mount.
216
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
+
217
475
  ## Spoke checkout
218
476
 
219
- A spoke that carries `commerce` sells through the standard spoke checkout
220
- (#8748), six files every such spoke shares and `create-ailk update` keeps
221
- current. A hub, or a spoke without `commerce`, never gets them: the hub's
222
- checkout is `apps/api`'s.
223
-
224
- - **The files.** `app/api/checkout/[offering]/route.ts` (the buy route),
225
- `app/api/webhooks/stripe/route.ts`, `app/[locale]/checkout/thank-you/page.tsx`,
226
- `app/[locale]/checkout/unavailable/page.tsx`, `lib/checkout.ts` (the
227
- standard) and `lib/checkout-config.ts` (the site's data). Every guarantee —
228
- live-key admission, the buy guards, the webhook's verification and filters,
229
- the duplicate guard — lives in `@working-theory/spoke-checkout` and arrives by
230
- a package bump.
231
- - **One data file.** `lib/checkout-config.ts` is the one file a site edits:
232
- the `const SITE` stamp and the `offerings` map, no import and no code.
233
- `create-ailk update` never rewrites it. Name an offering's slug after the last
234
- segment of the Service or Product page that sells it.
235
- - **A slug that is not an offering answers 404**, before any guard, limiter,
236
- admission or buyer lookup. Every other refusal is a 303 to
237
- `/checkout/unavailable`.
238
- - **Spoke only.** Both routes answer 404 and both pages call `notFound()`
239
- unless `lib/topology.generated.ts`'s `TOPOLOGY` is `"spoke"`, so this
240
- monorepo's hub-shaped `apps/web` serves none of them.
241
- - **One sender, two importers.** `lib/checkout.ts`'s private fulfilment is the
242
- one `apps/web` file that sends an `Idempotency-Key`: one `leads.submit` per
243
- paid session, `payload.site` set to the checkout stamp (never
244
- `AILK_LEAD_SITE`), the session id as the key, and a body that must stay
245
- byte-identical between attempts, because the hub replays only an identical
246
- body. The payload's last key, `termsAccepted`, is the session's `ailk_terms`
247
- stamp when the buyer accepted Stripe's terms box, else `null`; the hub writes
248
- the buyer's checkout consent grant from it (#8778). The hub leaves that key
249
- out of its replay comparison, so a redelivery across the update still
250
- replays. Only the two route files may import `lib/checkout.ts` or the package
251
- (`tests/architecture/idempotency-key-producers.test.ts`, `architecture.yaml`);
252
- a spoke that signs in through its hub adds a third, its rendered billing
253
- action, which its own `architecture.yaml` admits. The thank-you page may
254
- import the package for `readPaidCheckoutEmail` alone, never
255
- `lib/checkout.ts` (#8757 D13): it reads `session_id`, and for a paid
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
256
501
  session of this site under 24 hours old shows "Open your account" with the
257
502
  buyer's email (`accountInviteHref`), else the same link with no email. Each
258
503
  visitor gets 10 reads per 10 minutes (`src/lib/rate-limit.ts`); the page
259
- sends no referrer, renders dynamically and never touches a cookie.
260
- `spokeCheckoutConfig` reads only `site` and `offerings` from the site's file,
261
- so admission always reads `process.env`.
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.
262
515
 
263
516
  ## The same-origin relay (`/v1/*`)
264
517
 
@@ -286,7 +539,27 @@ A visitor's browser never calls the hub. Lead capture, newsletter signup, callba
286
539
  - Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
287
540
  - Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
288
541
 
289
- Last updated: 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`.
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`.
290
563
 
291
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`.
292
565
 
@@ -4,7 +4,7 @@
4
4
  * account, and its Server Action `acceptAccountAgreementsAction`.
5
5
  */
6
6
  import { consentStatementText } from "@working-theory/ui/blocks";
7
- import { CONTACT_FORM_TERMS_STATEMENT } from "@working-theory/validation";
7
+ import { ACCOUNT_TERMS_STATEMENT } from "@working-theory/validation";
8
8
  import {
9
9
  fireEvent,
10
10
  render,
@@ -56,7 +56,7 @@ const LABELS = {
56
56
 
57
57
  const AGREEMENT: AgreementsInterstitialAgreement = {
58
58
  id: "account_terms",
59
- statement: CONTACT_FORM_TERMS_STATEMENT,
59
+ statement: ACCOUNT_TERMS_STATEMENT,
60
60
  acceptedVersion: null,
61
61
  };
62
62
 
@@ -132,7 +132,7 @@ describe("AgreementsInterstitial — the statement renders with its links (AC-5,
132
132
  "account-agreements-statement-account_terms",
133
133
  );
134
134
  expect(statement.textContent).toBe(
135
- consentStatementText(CONTACT_FORM_TERMS_STATEMENT),
135
+ consentStatementText(ACCOUNT_TERMS_STATEMENT),
136
136
  );
137
137
  expect(statement.textContent).not.toContain("[");
138
138
  expect(statement.textContent).not.toContain("](");
@@ -123,7 +123,7 @@ jest.mock("next/navigation", () => ({
123
123
 
124
124
  import { usePhoneTab } from "@working-theory/templates/account";
125
125
  import { consentStatementText } from "@working-theory/ui/blocks";
126
- import { CONTACT_FORM_TERMS_STATEMENT } from "@working-theory/validation";
126
+ import { ACCOUNT_TERMS_STATEMENT } from "@working-theory/validation";
127
127
 
128
128
  import AccountLayout from "../layout";
129
129
  import InboxPage from "../page";
@@ -152,7 +152,7 @@ function agreementsAnswer(entry: { acceptedVersion: number | null; current: bool
152
152
  agreements: [
153
153
  {
154
154
  id: "account_terms",
155
- statement: CONTACT_FORM_TERMS_STATEMENT,
155
+ statement: ACCOUNT_TERMS_STATEMENT,
156
156
  policyVersion: 2,
157
157
  acceptedVersion: entry.acceptedVersion,
158
158
  acceptedAt: entry.acceptedVersion === null ? null : "2026-09-01T00:00:00.000Z",
@@ -694,7 +694,7 @@ describe("the agreements interstitial (#8765 D7)", () => {
694
694
  await renderLayout();
695
695
  expectInterstitialAlone("Please accept our terms");
696
696
  const statement = screen.getByTestId("account-agreements-statement-account_terms");
697
- expect(statement.textContent).toBe(consentStatementText(CONTACT_FORM_TERMS_STATEMENT));
697
+ expect(statement.textContent).toBe(consentStatementText(ACCOUNT_TERMS_STATEMENT));
698
698
  expect(errorSpy).toHaveBeenCalledWith(
699
699
  expect.any(String),
700
700
  { event: "account-agreements", outcome: "read_failed" },
@@ -9,7 +9,7 @@ import { join } from "node:path";
9
9
 
10
10
  import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
11
11
  import { consentStatementText } from "@working-theory/ui/blocks";
12
- import { CONTACT_FORM_TERMS_STATEMENT } from "@working-theory/validation";
12
+ import { ACCOUNT_TERMS_STATEMENT } from "@working-theory/validation";
13
13
 
14
14
  jest.mock("next-intl/server", () => {
15
15
  const msgs: unknown = require("../../../../../messages/en.json");
@@ -123,7 +123,7 @@ function agreementPurpose(overrides: Partial<{ id: string; state: unknown }> = {
123
123
  required: true,
124
124
  signUp: true,
125
125
  description: "Account terms.",
126
- statement: CONTACT_FORM_TERMS_STATEMENT,
126
+ statement: ACCOUNT_TERMS_STATEMENT,
127
127
  policyHref: "/working-agreement",
128
128
  policyVersion: 2,
129
129
  state: overrides.state === undefined ? null : overrides.state,
@@ -302,7 +302,7 @@ describe("AC-4 — account-privacy-agreements (read-only)", () => {
302
302
  mockConsents.mockResolvedValue(ok({ purposes: [agreementPurpose()] }));
303
303
  await renderPage(locale);
304
304
  const statement = screen.getByTestId("account-privacy-agreement-account_terms-statement");
305
- expect(statement.textContent).toBe(consentStatementText(CONTACT_FORM_TERMS_STATEMENT));
305
+ expect(statement.textContent).toBe(consentStatementText(ACCOUNT_TERMS_STATEMENT));
306
306
  expect(statement.textContent).not.toContain("[");
307
307
  expect(statement.textContent).not.toContain("](");
308
308
  const prefix = locale === "en" ? "" : `/${locale}`;