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.
- package/component-catalog.json +186 -8
- package/component-catalog.md +9 -7
- package/dist/add-module.js +20 -14
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +12 -3
- package/dist/derive-architecture.js +20 -8
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +5 -5
- 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 +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +99 -9
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/programmatic.js +19 -5
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/spoke-checkout.d.ts +65 -41
- package/dist/spoke-checkout.js +208 -430
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +52 -26
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +187 -13
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +8 -4
- package/dist/topology.js +25 -1
- package/dist/update.js +15 -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/designs/minimalism/DESIGN.md +1 -1
- 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 +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +87 -17
- 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 +3 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/lib/stripe.ts +74 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- 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/auth.ts +13 -6
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +33 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -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 +1300 -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 +386 -0
- 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 +268 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- 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 +4 -0
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- 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/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +164 -7
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -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 +5 -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 +245 -36
- package/templates/apps/api/src/server.ts +163 -46
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -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 +1383 -133
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +431 -47
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -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 +868 -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 +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -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 +164 -17
- package/templates/apps/api/src/services/plan-offerings.ts +125 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
- package/templates/apps/mcp/CLAUDE.md +22 -14
- 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 +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- 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__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- 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/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +24 -31
- package/templates/apps/web/CLAUDE.md +393 -9
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -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 +141 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
- 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__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -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]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -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]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- 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]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- 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]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- 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/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- 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/auth.ts +28 -2
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -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 +101 -25
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +16 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +7 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -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/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +71 -20
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -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 +69 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -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 +64 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -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/package.json +1 -1
- package/templates/database/seed.ts +62 -3
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
- 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/lib/__tests__/meeting-slugs.test.ts +0 -120
- 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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
104
|
+
await ailkPurchase({ productCode: "growth_monthly" });
|
|
103
105
|
|
|
104
106
|
expect(createSession).toHaveBeenCalledWith({
|
|
105
|
-
productCode: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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-
|
|
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-
|
|
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-
|
|
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: "
|
|
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: "
|
|
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");
|
|
@@ -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
|
-
*
|
|
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
|
}
|