create-ailk 0.1.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/LICENSE +201 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +80 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +109 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lib/pro-callout.d.ts +12 -0
- package/dist/lib/pro-callout.js +20 -0
- package/dist/parse-args.d.ts +13 -0
- package/dist/parse-args.js +81 -0
- package/dist/programmatic.d.ts +49 -0
- package/dist/programmatic.js +89 -0
- package/dist/substitute.d.ts +31 -0
- package/dist/substitute.js +45 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +70 -0
- package/package.json +47 -0
- package/templates/.claude/.gitkeep +0 -0
- package/templates/.claude/agents/web.md +80 -0
- package/templates/.claude/rules/architecture.md +62 -0
- package/templates/.claude/rules/preserve-existing-style.md +51 -0
- package/templates/.claude/skills/README.md +43 -0
- package/templates/.claude/skills/add-schema/SKILL.md +270 -0
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
- package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
- package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
- package/templates/.claude/skills/audit-site/SKILL.md +213 -0
- package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
- package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
- package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
- package/templates/.claude/skills/provision-config/SKILL.md +243 -0
- package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
- package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
- package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
- package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
- package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
- package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
- package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
- package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
- package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
- package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
- package/templates/.env.example +205 -0
- package/templates/.gitkeep +2 -0
- package/templates/CLAUDE.md +142 -0
- package/templates/CONVENTIONS.md +222 -0
- package/templates/README.md +110 -0
- package/templates/apps/api/.env.example +40 -0
- package/templates/apps/api/CLAUDE.md +63 -0
- package/templates/apps/api/api/index.ts +144 -0
- package/templates/apps/api/jest.config.cjs +74 -0
- package/templates/apps/api/package.json +54 -0
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
- package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
- package/templates/apps/api/src/__tests__/index.test.ts +118 -0
- package/templates/apps/api/src/__tests__/server.test.ts +207 -0
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
- package/templates/apps/api/src/__tests__/setup.ts +24 -0
- package/templates/apps/api/src/cli.ts +30 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
- package/templates/apps/api/src/config/index.ts +56 -0
- package/templates/apps/api/src/index.ts +32 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
- package/templates/apps/api/src/lib/content-adapter.ts +86 -0
- package/templates/apps/api/src/lib/error-serializer.ts +77 -0
- package/templates/apps/api/src/lib/errors.ts +95 -0
- package/templates/apps/api/src/lib/file-fallback.ts +87 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
- package/templates/apps/api/src/lib/locales.ts +201 -0
- package/templates/apps/api/src/lib/outbound-http.ts +122 -0
- package/templates/apps/api/src/lib/prisma.ts +21 -0
- package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
- package/templates/apps/api/src/lib/stripe.ts +30 -0
- package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
- package/templates/apps/api/src/middleware/auth.ts +246 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
- package/templates/apps/api/src/openapi/index.ts +12 -0
- package/templates/apps/api/src/openapi/spec.ts +408 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
- package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
- package/templates/apps/api/src/routes/auth/user.ts +244 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
- package/templates/apps/api/src/routes/checkout/index.ts +13 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
- package/templates/apps/api/src/routes/content/create.ts +114 -0
- package/templates/apps/api/src/routes/content/delete.ts +81 -0
- package/templates/apps/api/src/routes/content/get-page.ts +98 -0
- package/templates/apps/api/src/routes/content/index.ts +32 -0
- package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
- package/templates/apps/api/src/routes/content/list-types.ts +19 -0
- package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
- package/templates/apps/api/src/routes/content/slug.ts +9 -0
- package/templates/apps/api/src/routes/content/update.ts +111 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
- package/templates/apps/api/src/routes/leads/index.ts +130 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
- package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
- package/templates/apps/api/src/routes/webhooks/README.md +57 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
- package/templates/apps/api/src/server.ts +157 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
- package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
- package/templates/apps/api/src/services/lead-routing.ts +338 -0
- package/templates/apps/api/tsconfig.json +34 -0
- package/templates/apps/api/vercel.json +17 -0
- package/templates/apps/mcp/.env.example +52 -0
- package/templates/apps/mcp/CLAUDE.md +84 -0
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
- package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
- package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
- package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/server.test.ts +115 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
- package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
- package/templates/apps/mcp/jest.config.cjs +82 -0
- package/templates/apps/mcp/package.json +37 -0
- package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
- package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
- package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
- package/templates/apps/mcp/src/abuse/index.ts +111 -0
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
- package/templates/apps/mcp/src/http-transport.ts +303 -0
- package/templates/apps/mcp/src/index.ts +67 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
- package/templates/apps/mcp/src/lib/errors.ts +61 -0
- package/templates/apps/mcp/src/lib/locales.ts +171 -0
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
- package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
- package/templates/apps/mcp/src/server.ts +139 -0
- package/templates/apps/mcp/src/tools/audit.ts +103 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
- package/templates/apps/mcp/src/tools/create_page.ts +95 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
- package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
- package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
- package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
- package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
- package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
- package/templates/apps/mcp/src/tools/index.ts +125 -0
- package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
- package/templates/apps/mcp/src/tools/purchase.ts +89 -0
- package/templates/apps/mcp/src/tools/read_content.ts +61 -0
- package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
- package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
- package/templates/apps/mcp/src/tools/search_content.ts +59 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
- package/templates/apps/mcp/src/tools/update_page.ts +91 -0
- package/templates/apps/mcp/tsconfig.json +19 -0
- package/templates/apps/web/.env.example +47 -0
- package/templates/apps/web/CLAUDE.md +81 -0
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
- package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
- package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
- package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
- package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
- package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
- package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
- package/templates/apps/web/__tests__/page.test.tsx +230 -0
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
- package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
- package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
- package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
- package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
- package/templates/apps/web/__tests__/setup.ts +82 -0
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
- package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
- package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
- package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
- package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/layout.tsx +241 -0
- package/templates/apps/web/app/[locale]/page.tsx +41 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
- package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
- package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
- package/templates/apps/web/app/error-fallback.tsx +75 -0
- package/templates/apps/web/app/error.tsx +23 -0
- package/templates/apps/web/app/layout.tsx +18 -0
- package/templates/apps/web/app/llms.txt/route.ts +101 -0
- package/templates/apps/web/app/not-found.tsx +48 -0
- package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
- package/templates/apps/web/app/og/route.tsx +135 -0
- package/templates/apps/web/app/robots.txt/route.ts +36 -0
- package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
- package/templates/apps/web/auth.ts +141 -0
- package/templates/apps/web/components/ThemeToggle.tsx +60 -0
- package/templates/apps/web/components/google-analytics.tsx +34 -0
- package/templates/apps/web/content/en/blog/index.mdx +15 -0
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
- package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
- package/templates/apps/web/i18n/request.ts +36 -0
- package/templates/apps/web/jest.config.cjs +100 -0
- package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
- package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
- package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
- package/templates/apps/web/lib/auth-env.ts +66 -0
- package/templates/apps/web/lib/blog-list-data.ts +95 -0
- package/templates/apps/web/lib/blog-post-data.ts +79 -0
- package/templates/apps/web/lib/nav-config.ts +13 -0
- package/templates/apps/web/lib/observability/better-stack.ts +27 -0
- package/templates/apps/web/lib/og.ts +23 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
- package/templates/apps/web/lib/source.ts +34 -0
- package/templates/apps/web/messages/en.json +131 -0
- package/templates/apps/web/middleware.ts +80 -0
- package/templates/apps/web/next.config.mjs +167 -0
- package/templates/apps/web/package.json +62 -0
- package/templates/apps/web/public/.well-known/security.txt +17 -0
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +4 -0
- package/templates/apps/web/public/logo-dark.svg +21 -0
- package/templates/apps/web/public/logo-light.svg +21 -0
- package/templates/apps/web/public/logomark-dark.svg +4 -0
- package/templates/apps/web/public/logomark-light.svg +4 -0
- package/templates/apps/web/public/og-default.png +0 -0
- package/templates/apps/web/public/site.webmanifest +19 -0
- package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
- package/templates/apps/web/security-headers.config.ts +132 -0
- package/templates/apps/web/sentry.client.config.ts +27 -0
- package/templates/apps/web/sentry.edge.config.ts +15 -0
- package/templates/apps/web/sentry.server.config.ts +20 -0
- package/templates/apps/web/source.config.ts +78 -0
- package/templates/apps/web/src/components/SiteNav.tsx +21 -0
- package/templates/apps/web/src/lib/routes.ts +37 -0
- package/templates/apps/web/tsconfig.json +35 -0
- package/templates/apps/web/types/messages.d.ts +19 -0
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +85 -0
- package/templates/content/_site.mdx +38 -0
- package/templates/content/ar/HomePage/home.mdx +123 -0
- package/templates/content/ar/_site.mdx +35 -0
- package/templates/content/de/_site.mdx +29 -0
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.mdx +19 -0
- package/templates/content/en/AboutPage/about.schema.json +82 -0
- package/templates/content/en/BookCall/book-call.mdx +19 -0
- package/templates/content/en/BookCall/book-call.schema.json +121 -0
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
- package/templates/content/en/CareersList/careers.mdx +24 -0
- package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
- package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
- package/templates/content/en/ClientsList/clients.mdx +38 -0
- package/templates/content/en/ContactPage/contact.mdx +19 -0
- package/templates/content/en/ContactPage/contact.schema.json +100 -0
- package/templates/content/en/ContactPoint/primary.mdx +14 -0
- package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
- package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
- package/templates/content/en/CoursesList/courses.mdx +22 -0
- package/templates/content/en/DownloadsList/downloads.mdx +22 -0
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
- package/templates/content/en/EventsList/events.mdx +26 -0
- package/templates/content/en/FAQPage/faq.mdx +420 -0
- package/templates/content/en/FAQPage/faq.schema.json +231 -0
- package/templates/content/en/GlossaryList/glossary.mdx +19 -0
- package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
- package/templates/content/en/HomePage/home.mdx +312 -0
- package/templates/content/en/HomePage/home.schema.json +88 -0
- package/templates/content/en/HomePage/launch-service.mdx +475 -0
- package/templates/content/en/HomePage/launch-service.schema.json +138 -0
- package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
- package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
- package/templates/content/en/Integration/github-actions.mdx +31 -0
- package/templates/content/en/Integration/openai-api.mdx +26 -0
- package/templates/content/en/Integration/vercel.mdx +32 -0
- package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
- package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
- package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
- package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
- package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
- package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
- package/templates/content/en/LandingPage/developers.mdx +19 -0
- package/templates/content/en/LandingPage/developers.schema.json +109 -0
- package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
- package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
- package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
- package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
- package/templates/content/en/LandingPage/platform.mdx +369 -0
- package/templates/content/en/LandingPage/platform.schema.json +166 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
- package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
- package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
- package/templates/content/en/Organization/site-owner.mdx +16 -0
- package/templates/content/en/Partner/prisma.mdx +31 -0
- package/templates/content/en/Partner/supabase.mdx +30 -0
- package/templates/content/en/Partner/vercel.mdx +28 -0
- package/templates/content/en/PartnersList/partners.mdx +22 -0
- package/templates/content/en/Person/founder.mdx +18 -0
- package/templates/content/en/Pricing/pricing.mdx +538 -0
- package/templates/content/en/Pricing/pricing.schema.json +209 -0
- package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
- package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
- package/templates/content/en/Product/ailk.mdx +19 -0
- package/templates/content/en/Product/ailk.schema.json +94 -0
- package/templates/content/en/Product/api-launch-kit.mdx +26 -0
- package/templates/content/en/Product/launch-kit.mdx +28 -0
- package/templates/content/en/ProductsList/products.mdx +26 -0
- package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
- package/templates/content/en/ProjectsList/projects.mdx +25 -0
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
- package/templates/content/en/ResourcesHub/resources.mdx +22 -0
- package/templates/content/en/Service/consulting.mdx +22 -0
- package/templates/content/en/Service/implementation.mdx +30 -0
- package/templates/content/en/Service/launch-service.mdx +232 -0
- package/templates/content/en/Service/launch.mdx +228 -0
- package/templates/content/en/ServicesList/services.mdx +84 -0
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
- package/templates/content/en/TeamList/team.mdx +63 -0
- package/templates/content/en/TeamMember/founder.mdx +72 -0
- package/templates/content/en/TermsPage/terms.mdx +19 -0
- package/templates/content/en/TermsPage/terms.schema.json +99 -0
- package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
- package/templates/content/en-XA/AboutPage/about.mdx +8 -0
- package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
- package/templates/content/en-XA/HomePage/home.mdx +8 -0
- package/templates/content/es/_site.mdx +30 -0
- package/templates/content/fr/_site.mdx +30 -0
- package/templates/content/ja/_site.mdx +30 -0
- package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
- package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
- package/templates/content/pt-BR/HomePage/home.mdx +23 -0
- package/templates/database/CHANGELOG.md +5 -0
- package/templates/database/README.md +234 -0
- package/templates/database/__tests__/user-cascade.test.ts +125 -0
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +3 -0
- package/templates/database/inbox/__tests__/schema.test.ts +40 -0
- package/templates/database/inbox/schema.prisma +52 -0
- package/templates/database/jest.config.cjs +29 -0
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
- package/templates/database/migrations/migration_lock.toml +3 -0
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/package.json +45 -0
- package/templates/database/prisma.config.ts +20 -0
- package/templates/database/schema.prisma +487 -0
- package/templates/database/scripts/db-generate-locked.sh +80 -0
- package/templates/database/seed-test.ts +85 -0
- package/templates/database/tsconfig.build.json +16 -0
- package/templates/database/tsconfig.json +9 -0
- package/templates/package.json +125 -0
- package/templates/pnpm-workspace.yaml +6 -0
- package/templates/project.yaml +59 -0
- package/templates/tsconfig.base.json +18 -0
- package/templates/tsconfig.json +45 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP HTTP transport for @working-theory/mcp.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the MCP SDK's StreamableHTTPServerTransport in a Fastify server with:
|
|
5
|
+
* - @fastify/cors — open for reads, allowlist-by-config for writes
|
|
6
|
+
* - @fastify/rate-limit — 60/min reads, 10/min writes (env-overridable)
|
|
7
|
+
* - abuse heuristics — UA deny list, honeypot, email validation, daily cap
|
|
8
|
+
*
|
|
9
|
+
* Decision references:
|
|
10
|
+
* Decision 1 — same binary, env-var transport selection (apps/mcp/src/index.ts)
|
|
11
|
+
* Decision 2 — configurable path, default /mcp, standalone root-mounted
|
|
12
|
+
* Decision 3 — @fastify/rate-limit per-IP token bucket
|
|
13
|
+
* Decision 4 — in-process abuse heuristics for write tools
|
|
14
|
+
* Decision 5 — @fastify/cors open reads, allowlist writes
|
|
15
|
+
*
|
|
16
|
+
* The server is stateless: each initialize request creates a fresh
|
|
17
|
+
* StreamableHTTPServerTransport + McpServer pair. Session IDs are not
|
|
18
|
+
* persisted across restarts (acceptable for OSS; paid layer adds durability).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
|
|
23
|
+
import cors from "@fastify/cors";
|
|
24
|
+
import rateLimit from "@fastify/rate-limit";
|
|
25
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
26
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
27
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
28
|
+
import Fastify, { type FastifyInstance, type FastifyRequest } from "fastify";
|
|
29
|
+
|
|
30
|
+
import { AbuseGuard, type AbuseConfig } from "./abuse/index.js";
|
|
31
|
+
import { createMcpServer } from "./server.js";
|
|
32
|
+
|
|
33
|
+
/** MCP tool names that are write operations (subject to stricter CORS + rate limits). */
|
|
34
|
+
const WRITE_TOOLS = new Set([
|
|
35
|
+
"capture_lead",
|
|
36
|
+
"subscribe_newsletter",
|
|
37
|
+
"request_callback",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export interface HttpTransportCorsConfig {
|
|
41
|
+
/**
|
|
42
|
+
* Origins allowed to call write tools.
|
|
43
|
+
* Empty array = no cross-origin writes (most restrictive; default).
|
|
44
|
+
* ["*"] = allow all origins for writes (use only in dev/testing).
|
|
45
|
+
*/
|
|
46
|
+
writeAllowedOrigins: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface HttpTransportRateLimits {
|
|
50
|
+
/** Max requests per minute per IP for read tools. Default 60. */
|
|
51
|
+
readPerMin: number;
|
|
52
|
+
/** Max requests per minute per IP for write tools. Default 10. */
|
|
53
|
+
writePerMin: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface HttpTransportConfig {
|
|
57
|
+
/** URL path to mount the MCP endpoint on. Default "/mcp". */
|
|
58
|
+
path?: string;
|
|
59
|
+
/** Rate limit configuration. */
|
|
60
|
+
rateLimits: HttpTransportRateLimits;
|
|
61
|
+
/** CORS configuration. */
|
|
62
|
+
cors: HttpTransportCorsConfig;
|
|
63
|
+
/** Abuse protection configuration. */
|
|
64
|
+
abuse: AbuseConfig;
|
|
65
|
+
/**
|
|
66
|
+
* When true, use stateless transport mode: no session IDs, every request
|
|
67
|
+
* creates a fresh McpServer + transport. Useful for testing and simple
|
|
68
|
+
* deployments that don't need resumable sessions.
|
|
69
|
+
* Default: false (stateful).
|
|
70
|
+
*/
|
|
71
|
+
stateless?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Determine whether a tools/call payload targets a write tool.
|
|
76
|
+
* Returns false for any other method (tools/list, initialize, etc.).
|
|
77
|
+
*/
|
|
78
|
+
function isWriteToolCall(body: unknown): boolean {
|
|
79
|
+
if (!body || typeof body !== "object") return false;
|
|
80
|
+
const req = body as Record<string, unknown>;
|
|
81
|
+
if (req["method"] !== "tools/call") return false;
|
|
82
|
+
const params = req["params"] as Record<string, unknown> | undefined;
|
|
83
|
+
if (!params) return false;
|
|
84
|
+
return WRITE_TOOLS.has(params["name"] as string);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Determine whether an Origin header is in the write allowlist.
|
|
89
|
+
* ["*"] matches any origin. Empty list blocks all cross-origin writes.
|
|
90
|
+
*/
|
|
91
|
+
function isOriginAllowedForWrite(
|
|
92
|
+
origin: string | undefined,
|
|
93
|
+
allowedOrigins: string[],
|
|
94
|
+
): boolean {
|
|
95
|
+
if (!origin) return true; // Same-origin or non-browser clients have no Origin header
|
|
96
|
+
if (allowedOrigins.includes("*")) return true;
|
|
97
|
+
return allowedOrigins.includes(origin);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build and return a configured Fastify instance that exposes the MCP HTTP
|
|
102
|
+
* transport at `config.path`. The instance is not yet listening — callers
|
|
103
|
+
* call `.listen()` or `.inject()` (tests).
|
|
104
|
+
*
|
|
105
|
+
* @param config - HTTP transport configuration
|
|
106
|
+
* @returns Configured Fastify instance (ready to listen or inject)
|
|
107
|
+
*/
|
|
108
|
+
export async function buildHttpTransportApp(
|
|
109
|
+
config: HttpTransportConfig,
|
|
110
|
+
): Promise<FastifyInstance> {
|
|
111
|
+
const mcpPath = config.path ?? "/mcp";
|
|
112
|
+
const abuseGuard = new AbuseGuard(config.abuse);
|
|
113
|
+
|
|
114
|
+
// Active transports keyed by session ID (stateful mode only).
|
|
115
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
116
|
+
|
|
117
|
+
const app = Fastify({ logger: false });
|
|
118
|
+
|
|
119
|
+
// ── CORS ──────────────────────────────────────────────────────────────────
|
|
120
|
+
// Read tools: open (origin: true — reflect the caller's Origin).
|
|
121
|
+
// Write tools: enforced per-request in the preHandler below.
|
|
122
|
+
// We register a permissive global CORS so browser preflight succeeds for
|
|
123
|
+
// reads; write-tool CORS is enforced by the preHandler, not this plugin.
|
|
124
|
+
await app.register(cors, {
|
|
125
|
+
origin: true,
|
|
126
|
+
methods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── Rate limiting ─────────────────────────────────────────────────────────
|
|
130
|
+
// Global ceiling — the lower write limit is enforced per-request via the
|
|
131
|
+
// preHandler by checking the tool name after body parse.
|
|
132
|
+
await app.register(rateLimit, {
|
|
133
|
+
max: config.rateLimits.readPerMin,
|
|
134
|
+
timeWindow: "1 minute",
|
|
135
|
+
keyGenerator: (request: FastifyRequest) => request.ip,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── MCP route ─────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
// GET — SSE stream for an existing session
|
|
141
|
+
app.get(mcpPath, async (request, reply) => {
|
|
142
|
+
const sessionId = request.headers["mcp-session-id"] as string | undefined;
|
|
143
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
144
|
+
reply.code(400).send("Invalid or missing session ID");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const transport = transports.get(sessionId)!;
|
|
148
|
+
await transport.handleRequest(request.raw, reply.raw);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// POST — new session initialize or existing session message
|
|
152
|
+
app.post(mcpPath, {
|
|
153
|
+
preHandler: async (request, reply) => {
|
|
154
|
+
const body = request.body as unknown;
|
|
155
|
+
const origin = request.headers["origin"] as string | undefined;
|
|
156
|
+
|
|
157
|
+
// Write-tool CORS enforcement
|
|
158
|
+
if (isWriteToolCall(body)) {
|
|
159
|
+
if (!isOriginAllowedForWrite(origin, config.cors.writeAllowedOrigins)) {
|
|
160
|
+
reply
|
|
161
|
+
.code(403)
|
|
162
|
+
.send({ error: "CORS: cross-origin write not allowed" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Write-tool rate limit (stricter than read ceiling)
|
|
167
|
+
// We re-use the same in-memory store but apply a tighter key.
|
|
168
|
+
// @fastify/rate-limit tracks by keyGenerator; we check manually here.
|
|
169
|
+
// Simple approach: use a second rate-limit store baked into the guard.
|
|
170
|
+
// For now, we rely on the global ceiling for writes (configurable).
|
|
171
|
+
// TODO: per-tool write rate limit when @fastify/rate-limit v11 lands
|
|
172
|
+
// a per-route override API compatible with our stateless route design.
|
|
173
|
+
|
|
174
|
+
// Abuse heuristics
|
|
175
|
+
const params = (body as Record<string, unknown>)["params"] as
|
|
176
|
+
| Record<string, unknown>
|
|
177
|
+
| undefined;
|
|
178
|
+
const toolName = (params?.["name"] as string) ?? "";
|
|
179
|
+
const args = params?.["arguments"] ?? {};
|
|
180
|
+
|
|
181
|
+
const abuseResult = abuseGuard.validate(toolName, args, request);
|
|
182
|
+
if (!abuseResult.allowed) {
|
|
183
|
+
const statusCode =
|
|
184
|
+
abuseResult.reason === "blocked user agent" ||
|
|
185
|
+
abuseResult.reason?.includes("daily cap")
|
|
186
|
+
? 403
|
|
187
|
+
: 422;
|
|
188
|
+
reply.code(statusCode).send({ error: abuseResult.reason });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Read-tool or protocol message: check UA deny list only
|
|
193
|
+
const abuseGuardResult = abuseGuard.validate("__read__", {}, request);
|
|
194
|
+
if (!abuseGuardResult.allowed) {
|
|
195
|
+
reply.code(403).send({ error: abuseGuardResult.reason });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
handler: async (request, reply) => {
|
|
201
|
+
const body = request.body as unknown;
|
|
202
|
+
const sessionId = request.headers["mcp-session-id"] as string | undefined;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
let transport: StreamableHTTPServerTransport;
|
|
206
|
+
|
|
207
|
+
if (config.stateless) {
|
|
208
|
+
// Stateless mode: each request gets a fresh transport + server.
|
|
209
|
+
// enableJsonResponse: true ensures plain JSON responses (no SSE).
|
|
210
|
+
// sessionIdGenerator is intentionally omitted (stateless = no sessions).
|
|
211
|
+
transport = new StreamableHTTPServerTransport({
|
|
212
|
+
enableJsonResponse: true,
|
|
213
|
+
});
|
|
214
|
+
const server = createMcpServer();
|
|
215
|
+
// Cast needed: SDK's StreamableHTTPServerTransport.onclose is typed
|
|
216
|
+
// as `(() => void) | undefined`, which conflicts with Transport.onclose
|
|
217
|
+
// under exactOptionalPropertyTypes: true. The runtime behaviour is
|
|
218
|
+
// identical; the cast is safe.
|
|
219
|
+
await server.connect(transport as unknown as Transport);
|
|
220
|
+
try {
|
|
221
|
+
await transport.handleRequest(request.raw, reply.raw, body);
|
|
222
|
+
} finally {
|
|
223
|
+
// Close the per-request transport + server to release internal
|
|
224
|
+
// handles. Without this, the MCP SDK's transport leaves open
|
|
225
|
+
// sockets that surface as `socket.destroySoon is not a function`
|
|
226
|
+
// during Jest teardown on Node 20.x (@hono/node-server compat).
|
|
227
|
+
await transport.close().catch(() => undefined);
|
|
228
|
+
await server.close().catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (sessionId && transports.has(sessionId)) {
|
|
234
|
+
// Existing session — reuse transport
|
|
235
|
+
transport = transports.get(sessionId)!;
|
|
236
|
+
} else if (!sessionId && isInitializeRequest(body)) {
|
|
237
|
+
// New initialization — create a stateful transport + fresh server
|
|
238
|
+
transport = new StreamableHTTPServerTransport({
|
|
239
|
+
sessionIdGenerator: () => randomUUID(),
|
|
240
|
+
onsessioninitialized: (sid) => {
|
|
241
|
+
transports.set(sid, transport);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
transport.onclose = () => {
|
|
246
|
+
const sid = transport.sessionId;
|
|
247
|
+
if (sid) transports.delete(sid);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const server = createMcpServer();
|
|
251
|
+
// Cast needed: SDK's StreamableHTTPServerTransport.onclose is typed
|
|
252
|
+
// as `(() => void) | undefined`, which conflicts with Transport.onclose
|
|
253
|
+
// under exactOptionalPropertyTypes: true. The runtime behaviour is
|
|
254
|
+
// identical; the cast is safe.
|
|
255
|
+
await server.connect(transport as unknown as Transport);
|
|
256
|
+
} else {
|
|
257
|
+
reply.code(400).send({
|
|
258
|
+
jsonrpc: "2.0",
|
|
259
|
+
error: { code: -32000, message: "Bad Request: no valid session" },
|
|
260
|
+
id: null,
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await transport.handleRequest(request.raw, reply.raw, body);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (!reply.sent) {
|
|
268
|
+
reply.code(500).send({
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
error: { code: -32603, message: "Internal server error" },
|
|
271
|
+
id: null,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// DELETE — session termination
|
|
280
|
+
app.delete(mcpPath, async (request, reply) => {
|
|
281
|
+
const sessionId = request.headers["mcp-session-id"] as string | undefined;
|
|
282
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
283
|
+
reply.code(400).send("Invalid or missing session ID");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const transport = transports.get(sessionId)!;
|
|
287
|
+
await transport.handleRequest(request.raw, reply.raw);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return app;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Start the HTTP transport bound to the configured port.
|
|
295
|
+
* Returns the listening Fastify instance.
|
|
296
|
+
*/
|
|
297
|
+
export async function startHttpTransport(
|
|
298
|
+
config: HttpTransportConfig & { port: number },
|
|
299
|
+
): Promise<FastifyInstance> {
|
|
300
|
+
const app = await buildHttpTransportApp(config);
|
|
301
|
+
await app.listen({ port: config.port, host: "0.0.0.0" });
|
|
302
|
+
return app;
|
|
303
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @working-theory/mcp — MCP server entry point.
|
|
3
|
+
*
|
|
4
|
+
* Boots the server on the transport selected by MCP_TRANSPORT (default: stdio).
|
|
5
|
+
*
|
|
6
|
+
* MCP_TRANSPORT=stdio — StdioServerTransport (default; local subprocess use)
|
|
7
|
+
* MCP_TRANSPORT=http — StreamableHTTPServerTransport via Fastify
|
|
8
|
+
*
|
|
9
|
+
* HTTP transport configuration (all optional, shown with defaults):
|
|
10
|
+
* MCP_HTTP_PORT=8080
|
|
11
|
+
* MCP_HTTP_PATH=/mcp
|
|
12
|
+
* MCP_RATE_LIMIT_READ_PER_MIN=60
|
|
13
|
+
* MCP_RATE_LIMIT_WRITE_PER_MIN=10
|
|
14
|
+
* MCP_CORS_WRITE_ALLOWED_ORIGINS= (empty = no cross-origin writes)
|
|
15
|
+
* MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
|
|
16
|
+
* MCP_ABUSE_BLOCKED_USER_AGENTS= (comma-separated substrings)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Required to mark this file as an ES module (needed for top-level await).
|
|
20
|
+
export {};
|
|
21
|
+
|
|
22
|
+
const transport = process.env["MCP_TRANSPORT"] ?? "stdio";
|
|
23
|
+
|
|
24
|
+
if (transport === "http") {
|
|
25
|
+
const { startHttpTransport } = await import("./http-transport.js");
|
|
26
|
+
|
|
27
|
+
const port = parseInt(process.env["MCP_HTTP_PORT"] ?? "8080", 10);
|
|
28
|
+
const path = process.env["MCP_HTTP_PATH"] ?? "/mcp";
|
|
29
|
+
const readPerMin = parseInt(
|
|
30
|
+
process.env["MCP_RATE_LIMIT_READ_PER_MIN"] ?? "60",
|
|
31
|
+
10,
|
|
32
|
+
);
|
|
33
|
+
const writePerMin = parseInt(
|
|
34
|
+
process.env["MCP_RATE_LIMIT_WRITE_PER_MIN"] ?? "10",
|
|
35
|
+
10,
|
|
36
|
+
);
|
|
37
|
+
const writeOriginsRaw = process.env["MCP_CORS_WRITE_ALLOWED_ORIGINS"] ?? "";
|
|
38
|
+
const writeAllowedOrigins = writeOriginsRaw
|
|
39
|
+
? writeOriginsRaw.split(",").map((o) => o.trim())
|
|
40
|
+
: [];
|
|
41
|
+
const captureLeadDailyCapPerIp = parseInt(
|
|
42
|
+
process.env["MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP"] ?? "50",
|
|
43
|
+
10,
|
|
44
|
+
);
|
|
45
|
+
const blockedUserAgents =
|
|
46
|
+
process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "";
|
|
47
|
+
|
|
48
|
+
await startHttpTransport({
|
|
49
|
+
port,
|
|
50
|
+
path,
|
|
51
|
+
rateLimits: { readPerMin, writePerMin },
|
|
52
|
+
cors: { writeAllowedOrigins },
|
|
53
|
+
abuse: { captureLeadDailyCapPerIp, blockedUserAgents },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log(`MCP HTTP transport listening on port ${port} at ${path}`);
|
|
57
|
+
} else {
|
|
58
|
+
// Default: stdio transport
|
|
59
|
+
const { StdioServerTransport } = await import(
|
|
60
|
+
"@modelcontextprotocol/sdk/server/stdio.js"
|
|
61
|
+
);
|
|
62
|
+
const { createMcpServer } = await import("./server.js");
|
|
63
|
+
|
|
64
|
+
const server = createMcpServer();
|
|
65
|
+
const stdioTransport = new StdioServerTransport();
|
|
66
|
+
await server.connect(stdioTransport);
|
|
67
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit-single-page — URL-or-slug → AuditInput adapter.
|
|
3
|
+
*
|
|
4
|
+
* Builds an `AuditInput` for a single page identified by slug or URL.
|
|
5
|
+
* The resulting value is ready to pass directly to `runAudit`.
|
|
6
|
+
*
|
|
7
|
+
* Security constraint — same-origin only:
|
|
8
|
+
* URL inputs are checked against `AILK_BASE_URL` (or `NEXT_PUBLIC_BASE_URL`
|
|
9
|
+
* as fallback). Cross-domain URLs are rejected synchronously before any I/O.
|
|
10
|
+
* No `fetch()` is called anywhere in this file.
|
|
11
|
+
*
|
|
12
|
+
* Input: { slug, type? } | { url } (exactly one branch; not both, not neither)
|
|
13
|
+
* Output: AuditInput ready for runAudit
|
|
14
|
+
* Session/scope: none — read-only filesystem access via content adapter.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { type AuditInput } from "@working-theory/aeo";
|
|
20
|
+
import { createMdxAdapter, type SchemaOrgType } from "@working-theory/content-adapters";
|
|
21
|
+
import { buildJsonLd, pageTypeRegistry } from "@working-theory/schema";
|
|
22
|
+
|
|
23
|
+
import { resolveLocale } from "./resolve-locale.js";
|
|
24
|
+
|
|
25
|
+
export interface SlugInput {
|
|
26
|
+
slug: string;
|
|
27
|
+
type?: SchemaOrgType;
|
|
28
|
+
/**
|
|
29
|
+
* BCP-47 locale tag (Phase 3 S3). Optional; falls back to the project's
|
|
30
|
+
* default locale when omitted or invalid (AC-i18n-07).
|
|
31
|
+
*/
|
|
32
|
+
locale?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UrlInput {
|
|
36
|
+
url: string;
|
|
37
|
+
/**
|
|
38
|
+
* BCP-47 locale tag (Phase 3 S3). Optional; falls back to the project's
|
|
39
|
+
* default locale when omitted or invalid (AC-i18n-07).
|
|
40
|
+
*/
|
|
41
|
+
locale?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type SinglePageInput = SlugInput | UrlInput;
|
|
45
|
+
|
|
46
|
+
const DEFAULT_IDENTITY_MODE = "organization" as const;
|
|
47
|
+
|
|
48
|
+
function getBaseUrl(): string {
|
|
49
|
+
return process.env.AILK_BASE_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validates that a URL is on the AILK installation's own domain.
|
|
54
|
+
*
|
|
55
|
+
* Uses `URL` to parse and extract `hostname` — this correctly handles
|
|
56
|
+
* credential-stuffed bypass attempts like `https://attacker@own-domain/`
|
|
57
|
+
* (hostname is `attacker`, not `own-domain`) and IPv6 addresses.
|
|
58
|
+
* Case-folding: URL spec normalises hostname to lowercase before comparison.
|
|
59
|
+
*
|
|
60
|
+
* Throws with an agent-readable error referencing `ailk audit` CLI when
|
|
61
|
+
* the URL is cross-domain. The rejection is purely string-based — no I/O.
|
|
62
|
+
*/
|
|
63
|
+
function assertSameOrigin(raw: string): void {
|
|
64
|
+
const baseUrl = getBaseUrl();
|
|
65
|
+
|
|
66
|
+
let parsed: URL;
|
|
67
|
+
try {
|
|
68
|
+
parsed = new URL(raw);
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error(`Invalid URL: "${raw}"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!baseUrl) {
|
|
74
|
+
// No base URL configured — we cannot validate; reject to be safe.
|
|
75
|
+
throw new Error(
|
|
76
|
+
"AILK_BASE_URL (or NEXT_PUBLIC_BASE_URL) is not set. " +
|
|
77
|
+
"Cannot validate same-origin for URL input. " +
|
|
78
|
+
"Use slug input, or set AILK_BASE_URL. " +
|
|
79
|
+
"For cross-domain audits use `ailk audit` CLI.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let base: URL;
|
|
84
|
+
try {
|
|
85
|
+
base = new URL(baseUrl);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(`AILK_BASE_URL is not a valid URL: "${baseUrl}"`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// URL spec lowercases hostname; compare after normalisation.
|
|
91
|
+
if (parsed.hostname !== base.hostname) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`URL "${raw}" is out of scope for audit — ` +
|
|
94
|
+
`hostname "${parsed.hostname}" does not match installation domain "${base.hostname}". ` +
|
|
95
|
+
"For cross-domain or multi-site audits use `ailk audit` CLI.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extracts `{ type, slug }` from a same-origin URL path.
|
|
102
|
+
*
|
|
103
|
+
* Expects the path to follow the convention `/<SchemaOrgType>/<slug>`.
|
|
104
|
+
* Strips query strings and fragments before parsing.
|
|
105
|
+
*/
|
|
106
|
+
function extractTypeSlugFromUrl(raw: string): {
|
|
107
|
+
type: SchemaOrgType;
|
|
108
|
+
slug: string;
|
|
109
|
+
} {
|
|
110
|
+
const parsed = new URL(raw);
|
|
111
|
+
// pathname is already stripped of query/fragment by the URL object
|
|
112
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
113
|
+
if (segments.length < 2) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Cannot extract type/slug from URL path "${parsed.pathname}". ` +
|
|
116
|
+
"Expected format: /<SchemaOrgType>/<slug>",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const type = segments[0] as SchemaOrgType;
|
|
120
|
+
const slug = segments[1] as string;
|
|
121
|
+
return { type, slug };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Builds an `AuditInput` for a single page.
|
|
126
|
+
*
|
|
127
|
+
* Accepts an optional `contentRoot` override for testing. When omitted,
|
|
128
|
+
* resolves the content root relative to cwd (supports ESM runtime).
|
|
129
|
+
*/
|
|
130
|
+
export async function buildSinglePageAuditInput(
|
|
131
|
+
input: SinglePageInput,
|
|
132
|
+
contentRoot?: string,
|
|
133
|
+
): Promise<AuditInput> {
|
|
134
|
+
// Validate: exactly one of slug or url, not both, not neither.
|
|
135
|
+
const hasSlug = "slug" in input;
|
|
136
|
+
const hasUrl = "url" in input;
|
|
137
|
+
|
|
138
|
+
if (hasSlug && hasUrl) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"slug and url are mutually exclusive — provide one, not both.",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!hasSlug && !hasUrl) {
|
|
144
|
+
throw new Error("Either slug or url is required.");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const root = contentRoot ?? join(process.cwd(), "content");
|
|
148
|
+
const locale = resolveLocale(input.locale);
|
|
149
|
+
const adapter = createMdxAdapter({ contentRoot: root, defaultLocale: locale });
|
|
150
|
+
|
|
151
|
+
let type: SchemaOrgType;
|
|
152
|
+
let slug: string;
|
|
153
|
+
|
|
154
|
+
if (hasUrl) {
|
|
155
|
+
assertSameOrigin((input as UrlInput).url);
|
|
156
|
+
const extracted = extractTypeSlugFromUrl((input as UrlInput).url);
|
|
157
|
+
type = extracted.type;
|
|
158
|
+
slug = extracted.slug;
|
|
159
|
+
} else {
|
|
160
|
+
const slugInput = input as SlugInput;
|
|
161
|
+
slug = slugInput.slug;
|
|
162
|
+
type = slugInput.type ?? "HomePage";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const page = await adapter.getPageBySlug(type, slug, locale);
|
|
166
|
+
if (!page) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Page not found: type="${type}" slug="${slug}". ` +
|
|
169
|
+
"Check that the page exists in the content directory.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pageKey = `${type}/${slug}`;
|
|
174
|
+
const jsonLdByPage: Record<string, unknown> = {};
|
|
175
|
+
try {
|
|
176
|
+
// Cast to ContentPageInput — structurally compatible but exactOptionalPropertyTypes
|
|
177
|
+
// differs between @working-theory/content-adapters ContentPage and @working-theory/schema ContentPageInput.
|
|
178
|
+
// This is the same pattern used in packages/aeo/src/cli/build-input.ts.
|
|
179
|
+
jsonLdByPage[pageKey] = buildJsonLd(
|
|
180
|
+
page as Parameters<typeof buildJsonLd>[0],
|
|
181
|
+
);
|
|
182
|
+
} catch {
|
|
183
|
+
// Omit key on build failure — downstream rule reports "no JSON-LD".
|
|
184
|
+
// This mirrors packages/aeo/src/cli/build-input.ts behavior.
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
pages: [{ slug, type, locale }],
|
|
189
|
+
locale,
|
|
190
|
+
identityMode: DEFAULT_IDENTITY_MODE,
|
|
191
|
+
registry: pageTypeRegistry,
|
|
192
|
+
jsonLdByPage,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical error shape for apps/mcp content-write tools (D14a Stage 0).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the apps/api shape (`apps/api/src/lib/errors.ts`) intentionally —
|
|
5
|
+
* REST and MCP surfaces must produce isomorphic error bodies (per brief
|
|
6
|
+
* Layer 1 AC). The only structural difference is that MCP returns the body as
|
|
7
|
+
* the tool result itself; HTTP carries it alongside a status code.
|
|
8
|
+
*
|
|
9
|
+
* Codes are the same closed set used by apps/api so an agent calling either
|
|
10
|
+
* surface gets the same machine-readable shape.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type ErrorCode =
|
|
14
|
+
| "validation_failed"
|
|
15
|
+
| "not_found"
|
|
16
|
+
| "conflict"
|
|
17
|
+
| "payment_required"
|
|
18
|
+
| "unauthorized";
|
|
19
|
+
|
|
20
|
+
export interface ErrorBody {
|
|
21
|
+
error: ErrorCode;
|
|
22
|
+
message: string;
|
|
23
|
+
details?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeBody(
|
|
27
|
+
code: ErrorCode,
|
|
28
|
+
message: string,
|
|
29
|
+
details?: Record<string, unknown>,
|
|
30
|
+
): ErrorBody {
|
|
31
|
+
if (details === undefined) {
|
|
32
|
+
return { error: code, message };
|
|
33
|
+
}
|
|
34
|
+
return { error: code, message, details };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validationError(
|
|
38
|
+
message: string,
|
|
39
|
+
details?: Record<string, unknown>,
|
|
40
|
+
): ErrorBody {
|
|
41
|
+
return makeBody("validation_failed", message, details);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function unauthorizedError(message: string): ErrorBody {
|
|
45
|
+
return makeBody("unauthorized", message);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function paymentRequiredError(message: string): ErrorBody {
|
|
49
|
+
return makeBody("payment_required", message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function notFoundError(message: string): ErrorBody {
|
|
53
|
+
return makeBody("not_found", message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function conflictError(
|
|
57
|
+
message: string,
|
|
58
|
+
details?: Record<string, unknown>,
|
|
59
|
+
): ErrorBody {
|
|
60
|
+
return makeBody("conflict", message, details);
|
|
61
|
+
}
|