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
@@ -14,13 +14,35 @@
14
14
  * offering fields on its own frontmatter answer instead — a listing that
15
15
  * declares `price` + `priceCurrency` yields one tier named for the page. A
16
16
  * listing with neither an authored offer nor a price still returns `null`: no
17
- * price means no tier, never a fabricated zero.
17
+ * price means no tier, never a fabricated zero. `getPricingTiers`
18
+ * (`@working-theory/content-adapters`) documents the mapping.
19
+ *
20
+ * HTTP twin (#7178): `GET /v1/content/pricing` in `apps/api` takes the same
21
+ * arguments and answers through the same `getPricingTiers` — the shared seam
22
+ * the pair proves parity at. The tool still reads content locally, so it
23
+ * needs no API to answer.
24
+ *
25
+ * Offerings (#8933, spec D8): when the site sells through its connected
26
+ * Stripe catalog and `slug` is omitted or `pricing`, the tool answers the
27
+ * workspace's Offerings shown set instead — read over HTTP by
28
+ * `lib/shown-offerings.ts` (D6: only on a `claimed` host, at most
29
+ * `MCP_SHOWN_SET_BUDGET_MS`) and mapped by `pricingTiersFromShownSet`, the
30
+ * function the twin route answers through too, so each tier also carries
31
+ * `interval` and the full `lookupKey`. Names, descriptions and prices are
32
+ * seller-provided data returned as plain JSON values (T5). Any other slug, an
33
+ * unclaimed host, a workspace with no current set, a failed read or a mapping
34
+ * that throws answers the page content below, unchanged.
18
35
  *
19
36
  * Input: { slug?: string, locale?: string }
20
- * Output: { tiers: Array<{ name, price, priceCurrency, description? }> } | null
37
+ * Output: { tiers: Array<{ name, price, priceCurrency, description?, interval?, lookupKey? }> } | null
21
38
  * Session/scope: none — read-only public content.
22
39
  */
23
40
 
41
+ import {
42
+ getPricingTiers,
43
+ pricingTiersFromShownSet,
44
+ shownSetAnswersPricing,
45
+ } from "@working-theory/content-adapters";
24
46
  import {
25
47
  getPricingInputSchema,
26
48
  getPricingOutputSchema,
@@ -33,12 +55,14 @@ import {
33
55
  mcpContentRoot,
34
56
  } from "../lib/content-adapter.js";
35
57
  import { resolveLocale } from "../lib/resolve-locale.js";
58
+ import {
59
+ mcpCurrentShownSet,
60
+ reportMappingFailure,
61
+ } from "../lib/shown-offerings.js";
36
62
 
37
63
  export { getPricingInputSchema, getPricingOutputSchema };
38
64
  export type { GetPricingInput, GetPricingOutput };
39
65
 
40
- const DEFAULT_SLUG = "pricing";
41
-
42
66
  /**
43
67
  * get_pricing handler.
44
68
  *
@@ -51,44 +75,20 @@ export async function ailkGetPricing(
51
75
  ): Promise<GetPricingOutput> {
52
76
  getPricingInputSchema.parse(input);
53
77
 
78
+ if (shownSetAnswersPricing(input.slug)) {
79
+ const shownSet = await mcpCurrentShownSet();
80
+ if (shownSet !== null) {
81
+ try {
82
+ return pricingTiersFromShownSet(shownSet);
83
+ } catch (err) {
84
+ reportMappingFailure(err);
85
+ }
86
+ }
87
+ }
88
+
54
89
  const root = contentRoot ?? mcpContentRoot();
55
90
  const locale = resolveLocale(input.locale);
56
91
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
57
92
 
58
- const slug = input.slug ?? DEFAULT_SLUG;
59
- const data = await adapter.extractStructuredData("Product", slug, locale);
60
-
61
- const schema = (data ?? {}) as Record<string, unknown>;
62
-
63
- // Support both a top-level `offers` array and a single offer object.
64
- const offersRaw = schema["offers"];
65
- const offersArr: Record<string, unknown>[] = Array.isArray(offersRaw)
66
- ? (offersRaw as Record<string, unknown>[])
67
- : offersRaw && typeof offersRaw === "object"
68
- ? [offersRaw as Record<string, unknown>]
69
- : [];
70
-
71
- if (offersArr.length === 0) {
72
- const page = await adapter.getPageBySlug("Product", slug, locale);
73
- if (!page || page.price === undefined) return null;
74
- return getPricingOutputSchema.parse({
75
- tiers: [
76
- {
77
- name: page.title,
78
- price: page.price,
79
- priceCurrency: page.priceCurrency ?? "USD",
80
- ...(page.summary !== undefined && { description: page.summary }),
81
- },
82
- ],
83
- });
84
- }
85
-
86
- const tiers = offersArr.map((offer) => ({
87
- name: (offer["name"] as string) ?? "",
88
- price: (offer["price"] as string | number) ?? "",
89
- priceCurrency: (offer["priceCurrency"] as string) ?? "USD",
90
- description: offer["description"] as string | undefined,
91
- }));
92
-
93
- return getPricingOutputSchema.parse({ tiers });
93
+ return getPricingTiers(adapter, { slug: input.slug, locale });
94
94
  }
@@ -57,11 +57,24 @@
57
57
  * through `@working-theory/api-client` per the architecture invariants (apps
58
58
  * may not import each other; the shared HTTP surface lives in packages/).
59
59
  *
60
- * Spec: `docs/specs/5971-start-purchase-hosted-checkout-spec.md`.
60
+ * THE KEY — the full Offerings lookup_key (#8935). `productCode` is a price's
61
+ * full lookup_key (contract §3.7), the `lookupKey` `get_pricing` and
62
+ * `get_offerings` return. After the claim and the strict schema, the one
63
+ * §3.7 parser, `parseLookupKey`, refuses an ungrammatical key before any HTTP
64
+ * call, with the same not-found body every route 404 maps to (A3, A5). The
65
+ * workspace stays the server's (A1): the api-client attaches the
66
+ * deployment's own spoke key, and the route alone decides whether the key is
67
+ * shown and org-enabled. A spoke whose Organization has no Stripe connection
68
+ * gets the route's same 404, never a session on the hub's own env key, so
69
+ * this tool refuses it as the web buy route does (#9126).
70
+ *
71
+ * Spec: `docs/specs/5971-start-purchase-hosted-checkout-spec.md`,
72
+ * `docs/specs/8935-start-purchase-sells-a-named-spec.md`.
61
73
  */
62
74
 
63
75
  import { checkout } from "@working-theory/api-client";
64
76
  import {
77
+ parseLookupKey,
65
78
  purchaseMcpInputSchema,
66
79
  type PurchaseMcpInput,
67
80
  type PurchaseOutput,
@@ -83,6 +96,15 @@ function commerceDisabledBody(): ErrorBody {
83
96
  return notFoundError("Not found");
84
97
  }
85
98
 
99
+ /**
100
+ * The one not-found body for an offering (#8935 D2, A5): an ungrammatical key
101
+ * refused before the call and every route 404 answer this same value, and
102
+ * neither echoes the caller's key.
103
+ */
104
+ function offeringNotFoundBody(): ErrorBody {
105
+ return notFoundError("No offering matches that product code.");
106
+ }
107
+
86
108
  /**
87
109
  * Export name `ailkPurchase` satisfies the parity-test naming convention
88
110
  * (ailk<PascalVerb>) so the harness can discover it.
@@ -127,6 +149,12 @@ export async function ailkPurchase(
127
149
 
128
150
  const { productCode, quantity } = parsed.data;
129
151
 
152
+ // #8935 D2 — a key outside the §3.7 grammar can never sell, so it is
153
+ // refused here, before the route spends a rate-limit token on it.
154
+ if (parseLookupKey(productCode) === null) {
155
+ return offeringNotFoundBody();
156
+ }
157
+
130
158
  // successUrl/cancelUrl are ALWAYS derived from baseUrl (#6001) — never
131
159
  // accepted from the caller. allowPromotionCodes is never forwarded: it is
132
160
  // not part of purchaseMcpInputSchema, so it can never be present here.
@@ -152,7 +180,7 @@ export async function ailkPurchase(
152
180
  // states and tests that invariant ("Error responses never echo user
153
181
  // input", apps/api/src/routes/checkout/sessions.ts), and parity means
154
182
  // the pair answers alike.
155
- return notFoundError("No offering matches that product code.");
183
+ return offeringNotFoundBody();
156
184
  }
157
185
  return validationError(
158
186
  `checkout-unavailable: POST /v1/checkout/sessions returned ${result.status}`,
@@ -83,6 +83,21 @@ const WORKSPACE_TOOL_ENTRIES: Record<
83
83
  specifier: "./tools/inbox_submissions.js",
84
84
  exportName: "ailkMarkInboxSubmissionDone",
85
85
  },
86
+ list_listing_applications: {
87
+ specifier: "./tools/inbox_applications.js",
88
+ exportName: "ailkListListingApplications",
89
+ },
90
+ get_listing_application: {
91
+ specifier: "./tools/inbox_applications.js",
92
+ exportName: "ailkGetListingApplication",
93
+ },
94
+ decide_listing_application: {
95
+ specifier: "./tools/inbox_applications.js",
96
+ exportName: "ailkDecideListingApplication",
97
+ },
98
+ // #9137 — the pre-rename names, deprecated aliases of the three above for
99
+ // one release: each export is the same function, so the same arguments and
100
+ // result.
86
101
  list_inbox_applications: {
87
102
  specifier: "./tools/inbox_applications.js",
88
103
  exportName: "ailkListInboxApplications",
@@ -261,37 +261,16 @@ CONTENT_ADAPTER=mdx
261
261
  # BETTER_STACK_HEARTBEAT_URL=
262
262
 
263
263
  # =============================================================================
264
- # Scheduling — the scheduling module's booking page (#4633)
264
+ # Scheduling — the scheduling module's booking pages (#4633, #8843)
265
265
  # =============================================================================
266
- # Which event `/meet` books, as "<owner>/<event-slug>" — the path of the
267
- # provider's own public booking page. Read server-side in
268
- # app/[locale]/meet/page.tsx and handed to the `booker` section, which
269
- # sends it back to apps/api's /v1/schedule/* routes to name the event.
270
- #
271
- # Unset is a supported state: `/meet` 404s, exactly as if the scheduling
272
- # module were not installed.
273
- #
274
- # NOT a credential, and deliberately NOT a NEXT_PUBLIC_ variable — the API key
275
- # it pairs with (CAL_API_KEY) lives in apps/api and this app never reads it, so
276
- # keeping the whole scheduling config surface server-read is what makes that
277
- # boundary checkable rather than merely intended.
278
- # SCHEDULING_CAL_LINK=owner/event-slug
279
- #
280
- # The booker's /v1/schedule/* calls go through this site's same-origin relay
281
- # (see the relay block above, #7644), so the baseline CSP's
282
- # `connect-src 'self'` already covers them — no CSP extension is needed for
283
- # them, whatever NEXT_PUBLIC_AILK_API_URL is set to.
266
+ # No variable here. /meet, /private and /meet/<slug> read every meeting fact
267
+ # — which meetings are Live, public or private, their titles, lengths and
268
+ # Cal.com links — from apps/api's GET /v1/schedule/meetings, which resolves
269
+ # the workspace from this site (the site alias keyless, a spoke by its key)
270
+ # and composes each Cal.com link from that workspace's own account. The
271
+ # one-meeting "<owner>/<event-slug>" variable this block used to set is
272
+ # retired, and nothing reads it.
284
273
  #
285
274
  # On a SPOKE (create-ailk --topology spoke, #7872) this site has no apps/api:
286
- # /meet lists the meeting types the hub stores for this spoke's workspace and
287
- # books through the hub at AILK_API_URL with AILK_SPOKE_KEY. Then:
288
- # - the FIRST segment (the owner) must name the provider account the hub
289
- # uses for this spoke's workspace (its scheduling connection). It composes
290
- # each booker's calLink; it is not an access control — the hub checks a
291
- # spoke booking's slug, never its owner.
292
- # - the SECOND segment is ignored: a workspace with no stored meeting-type
293
- # list shows no meetings, because the hub books only a slug it stores.
294
- # - the booker's two calls count against the relay's per-visitor budget,
295
- # which holds per visitor only behind an edge that overwrites
296
- # x-forwarded-for and x-real-ip (Vercel does); elsewhere every visitor
297
- # shares one budget.
275
+ # the pages read the hub's meetings for this spoke's workspace at AILK_API_URL
276
+ # with AILK_SPOKE_KEY, so a workspace with no stored meeting list shows none.