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,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the update_page MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* D14a Stage 1 Unit C — Layer 1 / Layer 2 acceptance criteria mapped here:
|
|
5
|
+
* - Free tier → payment_required, no adapter call.
|
|
6
|
+
* - Missing ifMatch → validation_failed (caught by input schema).
|
|
7
|
+
* - Stale ifMatch → conflict body with details.currentVersion echoed.
|
|
8
|
+
* - Slug not found → not_found body.
|
|
9
|
+
* - Bad frontmatter → validation_failed, no adapter call.
|
|
10
|
+
* - Pro + valid + matching ifMatch → { page, version } success body.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AdapterWriteError, type ContentAdapter } from "@working-theory/content-adapters";
|
|
14
|
+
|
|
15
|
+
const mockCheckLicense = jest.fn();
|
|
16
|
+
|
|
17
|
+
jest.mock("@working-theory/license", () => ({
|
|
18
|
+
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { ailkUpdatePage } from "../src/tools/update_page.js";
|
|
22
|
+
|
|
23
|
+
function makeAdapter(
|
|
24
|
+
overrides: Partial<{
|
|
25
|
+
updatePage: jest.Mock;
|
|
26
|
+
}> = {},
|
|
27
|
+
): ContentAdapter {
|
|
28
|
+
// eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
|
|
29
|
+
return {
|
|
30
|
+
getPageBySlug: jest.fn().mockResolvedValue(null),
|
|
31
|
+
listPages: jest.fn().mockResolvedValue([]),
|
|
32
|
+
getDetailBySlug: jest.fn().mockResolvedValue(null),
|
|
33
|
+
listDetail: jest.fn().mockResolvedValue([]),
|
|
34
|
+
searchPages: jest.fn().mockResolvedValue([]),
|
|
35
|
+
extractStructuredData: jest.fn().mockResolvedValue(null),
|
|
36
|
+
createPage: jest.fn(),
|
|
37
|
+
updatePage: overrides.updatePage ?? jest.fn(),
|
|
38
|
+
deletePage: jest.fn(),
|
|
39
|
+
getPageVersion: jest.fn(),
|
|
40
|
+
} as unknown as ContentAdapter;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const VALID_INPUT = {
|
|
44
|
+
type: "Product" as const,
|
|
45
|
+
slug: "widget-pro",
|
|
46
|
+
page: {
|
|
47
|
+
title: "Widget Pro v2",
|
|
48
|
+
description: "Updated.",
|
|
49
|
+
},
|
|
50
|
+
body: "# Widget Pro v2\n",
|
|
51
|
+
ifMatch: "v1",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe("ailkUpdatePage", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
mockCheckLicense.mockReset();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns payment_required for free tier; adapter not called", async () => {
|
|
60
|
+
mockCheckLicense.mockResolvedValue({
|
|
61
|
+
tier: "free",
|
|
62
|
+
plan: null,
|
|
63
|
+
features: [],
|
|
64
|
+
expiresAt: null,
|
|
65
|
+
revoked: false,
|
|
66
|
+
});
|
|
67
|
+
const adapter = makeAdapter();
|
|
68
|
+
|
|
69
|
+
const result = await ailkUpdatePage(VALID_INPUT, adapter);
|
|
70
|
+
|
|
71
|
+
expect(result).toMatchObject({ error: "payment_required" });
|
|
72
|
+
expect(adapter.updatePage).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns validation_failed when ifMatch is missing", async () => {
|
|
76
|
+
mockCheckLicense.mockResolvedValue({
|
|
77
|
+
tier: "pro",
|
|
78
|
+
plan: "pro",
|
|
79
|
+
features: [],
|
|
80
|
+
expiresAt: null,
|
|
81
|
+
revoked: false,
|
|
82
|
+
});
|
|
83
|
+
const adapter = makeAdapter();
|
|
84
|
+
const { ifMatch: _ignored, ...inputNoIfMatch } = VALID_INPUT;
|
|
85
|
+
|
|
86
|
+
const result = await ailkUpdatePage(
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
inputNoIfMatch as any,
|
|
89
|
+
adapter,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
93
|
+
expect(adapter.updatePage).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns conflict with details.currentVersion on stale ifMatch", async () => {
|
|
97
|
+
mockCheckLicense.mockResolvedValue({
|
|
98
|
+
tier: "pro",
|
|
99
|
+
plan: "pro",
|
|
100
|
+
features: [],
|
|
101
|
+
expiresAt: null,
|
|
102
|
+
revoked: false,
|
|
103
|
+
});
|
|
104
|
+
const adapter = makeAdapter({
|
|
105
|
+
updatePage: jest
|
|
106
|
+
.fn()
|
|
107
|
+
.mockRejectedValue(
|
|
108
|
+
new AdapterWriteError(
|
|
109
|
+
"conflict",
|
|
110
|
+
"Version mismatch on Product/widget-pro.mdx",
|
|
111
|
+
{ currentVersion: "v9" },
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = await ailkUpdatePage(VALID_INPUT, adapter);
|
|
117
|
+
|
|
118
|
+
expect(result).toMatchObject({
|
|
119
|
+
error: "conflict",
|
|
120
|
+
details: { currentVersion: "v9" },
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns not_found when adapter signals missing slug", async () => {
|
|
125
|
+
mockCheckLicense.mockResolvedValue({
|
|
126
|
+
tier: "pro",
|
|
127
|
+
plan: "pro",
|
|
128
|
+
features: [],
|
|
129
|
+
expiresAt: null,
|
|
130
|
+
revoked: false,
|
|
131
|
+
});
|
|
132
|
+
const adapter = makeAdapter({
|
|
133
|
+
updatePage: jest
|
|
134
|
+
.fn()
|
|
135
|
+
.mockRejectedValue(
|
|
136
|
+
new AdapterWriteError(
|
|
137
|
+
"not_found",
|
|
138
|
+
"No page at Product/widget-pro.mdx",
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await ailkUpdatePage(VALID_INPUT, adapter);
|
|
144
|
+
|
|
145
|
+
expect(result).toMatchObject({ error: "not_found" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns validation_failed when frontmatter is bad", async () => {
|
|
149
|
+
mockCheckLicense.mockResolvedValue({
|
|
150
|
+
tier: "pro",
|
|
151
|
+
plan: "pro",
|
|
152
|
+
features: [],
|
|
153
|
+
expiresAt: null,
|
|
154
|
+
revoked: false,
|
|
155
|
+
});
|
|
156
|
+
const adapter = makeAdapter();
|
|
157
|
+
|
|
158
|
+
const result = await ailkUpdatePage(
|
|
159
|
+
{ ...VALID_INPUT, page: { title: "Missing description" } },
|
|
160
|
+
adapter,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
164
|
+
expect(adapter.updatePage).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns { page, version } on happy path", async () => {
|
|
168
|
+
mockCheckLicense.mockResolvedValue({
|
|
169
|
+
tier: "pro",
|
|
170
|
+
plan: "pro",
|
|
171
|
+
features: [],
|
|
172
|
+
expiresAt: null,
|
|
173
|
+
revoked: false,
|
|
174
|
+
});
|
|
175
|
+
const persistedPage = {
|
|
176
|
+
type: "Product" as const,
|
|
177
|
+
slug: "widget-pro",
|
|
178
|
+
title: "Widget Pro v2",
|
|
179
|
+
description: "Updated.",
|
|
180
|
+
};
|
|
181
|
+
const adapter = makeAdapter({
|
|
182
|
+
updatePage: jest
|
|
183
|
+
.fn()
|
|
184
|
+
.mockResolvedValue({ page: persistedPage, version: "v2" }),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = await ailkUpdatePage(VALID_INPUT, adapter);
|
|
188
|
+
|
|
189
|
+
expect(result).toEqual({ page: persistedPage, version: "v2" });
|
|
190
|
+
const callArgs = (adapter.updatePage as jest.Mock).mock.calls[0];
|
|
191
|
+
expect(callArgs[0]).toBe("Product");
|
|
192
|
+
expect(callArgs[1]).toBe("widget-pro");
|
|
193
|
+
expect(callArgs[3]).toEqual({ ifMatch: "v1" });
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
// Workaround: @hono/node-server@1.19.14 (transitive via
|
|
5
|
+
// @modelcontextprotocol/sdk's StreamableHTTPServerTransport) calls
|
|
6
|
+
// socket.destroySoon() at shutdown, which throws on Node 20.x. Tests pass;
|
|
7
|
+
// only the post-test process teardown fails. forceExit lets Jest exit
|
|
8
|
+
// cleanly after all tests have reported, bypassing the broken handle
|
|
9
|
+
// cleanup. Revisit when @hono/node-server publishes a Node-20-compatible fix.
|
|
10
|
+
forceExit: true,
|
|
11
|
+
roots: ['<rootDir>/__tests__'],
|
|
12
|
+
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
13
|
+
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
14
|
+
moduleNameMapper: {
|
|
15
|
+
// Mock server-only in Jest environments — the package throws by design
|
|
16
|
+
// outside Next.js. The real guard runs in production Next.js builds.
|
|
17
|
+
'^server-only$': '<rootDir>/../../__mocks__/server-only.ts',
|
|
18
|
+
// Map workspace packages to their TypeScript source so ts-jest compiles them
|
|
19
|
+
// directly (the published dist is ESM, which Jest's CJS preset can't load).
|
|
20
|
+
'^@working-theory/aeo$': '<rootDir>/../../packages/aeo/src/index.ts',
|
|
21
|
+
'^@working-theory/common$': '<rootDir>/../../packages/common/src/index.ts',
|
|
22
|
+
'^@working-theory/aeo/(.*)$': '<rootDir>/../../packages/aeo/src/$1',
|
|
23
|
+
'^@working-theory/content-adapters$': '<rootDir>/../../packages/content-adapters/src/index.ts',
|
|
24
|
+
'^@working-theory/content-adapters/(.*)$': '<rootDir>/../../packages/content-adapters/src/$1',
|
|
25
|
+
'^@working-theory/license$': '<rootDir>/../../packages/license/src/index.ts',
|
|
26
|
+
'^@working-theory/metadata$': '<rootDir>/../../packages/metadata/src/index.ts',
|
|
27
|
+
'^@working-theory/schema$': '<rootDir>/../../packages/schema/src/index.ts',
|
|
28
|
+
'^@working-theory/schema/(.*)$': '<rootDir>/../../packages/schema/src/$1',
|
|
29
|
+
'^@working-theory/schema-validator$': '<rootDir>/../../packages/schema-validator/src/index.ts',
|
|
30
|
+
'^@working-theory/schema-pack-registry$': '<rootDir>/../../packages/schema-pack-registry/src/index.ts',
|
|
31
|
+
'^@working-theory/validation$': '<rootDir>/../../packages/validation/src/index.ts',
|
|
32
|
+
'^@working-theory/api-client$': '<rootDir>/../../packages/api-client/src/index.ts',
|
|
33
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
34
|
+
},
|
|
35
|
+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
|
|
36
|
+
transformIgnorePatterns: [
|
|
37
|
+
'/node_modules/(?!(\\.pnpm/)?jose@?[^/]*?(/|$))',
|
|
38
|
+
],
|
|
39
|
+
transform: {
|
|
40
|
+
'^.+\\.(tsx?|jsx?|mjs)$': [
|
|
41
|
+
'ts-jest',
|
|
42
|
+
{
|
|
43
|
+
tsconfig: {
|
|
44
|
+
target: 'es2016',
|
|
45
|
+
module: 'commonjs',
|
|
46
|
+
lib: ['es2016'],
|
|
47
|
+
declaration: false,
|
|
48
|
+
strict: true,
|
|
49
|
+
esModuleInterop: true,
|
|
50
|
+
skipLibCheck: true,
|
|
51
|
+
ignoreDeprecations: '6.0',
|
|
52
|
+
allowJs: true,
|
|
53
|
+
// paths must mirror moduleNameMapper above so ts-jest's TypeScript
|
|
54
|
+
// type-checker can resolve workspace package imports during compile.
|
|
55
|
+
// Jest's moduleNameMapper handles runtime resolution; TS uses paths.
|
|
56
|
+
baseUrl: '.',
|
|
57
|
+
paths: {
|
|
58
|
+
'@working-theory/aeo': ['../../packages/aeo/src/index.ts'],
|
|
59
|
+
'@working-theory/common': ['../../packages/common/src/index.ts'],
|
|
60
|
+
'@working-theory/aeo/*': ['../../packages/aeo/src/*'],
|
|
61
|
+
'@working-theory/api-client': ['../../packages/api-client/src/index.ts'],
|
|
62
|
+
'@working-theory/content-adapters': ['../../packages/content-adapters/src/index.ts'],
|
|
63
|
+
'@working-theory/content-adapters/*': ['../../packages/content-adapters/src/*'],
|
|
64
|
+
'@working-theory/license': ['../../packages/license/src/index.ts'],
|
|
65
|
+
'@working-theory/metadata': ['../../packages/metadata/src/index.ts'],
|
|
66
|
+
'@working-theory/schema': ['../../packages/schema/src/index.ts'],
|
|
67
|
+
'@working-theory/schema/*': ['../../packages/schema/src/*'],
|
|
68
|
+
'@working-theory/schema-validator': ['../../packages/schema-validator/src/index.ts'],
|
|
69
|
+
'@working-theory/schema-pack-registry': ['../../packages/schema-pack-registry/src/index.ts'],
|
|
70
|
+
'@working-theory/validation': ['../../packages/validation/src/index.ts'],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
// Suppress "import.meta only allowed in ESM modules" — source is ESM,
|
|
74
|
+
// but ts-jest compiles to CommonJS for Jest. The import.meta fallback
|
|
75
|
+
// path is never exercised in tests (contentRoot is always passed explicitly).
|
|
76
|
+
diagnostics: {
|
|
77
|
+
ignoreCodes: [1343],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@working-theory/mcp",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -b",
|
|
10
|
+
"type-check": "tsc --noEmit",
|
|
11
|
+
"dev": "node --experimental-vm-modules dist/index.js",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test:watch": "jest --watch",
|
|
14
|
+
"test:coverage": "jest --coverage"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@working-theory/aeo": "workspace:*",
|
|
18
|
+
"@working-theory/api-client": "workspace:*",
|
|
19
|
+
"@working-theory/content-adapters": "workspace:*",
|
|
20
|
+
"@working-theory/license": "workspace:*",
|
|
21
|
+
"@working-theory/metadata": "workspace:*",
|
|
22
|
+
"@working-theory/schema": "workspace:*",
|
|
23
|
+
"@working-theory/validation": "workspace:*",
|
|
24
|
+
"@fastify/cors": "^11.2.0",
|
|
25
|
+
"@fastify/rate-limit": "^10.3.0",
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"fastify": "^5.7.2",
|
|
28
|
+
"zod": "^4.2.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/jest": "^30.0.0",
|
|
32
|
+
"@types/node": "^22.20.0",
|
|
33
|
+
"jest": "^30.4.2",
|
|
34
|
+
"ts-jest": "^29.4.11",
|
|
35
|
+
"typescript": "^6.0.3"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-IP daily cap counter.
|
|
3
|
+
*
|
|
4
|
+
* Tracks how many times each IP has triggered a capped action within a
|
|
5
|
+
* 24-hour rolling window. Uses an in-memory Map — loss on restart is
|
|
6
|
+
* acceptable for OSS. The paid layer replaces this with a durable store
|
|
7
|
+
* (Redis or the lead-routing service).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const DEFAULT_CAP = 50;
|
|
12
|
+
|
|
13
|
+
interface Entry {
|
|
14
|
+
count: number;
|
|
15
|
+
windowStart: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DailyCapTracker {
|
|
19
|
+
private readonly cap: number;
|
|
20
|
+
private readonly counters = new Map<string, Entry>();
|
|
21
|
+
|
|
22
|
+
constructor(cap: number = DEFAULT_CAP) {
|
|
23
|
+
this.cap = cap;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if the given IP is below the daily cap (and increments the
|
|
28
|
+
* counter). Returns false if the cap has already been reached today.
|
|
29
|
+
*/
|
|
30
|
+
isAllowed(ip: string): boolean {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const entry = this.counters.get(ip);
|
|
33
|
+
|
|
34
|
+
if (!entry || now - entry.windowStart >= ONE_DAY_MS) {
|
|
35
|
+
// Fresh window
|
|
36
|
+
this.counters.set(ip, { count: 1, windowStart: now });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (entry.count >= this.cap) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
entry.count += 1;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 5322-style email format validation for abuse rules.
|
|
3
|
+
*
|
|
4
|
+
* Validates that a string is a plausibly well-formed email address.
|
|
5
|
+
* This is intentionally lenient — the goal is to reject obviously malformed
|
|
6
|
+
* values (missing @, missing domain, missing TLD) rather than to implement
|
|
7
|
+
* the full RFC 5322 grammar.
|
|
8
|
+
*
|
|
9
|
+
* Paid-layer (lead-routing.ts) adds real deliverability checks via an
|
|
10
|
+
* external service; OSS gets basic format protection.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Requires: local@domain.tld — at least one dot in the domain part.
|
|
14
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if `email` passes the basic format check.
|
|
18
|
+
* Returns false for empty strings, missing @, missing domain, or missing TLD.
|
|
19
|
+
*/
|
|
20
|
+
export function isValidEmail(email: string): boolean {
|
|
21
|
+
if (!email || typeof email !== "string") return false;
|
|
22
|
+
// Reject multiple @ characters
|
|
23
|
+
if ((email.match(/@/g) ?? []).length !== 1) return false;
|
|
24
|
+
return EMAIL_RE.test(email);
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honeypot field check for abuse rules.
|
|
3
|
+
*
|
|
4
|
+
* If the tool input contains a `honeypot` field with any non-empty value,
|
|
5
|
+
* the request is rejected as bot traffic. Legitimate human-driven agents
|
|
6
|
+
* never populate hidden honeypot fields.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the honeypot field is present and non-empty in `input`.
|
|
11
|
+
* Treats missing, null, or empty-string honeypot as clean.
|
|
12
|
+
*/
|
|
13
|
+
export function isHoneypotTripped(input: unknown): boolean {
|
|
14
|
+
if (!input || typeof input !== "object") return false;
|
|
15
|
+
const rec = input as Record<string, unknown>;
|
|
16
|
+
if (!("honeypot" in rec)) return false;
|
|
17
|
+
const val = rec["honeypot"];
|
|
18
|
+
if (val === null || val === undefined) return false;
|
|
19
|
+
if (typeof val === "string" && val.length === 0) return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abuse rule orchestrator for MCP write tools.
|
|
3
|
+
*
|
|
4
|
+
* Runs all in-process heuristics for the three anonymous write tools:
|
|
5
|
+
* capture_lead, subscribe_newsletter, request_callback.
|
|
6
|
+
*
|
|
7
|
+
* Rules applied (in order):
|
|
8
|
+
* 1. User-Agent deny list (applies to all write tools)
|
|
9
|
+
* 2. Honeypot field check (applies to all write tools)
|
|
10
|
+
* 3. Email format validation (capture_lead, subscribe_newsletter)
|
|
11
|
+
* 4. Per-IP daily cap (capture_lead only)
|
|
12
|
+
*
|
|
13
|
+
* The paid layer (lead-routing.ts) extends with real abuse-protection
|
|
14
|
+
* services. OSS gets basic heuristics that stop obvious bots.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { FastifyRequest } from "fastify";
|
|
18
|
+
|
|
19
|
+
import { DailyCapTracker } from "./daily-cap.js";
|
|
20
|
+
import { isValidEmail } from "./email-validator.js";
|
|
21
|
+
import { isHoneypotTripped } from "./honeypot.js";
|
|
22
|
+
import { buildDenyList, isUserAgentBlocked } from "./user-agent-deny.js";
|
|
23
|
+
|
|
24
|
+
/** Tools that require email format validation on their input. */
|
|
25
|
+
const EMAIL_VALIDATED_TOOLS = new Set(["capture_lead", "subscribe_newsletter"]);
|
|
26
|
+
|
|
27
|
+
/** Tools that apply the per-IP daily cap. */
|
|
28
|
+
const DAILY_CAPPED_TOOLS = new Set(["capture_lead"]);
|
|
29
|
+
|
|
30
|
+
export interface AbuseConfig {
|
|
31
|
+
/** Per-IP daily cap for capture_lead. Default 50. */
|
|
32
|
+
captureLeadDailyCapPerIp: number;
|
|
33
|
+
/** Comma-separated extra UA substrings to block. */
|
|
34
|
+
blockedUserAgents: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AbuseResult {
|
|
38
|
+
allowed: boolean;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class AbuseGuard {
|
|
43
|
+
private readonly denyList: RegExp[];
|
|
44
|
+
private readonly captureLeadCap: DailyCapTracker;
|
|
45
|
+
|
|
46
|
+
constructor(config: AbuseConfig) {
|
|
47
|
+
this.denyList = buildDenyList(config.blockedUserAgents);
|
|
48
|
+
this.captureLeadCap = new DailyCapTracker(config.captureLeadDailyCapPerIp);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validates all abuse rules for the given tool call.
|
|
53
|
+
*
|
|
54
|
+
* @param toolName - MCP tool name (e.g. "capture_lead")
|
|
55
|
+
* @param input - Parsed tool arguments object
|
|
56
|
+
* @param request - Fastify request (for IP + UA extraction)
|
|
57
|
+
*/
|
|
58
|
+
validate(
|
|
59
|
+
toolName: string,
|
|
60
|
+
input: unknown,
|
|
61
|
+
request: FastifyRequest,
|
|
62
|
+
): AbuseResult {
|
|
63
|
+
const ua = request.headers["user-agent"];
|
|
64
|
+
const ip = request.ip;
|
|
65
|
+
|
|
66
|
+
// Rule 1: User-Agent deny list
|
|
67
|
+
if (isUserAgentBlocked(ua, this.denyList)) {
|
|
68
|
+
return { allowed: false, reason: "blocked user agent" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Rule 2: Honeypot
|
|
72
|
+
if (isHoneypotTripped(input)) {
|
|
73
|
+
return { allowed: false, reason: "honeypot field filled" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Rule 3: Email format validation
|
|
77
|
+
if (EMAIL_VALIDATED_TOOLS.has(toolName)) {
|
|
78
|
+
const rec = input as Record<string, unknown>;
|
|
79
|
+
const email = rec["email"];
|
|
80
|
+
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
|
+
return {
|
|
89
|
+
allowed: false,
|
|
90
|
+
reason: "daily capture_lead cap reached for this IP",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { allowed: true };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convenience function for constructing an AbuseGuard from env vars.
|
|
101
|
+
* Reads MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP and MCP_ABUSE_BLOCKED_USER_AGENTS.
|
|
102
|
+
*/
|
|
103
|
+
export function createAbuseGuardFromEnv(): AbuseGuard {
|
|
104
|
+
return new AbuseGuard({
|
|
105
|
+
captureLeadDailyCapPerIp: parseInt(
|
|
106
|
+
process.env["MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP"] ?? "50",
|
|
107
|
+
10,
|
|
108
|
+
),
|
|
109
|
+
blockedUserAgents: process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-Agent deny list for abuse rules.
|
|
3
|
+
*
|
|
4
|
+
* Built-in list blocks well-known script-runner and scraper UAs.
|
|
5
|
+
* Additional patterns can be provided via MCP_ABUSE_BLOCKED_USER_AGENTS
|
|
6
|
+
* (comma-separated substrings; case-insensitive prefix/infix match).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Built-in patterns for well-known bot/script user agents.
|
|
11
|
+
* Each is a case-insensitive regex that matches a substring of the UA string.
|
|
12
|
+
*/
|
|
13
|
+
const BUILTIN_PATTERNS: RegExp[] = [
|
|
14
|
+
/python-requests/i,
|
|
15
|
+
/curl\//i,
|
|
16
|
+
/wget\//i,
|
|
17
|
+
/scrapy/i,
|
|
18
|
+
/go-http-client/i,
|
|
19
|
+
/libwww-perl/i,
|
|
20
|
+
/java\//i,
|
|
21
|
+
/httpx/i,
|
|
22
|
+
/aiohttp/i,
|
|
23
|
+
/node-fetch/i,
|
|
24
|
+
/axios/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the full deny list by merging built-in patterns with any
|
|
29
|
+
* comma-separated substrings from `extraPatterns` (e.g., from env).
|
|
30
|
+
*/
|
|
31
|
+
export function buildDenyList(extraPatterns?: string): RegExp[] {
|
|
32
|
+
const list = [...BUILTIN_PATTERNS];
|
|
33
|
+
|
|
34
|
+
if (extraPatterns) {
|
|
35
|
+
for (const raw of extraPatterns.split(",")) {
|
|
36
|
+
const trimmed = raw.trim();
|
|
37
|
+
if (trimmed.length > 0) {
|
|
38
|
+
// Escape special regex chars so literal substrings match correctly
|
|
39
|
+
const escaped = trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40
|
+
list.push(new RegExp(escaped, "i"));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return list;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the user agent string matches any pattern in the deny list,
|
|
50
|
+
* or if the UA is missing/empty (headless script behaviour).
|
|
51
|
+
*/
|
|
52
|
+
export function isUserAgentBlocked(
|
|
53
|
+
ua: string | null | undefined,
|
|
54
|
+
denyList: RegExp[],
|
|
55
|
+
): boolean {
|
|
56
|
+
if (!ua || ua.trim().length === 0) return true;
|
|
57
|
+
return denyList.some((pattern) => pattern.test(ua));
|
|
58
|
+
}
|