create-ailk 0.4.0 → 0.6.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 +55 -8
- package/component-catalog.json +12248 -0
- package/component-catalog.md +193 -68
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +60 -8
- package/dist/module-architecture.js +587 -242
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +209 -36
- package/dist/template-manifest.d.ts +88 -3
- package/dist/template-manifest.js +281 -29
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +2 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
- package/site-templates/minimalism-site/ailk-template.json +22 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +102 -12
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +29 -2
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +7 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +521 -55
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +300 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +160 -0
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +284 -0
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +58 -25
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +8 -2
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +61 -37
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +9 -7
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +74 -8
- package/templates/apps/web/CLAUDE.md +46 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +308 -65
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +15 -19
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
- package/templates/apps/web/messages/en.json +216 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +8 -3
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Abuse rule orchestrator for MCP write tools.
|
|
3
3
|
*
|
|
4
|
-
* Runs all in-process heuristics for the
|
|
5
|
-
* capture_lead, subscribe_newsletter, request_callback
|
|
4
|
+
* Runs all in-process heuristics for the anonymous write tools:
|
|
5
|
+
* capture_lead, subscribe_newsletter, request_callback, and — since #5971 —
|
|
6
|
+
* start_purchase.
|
|
6
7
|
*
|
|
7
8
|
* Rules applied (in order):
|
|
8
9
|
* 1. User-Agent deny list (applies to all write tools)
|
|
9
10
|
* 2. Honeypot field check (applies to all write tools)
|
|
10
11
|
* 3. Email format validation (capture_lead, subscribe_newsletter)
|
|
11
|
-
* 4. Per-IP daily cap (capture_lead
|
|
12
|
+
* 4. Per-IP daily cap (capture_lead and start_purchase, each on its OWN
|
|
13
|
+
* tracker — sharing one would let purchase attempts consume the lead
|
|
14
|
+
* budget and vice versa, a silent cross-tool denial of service)
|
|
12
15
|
*
|
|
13
16
|
* The paid layer (lead-routing.ts) extends with real abuse-protection
|
|
14
17
|
* services. OSS gets basic heuristics that stop obvious bots.
|
|
@@ -24,28 +27,142 @@ import { buildDenyList, isUserAgentBlocked } from "./user-agent-deny.js";
|
|
|
24
27
|
/** Tools that require email format validation on their input. */
|
|
25
28
|
const EMAIL_VALIDATED_TOOLS = new Set(["capture_lead", "subscribe_newsletter"]);
|
|
26
29
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
30
|
+
/** The per-tool daily-cap budgets, keyed by MCP tool name. */
|
|
31
|
+
type DailyCapKey = "captureLead" | "startPurchase";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The typed reason codes an {@link AbuseResult} refusal can carry.
|
|
35
|
+
*
|
|
36
|
+
* The code — not the prose in `reason` — is the contract between this module
|
|
37
|
+
* and its callers. `http-transport.ts` maps each code to an HTTP status
|
|
38
|
+
* (`ABUSE_REFUSAL_STATUS`), so a reworded refusal reason can never move a
|
|
39
|
+
* refusal between statuses, and a NEW code fails to compile until the
|
|
40
|
+
* transport declares a status for it (#5994).
|
|
41
|
+
*/
|
|
42
|
+
export const AbuseReasonCode = {
|
|
43
|
+
/** The caller's User-Agent is on the deny list. */
|
|
44
|
+
BLOCKED_USER_AGENT: "blocked_user_agent",
|
|
45
|
+
/** The submitted input filled a honeypot field. */
|
|
46
|
+
HONEYPOT_FILLED: "honeypot_filled",
|
|
47
|
+
/** The submitted input carried a malformed email address. */
|
|
48
|
+
INVALID_EMAIL: "invalid_email",
|
|
49
|
+
/** The caller's IP has spent this tool's per-IP daily budget. */
|
|
50
|
+
DAILY_CAP_REACHED: "daily_cap_reached",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
export type AbuseReasonCode =
|
|
54
|
+
(typeof AbuseReasonCode)[keyof typeof AbuseReasonCode];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tools that apply a per-IP daily cap, each mapped to the tracker that budgets
|
|
58
|
+
* it and the refusal reason it answers with. One tracker per entry — never
|
|
59
|
+
* shared across tools, since a shared budget would let purchase attempts
|
|
60
|
+
* consume the lead allowance and vice versa.
|
|
61
|
+
*
|
|
62
|
+
* The reason strings here are prose for the caller and nothing more: every
|
|
63
|
+
* entry refuses with {@link AbuseReasonCode.DAILY_CAP_REACHED}, and that code
|
|
64
|
+
* is what decides the HTTP status. Reword either one freely.
|
|
65
|
+
*/
|
|
66
|
+
const DAILY_CAPPED_TOOLS = new Map<
|
|
67
|
+
string,
|
|
68
|
+
{ tracker: DailyCapKey; reason: string }
|
|
69
|
+
>([
|
|
70
|
+
[
|
|
71
|
+
"capture_lead",
|
|
72
|
+
{
|
|
73
|
+
tracker: "captureLead",
|
|
74
|
+
reason: "daily capture_lead cap reached for this IP",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"start_purchase",
|
|
79
|
+
{
|
|
80
|
+
tracker: "startPurchase",
|
|
81
|
+
reason: "daily cap reached for start_purchase from this IP",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
]);
|
|
29
85
|
|
|
30
86
|
export interface AbuseConfig {
|
|
31
87
|
/** Per-IP daily cap for capture_lead. Default 50. */
|
|
32
88
|
captureLeadDailyCapPerIp: number;
|
|
89
|
+
/**
|
|
90
|
+
* Per-IP daily cap for start_purchase (#5971). Default 20 — lower than
|
|
91
|
+
* capture_lead's, because each call creates a real Stripe Checkout Session.
|
|
92
|
+
* Optional so existing callers that construct an AbuseConfig without it
|
|
93
|
+
* keep compiling; omitted falls back to the default.
|
|
94
|
+
*/
|
|
95
|
+
startPurchaseDailyCapPerIp?: number;
|
|
33
96
|
/** Comma-separated extra UA substrings to block. */
|
|
34
97
|
blockedUserAgents: string;
|
|
35
98
|
}
|
|
36
99
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
100
|
+
/** Default per-IP daily cap for start_purchase when none is configured. */
|
|
101
|
+
export const DEFAULT_START_PURCHASE_DAILY_CAP = 20;
|
|
102
|
+
|
|
103
|
+
/** Default per-IP daily cap for capture_lead when none is configured. */
|
|
104
|
+
export const DEFAULT_CAPTURE_LEAD_DAILY_CAP = 50;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Reads a positive-integer env var, falling back to `fallback` for anything
|
|
108
|
+
* that is not one.
|
|
109
|
+
*
|
|
110
|
+
* `parseInt(process.env[X] ?? "20", 10)` is NOT enough, and the gap is a
|
|
111
|
+
* silent fail-OPEN on a security threshold (#5971 security review round 2):
|
|
112
|
+
* `?? "20"` fires only when the var is UNSET, so a var that is present but
|
|
113
|
+
* BLANK — the shape `.env.example` itself teaches for three neighbouring
|
|
114
|
+
* vars — reaches `parseInt("")` and yields `NaN`. `NaN` then survives every
|
|
115
|
+
* downstream `??` guard (nullish coalescing catches null/undefined, not NaN),
|
|
116
|
+
* and `count >= NaN` is always false, so the cap silently stops refusing
|
|
117
|
+
* anything. A misconfigured deployment looks identical to a correct one at
|
|
118
|
+
* rest, which is exactly why this needs a guard rather than a convention.
|
|
119
|
+
*/
|
|
120
|
+
export function readPositiveIntEnv(name: string, fallback: number): number {
|
|
121
|
+
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
122
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Coerces a configured cap to a usable one, defaulting anything non-finite
|
|
126
|
+
* or non-positive. The in-process twin of {@link readPositiveIntEnv}, for a
|
|
127
|
+
* cap that arrives through {@link AbuseConfig} rather than the environment. */
|
|
128
|
+
function usableCap(value: number | undefined, fallback: number): number {
|
|
129
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
130
|
+
? value
|
|
131
|
+
: fallback;
|
|
40
132
|
}
|
|
41
133
|
|
|
134
|
+
/** A refusal: always carries both the typed code and its caller-facing prose. */
|
|
135
|
+
export interface AbuseRefusal {
|
|
136
|
+
allowed: false;
|
|
137
|
+
/** What kind of refusal this is — the machine-readable half. */
|
|
138
|
+
code: AbuseReasonCode;
|
|
139
|
+
/** Caller-facing prose. Free to reword; nothing keys control flow on it. */
|
|
140
|
+
reason: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The verdict of {@link AbuseGuard.validate}. */
|
|
144
|
+
export type AbuseResult = { allowed: true } | AbuseRefusal;
|
|
145
|
+
|
|
42
146
|
export class AbuseGuard {
|
|
43
147
|
private readonly denyList: RegExp[];
|
|
44
|
-
private readonly
|
|
148
|
+
private readonly caps: Record<DailyCapKey, DailyCapTracker>;
|
|
45
149
|
|
|
46
150
|
constructor(config: AbuseConfig) {
|
|
47
151
|
this.denyList = buildDenyList(config.blockedUserAgents);
|
|
48
|
-
this.
|
|
152
|
+
this.caps = {
|
|
153
|
+
captureLead: new DailyCapTracker(
|
|
154
|
+
usableCap(
|
|
155
|
+
config.captureLeadDailyCapPerIp,
|
|
156
|
+
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
startPurchase: new DailyCapTracker(
|
|
160
|
+
usableCap(
|
|
161
|
+
config.startPurchaseDailyCapPerIp,
|
|
162
|
+
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
};
|
|
49
166
|
}
|
|
50
167
|
|
|
51
168
|
/**
|
|
@@ -65,12 +182,20 @@ export class AbuseGuard {
|
|
|
65
182
|
|
|
66
183
|
// Rule 1: User-Agent deny list
|
|
67
184
|
if (isUserAgentBlocked(ua, this.denyList)) {
|
|
68
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
allowed: false,
|
|
187
|
+
code: AbuseReasonCode.BLOCKED_USER_AGENT,
|
|
188
|
+
reason: "blocked user agent",
|
|
189
|
+
};
|
|
69
190
|
}
|
|
70
191
|
|
|
71
192
|
// Rule 2: Honeypot
|
|
72
193
|
if (isHoneypotTripped(input)) {
|
|
73
|
-
return {
|
|
194
|
+
return {
|
|
195
|
+
allowed: false,
|
|
196
|
+
code: AbuseReasonCode.HONEYPOT_FILLED,
|
|
197
|
+
reason: "honeypot field filled",
|
|
198
|
+
};
|
|
74
199
|
}
|
|
75
200
|
|
|
76
201
|
// Rule 3: Email format validation
|
|
@@ -78,33 +203,43 @@ export class AbuseGuard {
|
|
|
78
203
|
const rec = input as Record<string, unknown>;
|
|
79
204
|
const email = rec["email"];
|
|
80
205
|
if (typeof email === "string" && !isValidEmail(email)) {
|
|
81
|
-
return { allowed: false, reason: "invalid email format" };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Rule 4: Per-IP daily cap
|
|
86
|
-
if (DAILY_CAPPED_TOOLS.has(toolName)) {
|
|
87
|
-
if (!this.captureLeadCap.isAllowed(ip)) {
|
|
88
206
|
return {
|
|
89
207
|
allowed: false,
|
|
90
|
-
|
|
208
|
+
code: AbuseReasonCode.INVALID_EMAIL,
|
|
209
|
+
reason: "invalid email format",
|
|
91
210
|
};
|
|
92
211
|
}
|
|
93
212
|
}
|
|
94
213
|
|
|
214
|
+
// Rule 4: Per-IP daily cap, on the tool's OWN tracker and with the
|
|
215
|
+
// tool's OWN refusal prose — one shared code, see DAILY_CAPPED_TOOLS.
|
|
216
|
+
const cap = DAILY_CAPPED_TOOLS.get(toolName);
|
|
217
|
+
if (cap !== undefined && !this.caps[cap.tracker].isAllowed(ip)) {
|
|
218
|
+
return {
|
|
219
|
+
allowed: false,
|
|
220
|
+
code: AbuseReasonCode.DAILY_CAP_REACHED,
|
|
221
|
+
reason: cap.reason,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
95
225
|
return { allowed: true };
|
|
96
226
|
}
|
|
97
227
|
}
|
|
98
228
|
|
|
99
229
|
/**
|
|
100
230
|
* Convenience function for constructing an AbuseGuard from env vars.
|
|
101
|
-
* Reads MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP
|
|
231
|
+
* Reads MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP,
|
|
232
|
+
* MCP_ABUSE_START_PURCHASE_DAILY_CAP and MCP_ABUSE_BLOCKED_USER_AGENTS.
|
|
102
233
|
*/
|
|
103
234
|
export function createAbuseGuardFromEnv(): AbuseGuard {
|
|
104
235
|
return new AbuseGuard({
|
|
105
|
-
captureLeadDailyCapPerIp:
|
|
106
|
-
|
|
107
|
-
|
|
236
|
+
captureLeadDailyCapPerIp: readPositiveIntEnv(
|
|
237
|
+
"MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
|
|
238
|
+
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
239
|
+
),
|
|
240
|
+
startPurchaseDailyCapPerIp: readPositiveIntEnv(
|
|
241
|
+
"MCP_ABUSE_START_PURCHASE_DAILY_CAP",
|
|
242
|
+
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
108
243
|
),
|
|
109
244
|
blockedUserAgents: process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "",
|
|
110
245
|
});
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
* Recognized module keys: `docs`, `multi-tenant`, `scheduling`,
|
|
15
15
|
* `signal-collectors` (the `ModuleName` vocabulary
|
|
16
16
|
* `packages/create-ailk/src/module-architecture.ts` already models via
|
|
17
|
-
* `MODULE_SLICES`) plus `content-write` — the Pro
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* `MODULE_SLICES`) plus `content-write` — the Pro content-write surface,
|
|
18
|
+
* which owns NO MCP tool since #5853: `create_page`/`update_page`/`delete_page`
|
|
19
|
+
* were retired by deletion along with their untenanted HTTP twins, and their
|
|
20
|
+
* replacement is identity-bound, which the identity-blind MCP surface cannot
|
|
21
|
+
* carry (D7, D15; the authed MCP surface is #3591). The flag is read here only
|
|
22
|
+
* because this reader is shared with `apps/api`, where it still gates the
|
|
23
|
+
* tenant-guarded editor routes. `content-write` is intentionally NOT a
|
|
24
|
+
* `MODULE_SLICES` entry: it is not (yet) a create-ailk `--modules` selector,
|
|
25
|
+
* so it stays a local flag here rather than widening the create-ailk module
|
|
26
|
+
* vocabulary.
|
|
22
27
|
*/
|
|
23
28
|
|
|
24
29
|
function enabledModules(): Set<string> {
|
|
@@ -27,14 +27,55 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
|
27
27
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
28
28
|
import Fastify, { type FastifyInstance, type FastifyRequest } from "fastify";
|
|
29
29
|
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
AbuseGuard,
|
|
32
|
+
AbuseReasonCode,
|
|
33
|
+
type AbuseConfig,
|
|
34
|
+
} from "./abuse/index.js";
|
|
31
35
|
import { createMcpServer } from "./server.js";
|
|
32
36
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
37
|
+
/**
|
|
38
|
+
* HTTP status for each {@link AbuseReasonCode} an AbuseGuard refusal can
|
|
39
|
+
* carry (#5994).
|
|
40
|
+
*
|
|
41
|
+
* A refusal of the CALLER — a denied User-Agent, an IP that has spent its
|
|
42
|
+
* daily budget — is **403**: the input was fine, the caller is not allowed to
|
|
43
|
+
* make the call right now. A refusal of the INPUT — a filled honeypot, a
|
|
44
|
+
* malformed email — is **422**.
|
|
45
|
+
*
|
|
46
|
+
* Keying on the code rather than on the refusal prose is what this table is
|
|
47
|
+
* for: the split used to be `reason.includes("daily cap")`, so a reworded
|
|
48
|
+
* reason silently demoted a rate-limit refusal to 422 — and `capture_lead`'s
|
|
49
|
+
* wording only matched by accident, because the tool's name begins with
|
|
50
|
+
* "cap". Typing the record over the full union also means a new reason code
|
|
51
|
+
* fails to compile here until it declares its status.
|
|
52
|
+
*/
|
|
53
|
+
export const ABUSE_REFUSAL_STATUS: Record<AbuseReasonCode, 403 | 422> = {
|
|
54
|
+
[AbuseReasonCode.BLOCKED_USER_AGENT]: 403,
|
|
55
|
+
[AbuseReasonCode.DAILY_CAP_REACHED]: 403,
|
|
56
|
+
[AbuseReasonCode.HONEYPOT_FILLED]: 422,
|
|
57
|
+
[AbuseReasonCode.INVALID_EMAIL]: 422,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* MCP tool names that are write operations (subject to stricter CORS + rate
|
|
62
|
+
* limits, and to the AbuseGuard).
|
|
63
|
+
*
|
|
64
|
+
* `start_purchase` joined in #5971: it has a side effect at Stripe and it is
|
|
65
|
+
* worth money to abuse, so it takes the write branch — the origin allow-list,
|
|
66
|
+
* the write CORS posture, and `abuseGuard.validate()` — rather than the read
|
|
67
|
+
* branch's UA check alone.
|
|
68
|
+
*/
|
|
69
|
+
// Any name added here MUST be enforced through the batch-aware path in
|
|
70
|
+
// writeToolCalls() below — a request body can be a JSON-RPC ARRAY, and a
|
|
71
|
+
// per-request (rather than per-message) check lets N batched calls through on
|
|
72
|
+
// one cap unit. See the #5971 security review and the batch regression tests
|
|
73
|
+
// in __tests__/http-transport.test.ts.
|
|
74
|
+
export const WRITE_TOOLS = new Set([
|
|
35
75
|
"capture_lead",
|
|
36
76
|
"subscribe_newsletter",
|
|
37
77
|
"request_callback",
|
|
78
|
+
"start_purchase",
|
|
38
79
|
]);
|
|
39
80
|
|
|
40
81
|
export interface HttpTransportCorsConfig {
|
|
@@ -71,17 +112,49 @@ export interface HttpTransportConfig {
|
|
|
71
112
|
stateless?: boolean;
|
|
72
113
|
}
|
|
73
114
|
|
|
115
|
+
/** One write-tool invocation found in a request body. */
|
|
116
|
+
interface WriteToolCall {
|
|
117
|
+
toolName: string;
|
|
118
|
+
args: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
74
121
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
122
|
+
* Every write-tool invocation carried by a request body, in order.
|
|
123
|
+
*
|
|
124
|
+
* JSON-RPC 2.0 allows a BATCH: an array of messages in one POST. The MCP SDK
|
|
125
|
+
* honours it — `webStandardStreamableHttp.js` maps an array body to N parsed
|
|
126
|
+
* messages and dispatches every one. So a body must be normalised to a LIST of
|
|
127
|
+
* messages before any of them is classified; inspecting `body.method` on what
|
|
128
|
+
* may be an array silently answers "not a write" for the whole batch, and the
|
|
129
|
+
* request then takes the read branch: no write-origin check, and no
|
|
130
|
+
* `AbuseGuard` cap unit consumed by ANY of the N calls it goes on to execute.
|
|
131
|
+
*
|
|
132
|
+
* That is a real bypass of the per-IP daily cap, and for `start_purchase` each
|
|
133
|
+
* bypassed call creates a genuine Stripe Checkout Session on the seller's
|
|
134
|
+
* account. It predates `start_purchase` — `capture_lead`,
|
|
135
|
+
* `subscribe_newsletter` and `request_callback` have carried it since this
|
|
136
|
+
* transport was written — and normalising here closes it for all four at once
|
|
137
|
+
* (#5971 security review, High).
|
|
138
|
+
*
|
|
139
|
+
* Returns one entry PER matching message, so the caller can charge the guard
|
|
140
|
+
* once per call rather than once per request.
|
|
77
141
|
*/
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
142
|
+
function writeToolCalls(body: unknown): WriteToolCall[] {
|
|
143
|
+
const messages = Array.isArray(body) ? body : [body];
|
|
144
|
+
const calls: WriteToolCall[] = [];
|
|
145
|
+
|
|
146
|
+
for (const message of messages) {
|
|
147
|
+
if (!message || typeof message !== "object") continue;
|
|
148
|
+
const req = message as Record<string, unknown>;
|
|
149
|
+
if (req["method"] !== "tools/call") continue;
|
|
150
|
+
const params = req["params"] as Record<string, unknown> | undefined;
|
|
151
|
+
if (!params) continue;
|
|
152
|
+
const toolName = params["name"];
|
|
153
|
+
if (typeof toolName !== "string" || !WRITE_TOOLS.has(toolName)) continue;
|
|
154
|
+
calls.push({ toolName, args: params["arguments"] ?? {} });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return calls;
|
|
85
158
|
}
|
|
86
159
|
|
|
87
160
|
/**
|
|
@@ -154,8 +227,12 @@ export async function buildHttpTransportApp(
|
|
|
154
227
|
const body = request.body as unknown;
|
|
155
228
|
const origin = request.headers["origin"] as string | undefined;
|
|
156
229
|
|
|
230
|
+
// A batch body counts as a write request when ANY of its messages names
|
|
231
|
+
// a write tool — see writeToolCalls().
|
|
232
|
+
const writeCalls = writeToolCalls(body);
|
|
233
|
+
|
|
157
234
|
// Write-tool CORS enforcement
|
|
158
|
-
if (
|
|
235
|
+
if (writeCalls.length > 0) {
|
|
159
236
|
if (!isOriginAllowedForWrite(origin, config.cors.writeAllowedOrigins)) {
|
|
160
237
|
reply
|
|
161
238
|
.code(403)
|
|
@@ -171,22 +248,18 @@ export async function buildHttpTransportApp(
|
|
|
171
248
|
// TODO: per-tool write rate limit when @fastify/rate-limit v11 lands
|
|
172
249
|
// a per-route override API compatible with our stateless route design.
|
|
173
250
|
|
|
174
|
-
// Abuse heuristics
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const toolName
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
? 403
|
|
187
|
-
: 422;
|
|
188
|
-
reply.code(statusCode).send({ error: abuseResult.reason });
|
|
189
|
-
return;
|
|
251
|
+
// Abuse heuristics — charged ONCE PER CALL, not once per request, so a
|
|
252
|
+
// batch of N cannot spend one cap unit and execute N times. The whole
|
|
253
|
+
// request is refused on the first message that fails, before the SDK
|
|
254
|
+
// dispatches any of them.
|
|
255
|
+
for (const { toolName, args } of writeCalls) {
|
|
256
|
+
const abuseResult = abuseGuard.validate(toolName, args, request);
|
|
257
|
+
if (!abuseResult.allowed) {
|
|
258
|
+
reply
|
|
259
|
+
.code(ABUSE_REFUSAL_STATUS[abuseResult.code])
|
|
260
|
+
.send({ error: abuseResult.reason });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
190
263
|
}
|
|
191
264
|
} else {
|
|
192
265
|
// Read-tool or protocol message: check UA deny list only
|
|
@@ -13,11 +13,59 @@
|
|
|
13
13
|
* MCP_RATE_LIMIT_WRITE_PER_MIN=10
|
|
14
14
|
* MCP_CORS_WRITE_ALLOWED_ORIGINS= (empty = no cross-origin writes)
|
|
15
15
|
* MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
|
|
16
|
+
* MCP_ABUSE_START_PURCHASE_DAILY_CAP=20
|
|
16
17
|
* MCP_ABUSE_BLOCKED_USER_AGENTS= (comma-separated substrings)
|
|
18
|
+
*
|
|
19
|
+
* Content adapter (all optional; `CONTENT_ADAPTER` default: mdx):
|
|
20
|
+
* CONTENT_ADAPTER=mdx|projection|database
|
|
21
|
+
* DATABASE_URL= (required only when CONTENT_ADAPTER=database)
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
import { initMcpCommerceClaim } from "./lib/commerce-claim.js";
|
|
25
|
+
import { initMcpPublicWorkspace } from "./lib/public-workspace.js";
|
|
26
|
+
|
|
27
|
+
// Resolves the MCP server's own workspace ONCE, before either transport
|
|
28
|
+
// branch below — a no-op unless CONTENT_ADAPTER=database (#5876 spec D4/D6).
|
|
29
|
+
// This is the one `await` that covers both arms: `getMcpContentAdapter()`
|
|
30
|
+
// (content-adapter.ts) stays synchronous and reads the memoised result.
|
|
31
|
+
//
|
|
32
|
+
// A REJECTION here (a transient DB error — unreachable at boot, a migration
|
|
33
|
+
// in progress, a bad DATABASE_URL) is caught rather than left to crash the
|
|
34
|
+
// process: every OTHER tool (lead capture, scheduling, …) has nothing to do
|
|
35
|
+
// with the content adapter and must keep serving. The module's un-resolved
|
|
36
|
+
// `{ kind: "not-required" }` state is left in place on a caught failure —
|
|
37
|
+
// the SAME fail-closed shape an ambiguous workspace count already produces
|
|
38
|
+
// (D5) — so content tools serve their empty/not-found shape rather than the
|
|
39
|
+
// whole server going dark over one adapter's connectivity.
|
|
40
|
+
try {
|
|
41
|
+
await initMcpPublicWorkspace();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(
|
|
44
|
+
"[mcp] initMcpPublicWorkspace() failed — content tools will serve their " +
|
|
45
|
+
"fail-closed empty shape under CONTENT_ADAPTER=database until this " +
|
|
46
|
+
"resolves; every other tool is unaffected:",
|
|
47
|
+
err,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Resolves the deployment's `commerce` add-on claim ONCE, before either
|
|
52
|
+
// transport branch — the sibling of the workspace resolution above and the
|
|
53
|
+
// same #5876 idiom (#5971 spec D2). Without this await the memoised claim is
|
|
54
|
+
// never resolved and `start_purchase` is invisible on a KEYED site too.
|
|
55
|
+
//
|
|
56
|
+
// A REJECTION is caught for the same reason: a licence read that fails must
|
|
57
|
+
// not take the other 19 tools down. `initMcpCommerceClaim()` already resolves
|
|
58
|
+
// to the fail-closed "unclaimed" internally, so this catch is belt-and-braces
|
|
59
|
+
// for anything thrown outside its own try.
|
|
60
|
+
try {
|
|
61
|
+
await initMcpCommerceClaim();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(
|
|
64
|
+
"[mcp] initMcpCommerceClaim() failed — start_purchase stays unregistered " +
|
|
65
|
+
"(fail-closed) until this resolves; every other tool is unaffected:",
|
|
66
|
+
err,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
21
69
|
|
|
22
70
|
const transport = process.env["MCP_TRANSPORT"] ?? "stdio";
|
|
23
71
|
|
|
@@ -38,27 +86,41 @@ if (transport === "http") {
|
|
|
38
86
|
const writeAllowedOrigins = writeOriginsRaw
|
|
39
87
|
? writeOriginsRaw.split(",").map((o) => o.trim())
|
|
40
88
|
: [];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
// Both caps go through readPositiveIntEnv rather than a bare parseInt: a
|
|
90
|
+
// BLANK env var would otherwise yield NaN and silently disable the cap
|
|
91
|
+
// (#5971 security review round 2).
|
|
92
|
+
const {
|
|
93
|
+
readPositiveIntEnv,
|
|
94
|
+
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
95
|
+
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
96
|
+
} = await import("./abuse/index.js");
|
|
97
|
+
const captureLeadDailyCapPerIp = readPositiveIntEnv(
|
|
98
|
+
"MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
|
|
99
|
+
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
44
100
|
);
|
|
45
|
-
const
|
|
46
|
-
|
|
101
|
+
const startPurchaseDailyCapPerIp = readPositiveIntEnv(
|
|
102
|
+
"MCP_ABUSE_START_PURCHASE_DAILY_CAP",
|
|
103
|
+
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
104
|
+
);
|
|
105
|
+
const blockedUserAgents = process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "";
|
|
47
106
|
|
|
48
107
|
await startHttpTransport({
|
|
49
108
|
port,
|
|
50
109
|
path,
|
|
51
110
|
rateLimits: { readPerMin, writePerMin },
|
|
52
111
|
cors: { writeAllowedOrigins },
|
|
53
|
-
abuse: {
|
|
112
|
+
abuse: {
|
|
113
|
+
captureLeadDailyCapPerIp,
|
|
114
|
+
startPurchaseDailyCapPerIp,
|
|
115
|
+
blockedUserAgents,
|
|
116
|
+
},
|
|
54
117
|
});
|
|
55
118
|
|
|
56
119
|
console.log(`MCP HTTP transport listening on port ${port} at ${path}`);
|
|
57
120
|
} else {
|
|
58
121
|
// Default: stdio transport
|
|
59
|
-
const { StdioServerTransport } =
|
|
60
|
-
"@modelcontextprotocol/sdk/server/stdio.js"
|
|
61
|
-
);
|
|
122
|
+
const { StdioServerTransport } =
|
|
123
|
+
await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
62
124
|
const { createMcpServer } = await import("./server.js");
|
|
63
125
|
|
|
64
126
|
const server = createMcpServer();
|