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
@@ -32,6 +32,8 @@
32
32
  */
33
33
 
34
34
  import { checkout } from "@working-theory/api-client";
35
+ import { getMcpToolCatalog } from "@working-theory/metadata";
36
+ import { purchaseMcpInputSchema } from "@working-theory/validation";
35
37
 
36
38
  import {
37
39
  __setMcpCommerceClaimForTest,
@@ -72,13 +74,13 @@ describe("start_purchase — parity with the payload POST /v1/checkout/sessions
72
74
  createSession.mockResolvedValueOnce(sessionCreated());
73
75
 
74
76
  await ailkPurchase({
75
- productCode: "pro-plan",
77
+ productCode: "growth_monthly",
76
78
  quantity: 2,
77
79
  });
78
80
 
79
81
  expect(createSession).toHaveBeenCalledTimes(1);
80
82
  expect(createSession).toHaveBeenCalledWith({
81
- productCode: "pro-plan",
83
+ productCode: "growth_monthly",
82
84
  quantity: 2,
83
85
  successUrl:
84
86
  "https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
@@ -89,7 +91,7 @@ describe("start_purchase — parity with the payload POST /v1/checkout/sessions
89
91
  it("omits quantity entirely when it is not supplied", async () => {
90
92
  createSession.mockResolvedValueOnce(sessionCreated());
91
93
 
92
- await ailkPurchase({ productCode: "pro-plan" });
94
+ await ailkPurchase({ productCode: "growth_monthly" });
93
95
 
94
96
  const payload = createSession.mock.calls[0]![0];
95
97
  expect(payload).not.toHaveProperty("quantity");
@@ -99,10 +101,10 @@ describe("start_purchase — parity with the payload POST /v1/checkout/sessions
99
101
  it("derives both redirect URLs from AILK_BASE_URL — always, not only when omitted (#6001)", async () => {
100
102
  createSession.mockResolvedValueOnce(sessionCreated());
101
103
 
102
- await ailkPurchase({ productCode: "pro-plan" });
104
+ await ailkPurchase({ productCode: "growth_monthly" });
103
105
 
104
106
  expect(createSession).toHaveBeenCalledWith({
105
- productCode: "pro-plan",
107
+ productCode: "growth_monthly",
106
108
  successUrl:
107
109
  "https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
108
110
  cancelUrl: "https://example.com/purchase/cancel",
@@ -114,7 +116,7 @@ describe("start_purchase — FR-1: the hosted Checkout URL", () => {
114
116
  it("returns { url } deep-equal to the seam's url, and nothing else", async () => {
115
117
  createSession.mockResolvedValueOnce(sessionCreated());
116
118
 
117
- const result = await ailkPurchase({ productCode: "pro-plan" });
119
+ const result = await ailkPurchase({ productCode: "growth_monthly" });
118
120
 
119
121
  expect(result).toEqual({ url: HOSTED_URL });
120
122
  });
@@ -122,7 +124,7 @@ describe("start_purchase — FR-1: the hosted Checkout URL", () => {
122
124
  it("does NOT surface the route's sessionId (D5 — it identifies a buyer's session)", async () => {
123
125
  createSession.mockResolvedValueOnce(sessionCreated());
124
126
 
125
- const result = await ailkPurchase({ productCode: "pro-plan" });
127
+ const result = await ailkPurchase({ productCode: "growth_monthly" });
126
128
 
127
129
  expect(result).not.toHaveProperty("sessionId");
128
130
  });
@@ -130,7 +132,7 @@ describe("start_purchase — FR-1: the hosted Checkout URL", () => {
130
132
  it("the URL it returns is Stripe-hosted — nothing is charged in-tool", async () => {
131
133
  createSession.mockResolvedValueOnce(sessionCreated());
132
134
 
133
- const result = await ailkPurchase({ productCode: "pro-plan" });
135
+ const result = await ailkPurchase({ productCode: "growth_monthly" });
134
136
 
135
137
  expect((result as { url: string }).url).toMatch(
136
138
  /^https:\/\/checkout\.stripe\.com\//,
@@ -146,7 +148,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
146
148
  body: { error: "unknown-product-code" },
147
149
  });
148
150
 
149
- const result = await ailkPurchase({ productCode: "no-such-offering" });
151
+ const result = await ailkPurchase({ productCode: "no-such-offering_monthly" });
150
152
 
151
153
  // The HTTP twin states and tests "Error responses never echo user input";
152
154
  // parity means the pair answers alike (#5971 security review, Low).
@@ -160,7 +162,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
160
162
  body: { error: "unknown-product-code" },
161
163
  });
162
164
 
163
- const result = await ailkPurchase({ productCode: "no-such-offering" });
165
+ const result = await ailkPurchase({ productCode: "no-such-offering_monthly" });
164
166
 
165
167
  expect(result).toMatchObject({ error: "not_found" });
166
168
  });
@@ -172,7 +174,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
172
174
  body: { error: "unknown-product-code" },
173
175
  });
174
176
 
175
- await ailkPurchase({ productCode: "no-such-offering" });
177
+ await ailkPurchase({ productCode: "no-such-offering_monthly" });
176
178
 
177
179
  expect(createSession).toHaveBeenCalledTimes(1);
178
180
  });
@@ -184,7 +186,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
184
186
  body: { error: "checkout-unavailable" },
185
187
  });
186
188
 
187
- const result = await ailkPurchase({ productCode: "pro-plan" });
189
+ const result = await ailkPurchase({ productCode: "growth_monthly" });
188
190
 
189
191
  expect(result).toMatchObject({ error: "validation_failed" });
190
192
  });
@@ -250,3 +252,197 @@ describe("start_purchase — the redirect fix (#6001): a caller-supplied redirec
250
252
  expect(createSession).not.toHaveBeenCalled();
251
253
  });
252
254
  });
255
+
256
+ // ── #8935 — the full Offerings lookup_key ───────────────────────────────────
257
+ //
258
+ // The tool takes a price by its full lookup_key (contract §3.7) and refuses an
259
+ // ungrammatical one before any HTTP call, with the same not-found body every
260
+ // route 404 maps to (A3, A5). The route decides the workspace and whether the
261
+ // key is shown and org-enabled (A1, #8931); the mocked seam below answers with
262
+ // the route's Offerings-path `201` body, `reply.status(201).send({ sessionId,
263
+ // url })`. Spec: `docs/specs/8935-start-purchase-sells-a-named-spec.md`.
264
+
265
+ const OFFERINGS_URL = "https://checkout.stripe.com/c/pay/cs_test_offerings";
266
+
267
+ function offeringsSessionCreated() {
268
+ return {
269
+ ok: true as const,
270
+ body: { sessionId: "cs_test_offerings", url: OFFERINGS_URL },
271
+ };
272
+ }
273
+
274
+ const NOT_FOUND = {
275
+ error: "not_found",
276
+ message: "No offering matches that product code.",
277
+ };
278
+
279
+ describe("start_purchase — a shown, org-enabled lookup_key gets the route's hosted URL (#8935 AC-1)", () => {
280
+ const keys = [
281
+ "growth_monthly",
282
+ "growth_yearly",
283
+ "basic_once",
284
+ "samhenry-coaching_monthly",
285
+ `${"a".repeat(40)}_monthly`,
286
+ ];
287
+
288
+ it.each(keys)(
289
+ "%s reaches the route byte for byte and returns { url } only",
290
+ async (key) => {
291
+ createSession.mockResolvedValueOnce(offeringsSessionCreated());
292
+
293
+ const result = await ailkPurchase({ productCode: key });
294
+
295
+ expect(result).toEqual({ url: OFFERINGS_URL });
296
+ expect(result).not.toHaveProperty("sessionId");
297
+ expect(createSession).toHaveBeenCalledTimes(1);
298
+ // One argument, the payload — no init, so no header of the tool's own.
299
+ expect(createSession.mock.calls[0]).toHaveLength(1);
300
+ expect(createSession.mock.calls[0]![0]).toEqual({
301
+ productCode: key,
302
+ successUrl:
303
+ "https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
304
+ cancelUrl: "https://example.com/purchase/cancel",
305
+ });
306
+ },
307
+ );
308
+
309
+ it("the 48-character key is the longest §3.7 allows", () => {
310
+ expect(keys[4]).toHaveLength(48);
311
+ });
312
+
313
+ it("forwards quantity alongside the key", async () => {
314
+ createSession.mockResolvedValueOnce(offeringsSessionCreated());
315
+
316
+ await ailkPurchase({ productCode: "growth_monthly", quantity: 2 });
317
+
318
+ expect(createSession.mock.calls[0]![0]).toEqual({
319
+ productCode: "growth_monthly",
320
+ quantity: 2,
321
+ successUrl:
322
+ "https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
323
+ cancelUrl: "https://example.com/purchase/cancel",
324
+ });
325
+ });
326
+ });
327
+
328
+ describe("start_purchase — an ungrammatical key is refused before any call, with the route-404 body (#8935 AC-2)", () => {
329
+ const ungrammatical = [
330
+ "pro-plan",
331
+ "Growth_Monthly",
332
+ "growth_pro_monthly",
333
+ "wendi_retainer",
334
+ "growth_quarterly",
335
+ "growth_",
336
+ "_monthly",
337
+ `${"a".repeat(41)}_monthly`,
338
+ ];
339
+
340
+ it.each(ungrammatical)(
341
+ "%s passes the shared schema but answers not_found with no call and no echo",
342
+ async (key) => {
343
+ expect(purchaseMcpInputSchema.safeParse({ productCode: key }).success).toBe(
344
+ true,
345
+ );
346
+
347
+ const result = await ailkPurchase({ productCode: key });
348
+
349
+ expect(result).toEqual(NOT_FOUND);
350
+ expect(createSession).not.toHaveBeenCalled();
351
+ expect(JSON.stringify(result)).not.toContain(key);
352
+ },
353
+ );
354
+
355
+ it("the 49-character key is one past the §3.7 bound", () => {
356
+ expect(ungrammatical[7]).toHaveLength(49);
357
+ });
358
+
359
+ it.each([
360
+ ["{ error: unknown-product-code }", { error: "unknown-product-code" }],
361
+ ["null", null],
362
+ ["{}", {}],
363
+ ])(
364
+ "every route 404 maps to the same body, whatever the route's body (%s)",
365
+ async (_label, body) => {
366
+ createSession.mockResolvedValueOnce({
367
+ ok: false as const,
368
+ status: 404,
369
+ body,
370
+ } as never);
371
+
372
+ const result = await ailkPurchase({ productCode: "growth_monthly" });
373
+
374
+ expect(result).toEqual(NOT_FOUND);
375
+ expect(JSON.stringify(result)).not.toContain("growth_monthly");
376
+ },
377
+ );
378
+
379
+ it("the pre-call refusal and the route-404 mapping are one value", async () => {
380
+ const refused = await ailkPurchase({ productCode: "pro-plan" });
381
+ createSession.mockResolvedValueOnce({
382
+ ok: false as const,
383
+ status: 404,
384
+ body: { error: "unknown-product-code" },
385
+ });
386
+ const mapped = await ailkPurchase({ productCode: "growth_monthly" });
387
+
388
+ expect(refused).toEqual(mapped);
389
+ });
390
+
391
+ it("a code the route's regex refuses still answers validation_failed, with no call", async () => {
392
+ const result = await ailkPurchase({ productCode: "has spaces" });
393
+
394
+ expect(result).toMatchObject({ error: "validation_failed" });
395
+ expect(createSession).not.toHaveBeenCalled();
396
+ });
397
+ });
398
+
399
+ describe("start_purchase — the claim and the strict schema still answer first (#8935 AC-3)", () => {
400
+ it("an unclaimed site answers the module-disabled body for an ungrammatical key, with no call", async () => {
401
+ __setMcpCommerceClaimForTest("unclaimed");
402
+
403
+ const result = await ailkPurchase({ productCode: "Growth_Monthly" });
404
+
405
+ expect(result).toEqual({ error: "not_found", message: "Not found" });
406
+ expect(createSession).not.toHaveBeenCalled();
407
+ });
408
+
409
+ it.each(["workspaceId", "organizationId", "site", "metadata"])(
410
+ "a direct call carrying %s fails validation, with no call",
411
+ async (field) => {
412
+ const result = await ailkPurchase({
413
+ productCode: "growth_monthly",
414
+ [field]: "attacker-chosen",
415
+ } as never);
416
+
417
+ expect(result).toMatchObject({ error: "validation_failed" });
418
+ expect(createSession).not.toHaveBeenCalled();
419
+ },
420
+ );
421
+
422
+ it("the input schema has exactly productCode and quantity", () => {
423
+ expect(Object.keys(purchaseMcpInputSchema.shape)).toEqual([
424
+ "productCode",
425
+ "quantity",
426
+ ]);
427
+ });
428
+ });
429
+
430
+ describe("start_purchase — the catalog description names the lookup key (#8935 AC-5)", () => {
431
+ it("tells an agent to pass the lookupKey get_pricing or get_offerings returns", () => {
432
+ const entry = getMcpToolCatalog({ addons: { commerce: true } }).find(
433
+ (tool) => tool.name === "start_purchase",
434
+ );
435
+
436
+ expect(entry?.description).toBe(
437
+ "Start a purchase for an offering on this site. Provide the price's full lookup key as productCode — the lookupKey that get_pricing or get_offerings returns, such as growth_monthly — and an optional quantity; returns a Stripe-hosted Checkout URL for the buyer to complete payment on. Nothing is charged by this tool.",
438
+ );
439
+ for (const phrase of [
440
+ "lookupKey",
441
+ "get_pricing",
442
+ "get_offerings",
443
+ "Nothing is charged by this tool",
444
+ ]) {
445
+ expect(entry?.description).toContain(phrase);
446
+ }
447
+ });
448
+ });
@@ -274,13 +274,13 @@ describe("ailkPurchase — valid input", () => {
274
274
  );
275
275
 
276
276
  await ailkPurchase({
277
- productCode: "price_test_123",
277
+ productCode: "growth_monthly",
278
278
  quantity: 1,
279
279
  });
280
280
 
281
281
  expect(createSession).toHaveBeenCalledTimes(1);
282
282
  const payload = createSession.mock.calls[0]![0];
283
- expect(payload.productCode).toBe("price_test_123");
283
+ expect(payload.productCode).toBe("growth_monthly");
284
284
  expect(payload.quantity).toBe(1);
285
285
  });
286
286
 
@@ -289,7 +289,7 @@ describe("ailkPurchase — valid input", () => {
289
289
  sessionCreated("https://checkout.stripe.com/pay/cs_test_promo"),
290
290
  );
291
291
 
292
- await ailkPurchase({ productCode: "price_test_123" });
292
+ await ailkPurchase({ productCode: "growth_monthly" });
293
293
 
294
294
  expect(createSession.mock.calls[0]![0]).not.toHaveProperty(
295
295
  "allowPromotionCodes",
@@ -302,7 +302,7 @@ describe("ailkPurchase — valid input", () => {
302
302
  );
303
303
 
304
304
  const output = await ailkPurchase({
305
- productCode: "price_test_xyz",
305
+ productCode: "growth_monthly",
306
306
  });
307
307
 
308
308
  expect(output).toEqual({
@@ -315,7 +315,7 @@ describe("ailkPurchase — valid input", () => {
315
315
  sessionCreated("https://checkout.stripe.com/pay/cs_test_valid"),
316
316
  );
317
317
 
318
- const output = await ailkPurchase({ productCode: "price_test_valid" });
318
+ const output = await ailkPurchase({ productCode: "growth_monthly" });
319
319
  expect(PurchaseOutputSchema.safeParse(output).success).toBe(true);
320
320
  });
321
321
  });
@@ -373,10 +373,10 @@ describe("ailkPurchase — always derives redirects from AILK_BASE_URL (#6001 AC
373
373
  sessionCreated("https://checkout.stripe.com/pay/cs_test_base"),
374
374
  );
375
375
 
376
- await ailkPurchase({ productCode: "price_test_123" });
376
+ await ailkPurchase({ productCode: "growth_monthly" });
377
377
 
378
378
  expect(createSession).toHaveBeenCalledWith({
379
- productCode: "price_test_123",
379
+ productCode: "growth_monthly",
380
380
  successUrl:
381
381
  "https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
382
382
  cancelUrl: "https://example.com/purchase/cancel",
@@ -390,10 +390,10 @@ describe("ailkPurchase — always derives redirects from AILK_BASE_URL (#6001 AC
390
390
  sessionCreated("https://checkout.stripe.com/pay/cs_test_fallback"),
391
391
  );
392
392
 
393
- await ailkPurchase({ productCode: "price_test_123" });
393
+ await ailkPurchase({ productCode: "growth_monthly" });
394
394
 
395
395
  expect(createSession).toHaveBeenCalledWith({
396
- productCode: "price_test_123",
396
+ productCode: "growth_monthly",
397
397
  successUrl:
398
398
  "http://localhost:3000/purchase/success?session_id={CHECKOUT_SESSION_ID}",
399
399
  cancelUrl: "http://localhost:3000/purchase/cancel",
@@ -408,7 +408,7 @@ describe("ailkPurchase — always derives redirects from AILK_BASE_URL (#6001 AC
408
408
  sessionCreated("https://checkout.stripe.com/pay/cs_test_braces"),
409
409
  );
410
410
 
411
- await ailkPurchase({ productCode: "price_test_123" });
411
+ await ailkPurchase({ productCode: "growth_monthly" });
412
412
 
413
413
  const payload = createSession.mock.calls[0]![0] as {
414
414
  successUrl: string;
@@ -427,7 +427,7 @@ describe("ailkPurchase — parity shape with purchaseAction", () => {
427
427
  );
428
428
 
429
429
  const output = await ailkPurchase({
430
- productCode: "price_parity",
430
+ productCode: "growth_monthly",
431
431
  });
432
432
 
433
433
  expect(output).toHaveProperty("url");
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The MCP read of the site's Offerings shown set (#8933, spec D6).
3
+ *
4
+ * `get_pricing` and `get_offerings` answer a connected site's shown set —
5
+ * the persisted Stripe snapshot `GET /v1/content/shown-offerings` serves
6
+ * (#8924) — rather than its page content. apps/mcp reaches Offerings data
7
+ * only over HTTP (#8876 D9), so this file reads that route through
8
+ * `@working-theory/api-client` and hands the tools the current set, or
9
+ * `null` for the content path:
10
+ *
11
+ * 1. The deployment's own claim first: unless `mcpCommerceClaim()` reads
12
+ * `claimed`, no HTTP call is made at all. The API's claim, read inside
13
+ * the route, still decides; this one only spares an unlicensed host the
14
+ * call.
15
+ * 2. One `apiClient.offerings.shown()`, raced against
16
+ * `MCP_SHOWN_SET_BUDGET_MS`, so a hung API never holds a tool call.
17
+ * 3. A refusal, a body `shownOfferingsAnswerSchema` refuses, the budget
18
+ * passing or a rejection is `null` and one `console.error` line.
19
+ * 4. Otherwise `currentShownSet` — a `current` set with offerings, or `null`.
20
+ *
21
+ * The workspace is the API's: api-client sends the deployment's own spoke
22
+ * key, and no tool argument names one (identity-blind). No Stripe package is
23
+ * imported here or anywhere in apps/mcp (T7).
24
+ *
25
+ * T6 — every log line is `{ event, outcome }` plus an HTTP `status` or an
26
+ * error's class name as `errName`: never a body, a message, the shown set, a
27
+ * lookup key, an id, a name or a description. The log calls live here
28
+ * because the tool files may not call `console.*`.
29
+ */
30
+
31
+ import { apiClient } from "@working-theory/api-client";
32
+ import { currentShownSet } from "@working-theory/content-adapters";
33
+ import {
34
+ shownOfferingsAnswerSchema,
35
+ type WorkspaceShownSetWire,
36
+ } from "@working-theory/validation";
37
+
38
+ import { mcpCommerceClaim } from "./commerce-claim.js";
39
+
40
+ /** The longest one tool call waits on the shown set before answering content. */
41
+ export const MCP_SHOWN_SET_BUDGET_MS = 5000;
42
+
43
+ const OVER_BUDGET = Symbol("over-budget");
44
+
45
+ /** An error's class name only — never its message, which may carry Stripe text. */
46
+ function errName(err: unknown): string {
47
+ return err instanceof Error ? err.constructor.name : typeof err;
48
+ }
49
+
50
+ /** The site's current shown set, or `null` for the content path. */
51
+ export async function mcpCurrentShownSet(): Promise<WorkspaceShownSetWire | null> {
52
+ if (mcpCommerceClaim() !== "claimed") return null;
53
+
54
+ // Caught before the race, so a rejection after the budget is never unhandled.
55
+ const read = apiClient.offerings.shown().catch((err: unknown) => {
56
+ console.error({
57
+ event: "shown-offerings",
58
+ outcome: "unreachable",
59
+ errName: errName(err),
60
+ });
61
+ return null;
62
+ });
63
+ let timer: NodeJS.Timeout | undefined;
64
+ const budget = new Promise<typeof OVER_BUDGET>((resolve) => {
65
+ timer = setTimeout(() => resolve(OVER_BUDGET), MCP_SHOWN_SET_BUDGET_MS);
66
+ });
67
+ let res: Awaited<typeof read> | typeof OVER_BUDGET;
68
+ try {
69
+ res = await Promise.race([read, budget]);
70
+ } finally {
71
+ clearTimeout(timer);
72
+ }
73
+
74
+ if (res === OVER_BUDGET) {
75
+ console.error({ event: "shown-offerings", outcome: "over_budget" });
76
+ return null;
77
+ }
78
+ if (res === null) return null;
79
+ if (!res.ok) {
80
+ console.error({
81
+ event: "shown-offerings",
82
+ outcome: "refused",
83
+ status: res.status,
84
+ });
85
+ return null;
86
+ }
87
+ const parsed = shownOfferingsAnswerSchema.safeParse(res.body);
88
+ if (!parsed.success) {
89
+ console.error({ event: "shown-offerings", outcome: "unreadable" });
90
+ return null;
91
+ }
92
+ return currentShownSet(parsed.data);
93
+ }
94
+
95
+ /**
96
+ * Logs a shown-set mapping that threw (D12): the tool then answers its
97
+ * content path, so no `ZodError` message ever reaches the agent.
98
+ */
99
+ export function reportMappingFailure(err: unknown): void {
100
+ console.error({
101
+ event: "shown-offerings",
102
+ outcome: "mapping_failed",
103
+ errName: errName(err),
104
+ });
105
+ }
@@ -119,6 +119,11 @@ const MODULE_TOOL_ENTRIES: Record<
119
119
  specifier: "./tools/create_booking.js",
120
120
  exportName: "ailkCreateBooking",
121
121
  },
122
+ // #8843 D5 — the twin of GET /v1/schedule/meetings.
123
+ list_meetings: {
124
+ specifier: "./tools/list_meetings.js",
125
+ exportName: "ailkListMeetings",
126
+ },
122
127
  // D-48 signal-collectors — the flow engine's MCP twins of GET
123
128
  // /v1/flows/:slug and POST /v1/flows/:slug/steps. Anonymous, identity-blind;
124
129
  // free-anonymous status is now module-gated rather than unconditional.
@@ -2,26 +2,45 @@
2
2
  * get_offerings — returns Services and/or Products as a unified offerings array.
3
3
  *
4
4
  * Pure read: resolves its adapter through `getMcpContentAdapter()` — the one
5
- * resolver seam (#5805, ADR 0011), MDX by default.
6
- * Lists pages of type "Service" and/or "Product" and shapes them into a
7
- * unified offerings array. Optionally filtered by type.
5
+ * resolver seam (#5805, ADR 0011), MDX by default — and answers through
6
+ * `listOfferings` (`@working-theory/content-adapters`), which lists pages of
7
+ * type "Service" and/or "Product" and shapes them into a unified offerings
8
+ * array. Optionally filtered by type.
9
+ *
10
+ * HTTP twin (#7177): `GET /v1/content/offerings` in `apps/api` takes the same
11
+ * arguments and answers through the same `listOfferings` — the shared seam
12
+ * the pair proves parity at. The tool still reads content locally, so it
13
+ * needs no API to answer.
8
14
  *
9
15
  * Phase 3 S3: optional `locale` parameter (BCP-47). Validated against the
10
16
  * project's allowlist; invalid or unknown values silently fall back to the
11
17
  * default locale (AC-i18n-07). Lists are locale-scoped.
12
18
  *
13
19
  * The offering fields (#5838) — `provider`, `categories`, `price` and
14
- * `priceCurrency` — come off the page's own frontmatter, the same shape the
15
- * listing card renders from. `price` and `priceCurrency` are `null` (never a
16
- * fabricated zero) for a listing that declares no price, and `categories` is
17
- * `[]` for one that is untagged.
20
+ * `priceCurrency` — come off the page's own frontmatter; `listOfferings`
21
+ * documents the mapping.
22
+ *
23
+ * Offerings (#8933, spec D8): when the site sells through its connected
24
+ * Stripe catalog and `type` is not `service`, the tool answers the Service
25
+ * pages followed by the workspace's Offerings shown set, in place of the
26
+ * Product pages — read over HTTP by `lib/shown-offerings.ts` (D6: only on a
27
+ * `claimed` host, at most `MCP_SHOWN_SET_BUDGET_MS`) and mapped by
28
+ * `listOfferingsWithShownSet`, the function the twin route answers through
29
+ * too, so each shown product also carries `prices` (every price with its
30
+ * interval and full lookup key). Names, descriptions and prices are
31
+ * seller-provided data returned as plain JSON values (T5). `type: "service"`,
32
+ * an unclaimed host, a workspace with no current set, a failed read or a
33
+ * mapping that throws answers the page content below, unchanged.
18
34
  *
19
35
  * Input: { type?: 'service' | 'product', locale?: string }
20
- * Output: { offerings: Array<{ name, description, category?, image?, provider, categories, price, priceCurrency }> }
36
+ * Output: { offerings: Array<{ name, description, category?, image?, provider, categories, price, priceCurrency, prices? }> }
21
37
  * Session/scope: none — read-only public content.
22
38
  */
23
39
 
24
- import { type SchemaOrgType } from "@working-theory/content-adapters";
40
+ import {
41
+ listOfferings,
42
+ listOfferingsWithShownSet,
43
+ } from "@working-theory/content-adapters";
25
44
  import {
26
45
  getOfferingsInputSchema,
27
46
  getOfferingsOutputSchema,
@@ -34,6 +53,10 @@ import {
34
53
  mcpContentRoot,
35
54
  } from "../lib/content-adapter.js";
36
55
  import { resolveLocale } from "../lib/resolve-locale.js";
56
+ import {
57
+ mcpCurrentShownSet,
58
+ reportMappingFailure,
59
+ } from "../lib/shown-offerings.js";
37
60
 
38
61
  export { getOfferingsInputSchema, getOfferingsOutputSchema };
39
62
  export type { GetOfferingsInput, GetOfferingsOutput };
@@ -54,26 +77,20 @@ export async function ailkGetOfferings(
54
77
  const locale = resolveLocale(input.locale);
55
78
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
56
79
 
57
- const typesToFetch: SchemaOrgType[] = [];
58
- if (!input.type || input.type === "service") typesToFetch.push("Service");
59
- if (!input.type || input.type === "product") typesToFetch.push("Product");
60
-
61
- const perType = await Promise.all(
62
- typesToFetch.map((type) => adapter.listPages(type, locale)),
63
- );
64
-
65
- const offerings: GetOfferingsOutput["offerings"] = perType
66
- .flat()
67
- .map((page) => ({
68
- name: page.title,
69
- description: page.description,
70
- category: page.schema?.["category"] as string | undefined,
71
- image: page.image,
72
- provider: page.provider ?? null,
73
- categories: page.categories ?? [],
74
- price: page.price ?? null,
75
- priceCurrency: page.priceCurrency ?? null,
76
- }));
80
+ if (input.type !== "service") {
81
+ const shownSet = await mcpCurrentShownSet();
82
+ if (shownSet !== null) {
83
+ try {
84
+ return await listOfferingsWithShownSet(
85
+ adapter,
86
+ { type: input.type, locale },
87
+ shownSet,
88
+ );
89
+ } catch (err) {
90
+ reportMappingFailure(err);
91
+ }
92
+ }
93
+ }
77
94
 
78
- return getOfferingsOutputSchema.parse({ offerings });
95
+ return listOfferings(adapter, { type: input.type, locale });
79
96
  }