create-ailk 0.22.0 → 0.24.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.
- package/README.md +27 -14
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +17 -10
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +301 -13
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +26 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +36 -2
- package/dist/lib/scaffold-render-sweep.js +50 -8
- package/dist/module-architecture.js +115 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +68 -5
- package/dist/sync-routes.js +455 -29
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +13 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +34 -0
- package/templates/apps/api/CLAUDE.md +94 -19
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +77 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +343 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/search.ts +16 -45
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +377 -308
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
- package/templates/apps/api/src/server.ts +165 -51
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +147 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +32 -18
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +324 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +44 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +205 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +63 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +57 -2
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -274,13 +274,13 @@ describe("ailkPurchase — valid input", () => {
|
|
|
274
274
|
);
|
|
275
275
|
|
|
276
276
|
await ailkPurchase({
|
|
277
|
-
productCode: "
|
|
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("
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
376
|
+
await ailkPurchase({ productCode: "growth_monthly" });
|
|
377
377
|
|
|
378
378
|
expect(createSession).toHaveBeenCalledWith({
|
|
379
|
-
productCode: "
|
|
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: "
|
|
393
|
+
await ailkPurchase({ productCode: "growth_monthly" });
|
|
394
394
|
|
|
395
395
|
expect(createSession).toHaveBeenCalledWith({
|
|
396
|
-
productCode: "
|
|
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: "
|
|
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: "
|
|
430
|
+
productCode: "growth_monthly",
|
|
431
431
|
});
|
|
432
432
|
|
|
433
433
|
expect(output).toHaveProperty("url");
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@working-theory/license": "workspace:*",
|
|
28
28
|
"@working-theory/metadata": "workspace:*",
|
|
29
29
|
"@working-theory/schema": "workspace:*",
|
|
30
|
+
"@working-theory/site-config": "workspace:*",
|
|
30
31
|
"@working-theory/validation": "workspace:*",
|
|
31
32
|
"fastify": "^5.12.5",
|
|
32
33
|
"yaml": "^2.9.0",
|
|
@@ -28,14 +28,18 @@ import { buildDenyList, isUserAgentBlocked } from "./user-agent-deny.js";
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Tools that require email format validation on their input, each mapped to
|
|
31
|
-
*
|
|
32
|
-
* for
|
|
33
|
-
*
|
|
31
|
+
* the paths the email may sit at, read in order — the first string wins: top
|
|
32
|
+
* level for the lead tools, then `payload.email`, since the route lifts the
|
|
33
|
+
* address from there when the top level has none (#7171, #7172: `email` is
|
|
34
|
+
* optional at the top level of both bodies, so an agent could otherwise nest
|
|
35
|
+
* a malformed one past this check); `attendee.email` for create_booking
|
|
36
|
+
* (#8071 D3). submit_flow_step has no fixed email field — its `values` is a
|
|
37
|
+
* free record the hub's flow engine validates.
|
|
34
38
|
*/
|
|
35
|
-
const EMAIL_VALIDATED_TOOLS = new Map<string, readonly string[]>([
|
|
36
|
-
["capture_lead", ["email"]],
|
|
37
|
-
["subscribe_newsletter", ["email"]],
|
|
38
|
-
["create_booking", ["attendee", "email"]],
|
|
39
|
+
const EMAIL_VALIDATED_TOOLS = new Map<string, readonly (readonly string[])[]>([
|
|
40
|
+
["capture_lead", [["email"], ["payload", "email"]]],
|
|
41
|
+
["subscribe_newsletter", [["email"], ["payload", "email"]]],
|
|
42
|
+
["create_booking", [["attendee", "email"]]],
|
|
39
43
|
]);
|
|
40
44
|
|
|
41
45
|
/**
|
|
@@ -274,12 +278,15 @@ export class AbuseGuard {
|
|
|
274
278
|
};
|
|
275
279
|
}
|
|
276
280
|
|
|
277
|
-
// Rule 3: Email format validation, at the tool's own email
|
|
281
|
+
// Rule 3: Email format validation, at the tool's own email paths — the
|
|
282
|
+
// first path holding a string is the address the route will read. A
|
|
278
283
|
// missing or non-string email passes — the handler's Zod schema is the
|
|
279
284
|
// authority on required fields.
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
const email =
|
|
285
|
+
const emailPaths = EMAIL_VALIDATED_TOOLS.get(toolName);
|
|
286
|
+
if (emailPaths !== undefined) {
|
|
287
|
+
const email = emailPaths
|
|
288
|
+
.map((path) => valueAt(input, path))
|
|
289
|
+
.find((value) => typeof value === "string");
|
|
283
290
|
if (typeof email === "string" && !isValidEmail(email)) {
|
|
284
291
|
return {
|
|
285
292
|
allowed: false,
|
|
@@ -201,6 +201,17 @@ function isOriginAllowedForWrite(
|
|
|
201
201
|
return allowedOrigins.includes(origin);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The "retry in …" wording of the write bucket's refusal, matching what
|
|
206
|
+
* @fastify/rate-limit's default error says for the same window — `format()`
|
|
207
|
+
* from `@lukeed/ms` over whole seconds, which for a one-minute window is
|
|
208
|
+
* "1 minute" or "N second(s)".
|
|
209
|
+
*/
|
|
210
|
+
function retryIn(seconds: number): string {
|
|
211
|
+
if (seconds >= 60) return "1 minute";
|
|
212
|
+
return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
204
215
|
/**
|
|
205
216
|
* An agent key's shape (#3591 D2): `ailk_agt_` plus 32 random bytes in
|
|
206
217
|
* base64url. apps/mcp checks only this; apps/api is where the key is checked.
|
|
@@ -322,14 +333,25 @@ export async function registerHttpTransport(
|
|
|
322
333
|
});
|
|
323
334
|
|
|
324
335
|
// ── Rate limiting ─────────────────────────────────────────────────────────
|
|
325
|
-
// Global ceiling —
|
|
326
|
-
//
|
|
336
|
+
// Global ceiling — readPerMin per IP, counted on every request (writes
|
|
337
|
+
// included) by the plugin's own onRequest hook.
|
|
327
338
|
await app.register(rateLimit, {
|
|
328
339
|
max: config.rateLimits.readPerMin,
|
|
329
340
|
timeWindow: "1 minute",
|
|
330
341
|
keyGenerator: (request: FastifyRequest) => request.ip,
|
|
331
342
|
});
|
|
332
343
|
|
|
344
|
+
// The write bucket — writePerMin per IP, charged in the POST preHandler
|
|
345
|
+
// below once per write-tool call, after the body is parsed and classified
|
|
346
|
+
// (#9251). `createRateLimit()` gives it its own in-memory store with the
|
|
347
|
+
// ceiling's key generator and window; it is the plugin's seam for a second
|
|
348
|
+
// limiter on a request the ceiling has already counted (a hook from
|
|
349
|
+
// `app.rateLimit()` would skip such a request as already limited).
|
|
350
|
+
const writeLimit = app.createRateLimit({
|
|
351
|
+
max: config.rateLimits.writePerMin,
|
|
352
|
+
timeWindow: "1 minute",
|
|
353
|
+
});
|
|
354
|
+
|
|
333
355
|
// ── MCP route ─────────────────────────────────────────────────────────────
|
|
334
356
|
|
|
335
357
|
// GET — SSE stream for an existing session
|
|
@@ -362,13 +384,29 @@ export async function registerHttpTransport(
|
|
|
362
384
|
return;
|
|
363
385
|
}
|
|
364
386
|
|
|
365
|
-
// Write-tool rate limit (
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
387
|
+
// Write-tool rate limit (#9251) — writePerMin per IP, tighter than
|
|
388
|
+
// the ceiling and counting write-tool calls only. Charged once per
|
|
389
|
+
// call, like the abuse guard below, so a batch of N spends N units.
|
|
390
|
+
// The refusal is the one the ceiling emits — the plugin's four
|
|
391
|
+
// headers and Fastify's 429 error body — so a caller sees one
|
|
392
|
+
// rate-limit shape whichever bucket ran out, never a tool-level
|
|
393
|
+
// error (see ABUSE_REFUSAL_STATUS on why that distinction matters).
|
|
394
|
+
for (const _call of writeCalls) {
|
|
395
|
+
const limit = await writeLimit(request);
|
|
396
|
+
if (limit.isAllowed || !limit.isExceeded) continue;
|
|
397
|
+
reply
|
|
398
|
+
.code(429)
|
|
399
|
+
.header("x-ratelimit-limit", limit.max)
|
|
400
|
+
.header("x-ratelimit-remaining", 0)
|
|
401
|
+
.header("x-ratelimit-reset", limit.ttlInSeconds)
|
|
402
|
+
.header("retry-after", limit.ttlInSeconds)
|
|
403
|
+
.send({
|
|
404
|
+
statusCode: 429,
|
|
405
|
+
error: "Too Many Requests",
|
|
406
|
+
message: `Rate limit exceeded, retry in ${retryIn(limit.ttlInSeconds)}`,
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
372
410
|
|
|
373
411
|
// Abuse heuristics — charged ONCE PER CALL, not once per request, so a
|
|
374
412
|
// batch of N cannot spend one cap unit and execute N times. The whole
|
|
@@ -114,29 +114,3 @@ export function getMcpContentAdapter(
|
|
|
114
114
|
// untouched and gate only the four write methods (D13).
|
|
115
115
|
return readOnlyContentAdapter(adapter as MdxContentAdapter);
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The same resolution, narrowed to the MDX adapter's richer surface.
|
|
120
|
-
*
|
|
121
|
-
* `read_content` returns a page's raw MDX body, which only
|
|
122
|
-
* `MdxContentAdapter.getPageSource()` provides — the base `ContentAdapter`
|
|
123
|
-
* contract returns frontmatter only. Rather than cast, this asserts the
|
|
124
|
-
* capability on the adapter the resolver actually built, so a server pointed
|
|
125
|
-
* at a non-MDX `CONTENT_ADAPTER` gets a named error instead of a
|
|
126
|
-
* `getPageSource is not a function` TypeError. On the default `mdx` path the
|
|
127
|
-
* check always passes and behaviour is unchanged. Under `database`, both the
|
|
128
|
-
* fail-closed empty adapter and the read-only-wrapped database adapter also
|
|
129
|
-
* provide `getPageSource`, so the check passes there too (D5).
|
|
130
|
-
*/
|
|
131
|
-
export function getMcpMdxContentAdapter(
|
|
132
|
-
options: McpContentAdapterOptions = {},
|
|
133
|
-
): MdxContentAdapter {
|
|
134
|
-
const adapter = getMcpContentAdapter(options);
|
|
135
|
-
if (typeof (adapter as MdxContentAdapter).getPageSource !== "function") {
|
|
136
|
-
throw new Error(
|
|
137
|
-
"This tool needs the MDX adapter's getPageSource(): the adapter selected by " +
|
|
138
|
-
"CONTENT_ADAPTER does not provide a raw MDX body. Use CONTENT_ADAPTER=mdx (the default).",
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
return adapter as MdxContentAdapter;
|
|
142
|
-
}
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
*
|
|
19
19
|
* 1. Every content tool's own not-found/empty mapping fires with NO tool
|
|
20
20
|
* edit — `read_content` and `get_*` already treat `null` as not-found;
|
|
21
|
-
* `list_pages` and `search_content` already
|
|
21
|
+
* `list_pages` and `search_content` already answer their empty
|
|
22
|
+
* `ItemList` over `[]` (#7167: the routes' own envelopes).
|
|
22
23
|
* 2. "Before any content lookup" (NFR-5) is structural, not asserted —
|
|
23
24
|
* there is nothing here to look anything up in.
|
|
24
|
-
* 3. `
|
|
25
|
-
* capability check (
|
|
26
|
-
* never sees THAT named throw on the fail-closed path.
|
|
25
|
+
* 3. `readContentDocument`'s `typeof getPageSource === "function"`
|
|
26
|
+
* capability check (`@working-theory/content-adapters`) passes, so
|
|
27
|
+
* `read_content` never sees THAT named throw on the fail-closed path.
|
|
27
28
|
*/
|
|
28
29
|
import type {
|
|
29
30
|
ContentPage,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment's `_site.mdx`, read once per content root (#7169).
|
|
3
|
+
*
|
|
4
|
+
* `read_content` answers through `readContentDocument` in
|
|
5
|
+
* `@working-theory/content-adapters`, which takes the parsed site config for
|
|
6
|
+
* the `HomePage` graph and the `origin` read. That package may not import
|
|
7
|
+
* `@working-theory/site-config` (`architecture.yaml`), so the read stays with
|
|
8
|
+
* each app: this is the same reader `apps/api/src/routes/content/get-page.ts`
|
|
9
|
+
* carries — the one duplication the import rule leaves, kept identical so a
|
|
10
|
+
* `HomePage` read through the tool is the route's to the byte. The rejected
|
|
11
|
+
* alternative, `@working-theory/aeo`'s `loadSitewideIdentity`, applies its own
|
|
12
|
+
* acceptance rules and a parent-directory fallback, which could make the two
|
|
13
|
+
* differ on an edge site.
|
|
14
|
+
*
|
|
15
|
+
* A local read with the client-safe `/schema` entry:
|
|
16
|
+
* `@working-theory/site-config/server` imports `server-only`, which throws
|
|
17
|
+
* outside a React Server Components runtime.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
SiteConfigSchema,
|
|
24
|
+
type SiteConfig,
|
|
25
|
+
} from "@working-theory/site-config/schema";
|
|
26
|
+
import { parse as parseYaml } from "yaml";
|
|
27
|
+
|
|
28
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
29
|
+
|
|
30
|
+
/** `_site.mdx` per content root, read once; `null` when it does not load. */
|
|
31
|
+
const siteConfigByRoot = new Map<string, SiteConfig | null>();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The deployment's `_site.mdx`, validated against `SiteConfigSchema`, or
|
|
35
|
+
* `undefined` on any failure (absent, unreadable, invalid).
|
|
36
|
+
*/
|
|
37
|
+
export function readSiteConfig(contentRoot: string): SiteConfig | undefined {
|
|
38
|
+
if (!siteConfigByRoot.has(contentRoot)) {
|
|
39
|
+
let config: SiteConfig | null = null;
|
|
40
|
+
try {
|
|
41
|
+
const raw = readFileSync(join(contentRoot, "_site.mdx"), "utf8");
|
|
42
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
43
|
+
if (match) {
|
|
44
|
+
const parsed = SiteConfigSchema.safeParse(parseYaml(match[1] ?? ""));
|
|
45
|
+
if (parsed.success) config = parsed.data;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Absent or unreadable — no site.
|
|
49
|
+
}
|
|
50
|
+
siteConfigByRoot.set(contentRoot, config);
|
|
51
|
+
}
|
|
52
|
+
return siteConfigByRoot.get(contentRoot) ?? undefined;
|
|
53
|
+
}
|
|
@@ -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.
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* capture_lead — capture a
|
|
2
|
+
* capture_lead — capture a lead: the body `POST /v1/leads/submit` takes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The tool adopted the API, arguments and results (#7171 — the operator's
|
|
5
|
+
* decision of 2026-10-19). Its input IS the route's `leadSubmitBodySchema`
|
|
6
|
+
* (the catalog registers the same shape, so the SDK has already parsed the
|
|
7
|
+
* arguments against it; the `safeParse` here keeps the stdio path and the
|
|
8
|
+
* `invalid_input` branch honest), the parsed body is posted as the agent
|
|
9
|
+
* sent it — no `siteId`, no stamped `source`, no wrapping — and the route's
|
|
10
|
+
* 201 body `{ id, persisted, email }` comes back unchanged. One addition
|
|
11
|
+
* survives, because it is deployment attribution rather than an argument:
|
|
12
|
+
* the spoke marker `payload.site` from `AILK_LEAD_SITE` (#7397), spread LAST
|
|
13
|
+
* over the agent's `payload` as the site's own writer
|
|
14
|
+
* (`apps/web/src/lib/lead-write.ts`) does; unset or blank, the payload goes
|
|
15
|
+
* as sent. No business logic — orchestration lives in apps/api.
|
|
7
16
|
*
|
|
8
17
|
* Paired with captureLeadAction Server Action per ADR 0013.
|
|
9
18
|
*/
|
|
@@ -11,21 +20,21 @@
|
|
|
11
20
|
import { apiClient } from "@working-theory/api-client";
|
|
12
21
|
import {
|
|
13
22
|
LEAD_SITE_MARKER_KEY,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type
|
|
17
|
-
type
|
|
23
|
+
leadSubmitBodySchema,
|
|
24
|
+
leadWriteOutputSchema,
|
|
25
|
+
type LeadSubmitBody,
|
|
26
|
+
type LeadWriteOutput,
|
|
18
27
|
} from "@working-theory/validation";
|
|
19
28
|
|
|
20
29
|
import { visitorForwardHeadersForThisCall } from "../request-context.js";
|
|
21
30
|
|
|
22
|
-
export {
|
|
23
|
-
export type {
|
|
31
|
+
export { leadSubmitBodySchema, leadWriteOutputSchema };
|
|
32
|
+
export type { LeadSubmitBody, LeadWriteOutput };
|
|
24
33
|
|
|
25
34
|
export async function ailkCaptureLead(
|
|
26
|
-
input:
|
|
27
|
-
): Promise<
|
|
28
|
-
const parsed =
|
|
35
|
+
input: LeadSubmitBody,
|
|
36
|
+
): Promise<LeadWriteOutput> {
|
|
37
|
+
const parsed = leadSubmitBodySchema.safeParse(input);
|
|
29
38
|
if (!parsed.success) {
|
|
30
39
|
return {
|
|
31
40
|
ok: false,
|
|
@@ -33,21 +42,14 @@ export async function ailkCaptureLead(
|
|
|
33
42
|
};
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
const siteId = process.env.AILK_SITE_ID ?? "default";
|
|
38
|
-
// The spoke marker (#7397) — unset or blank leaves the payload unmarked.
|
|
45
|
+
// The spoke marker (#7397) — unset or blank leaves the payload as sent.
|
|
39
46
|
const site = process.env.AILK_LEAD_SITE?.trim();
|
|
40
|
-
const body =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
...(message !== undefined && { message }),
|
|
47
|
-
...(phone !== undefined && { phone }),
|
|
48
|
-
...(site && { [LEAD_SITE_MARKER_KEY]: site }),
|
|
49
|
-
},
|
|
50
|
-
};
|
|
47
|
+
const body = site
|
|
48
|
+
? {
|
|
49
|
+
...parsed.data,
|
|
50
|
+
payload: { ...parsed.data.payload, [LEAD_SITE_MARKER_KEY]: site },
|
|
51
|
+
}
|
|
52
|
+
: parsed.data;
|
|
51
53
|
|
|
52
54
|
// #8073 — forward the address the transport trusted (never an identity
|
|
53
55
|
// argument on this tool's own schema) so the hub counts the write in that
|
|
@@ -60,5 +62,5 @@ export async function ailkCaptureLead(
|
|
|
60
62
|
return { ok: false, error: { kind: "api_error", status: res.status } };
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
return
|
|
65
|
+
return res.body;
|
|
64
66
|
}
|
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 {
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
95
|
+
return listOfferings(adapter, { type: input.type, locale });
|
|
79
96
|
}
|