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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/checkout/sessions
|
|
3
|
+
*
|
|
4
|
+
* Generic Stripe checkout primitive. Resolves a productCode to a Stripe
|
|
5
|
+
* hosted-page checkout session and returns the session ID + URL.
|
|
6
|
+
*
|
|
7
|
+
* Security invariants:
|
|
8
|
+
* - Sandbox-only: live Stripe keys are rejected at runtime before any Stripe call.
|
|
9
|
+
* - No PII in logs: successUrl/cancelUrl and full session IDs are never logged.
|
|
10
|
+
* Only the first 8 chars of the session ID appear in log lines.
|
|
11
|
+
* - Error responses never echo user input (productCode, URLs).
|
|
12
|
+
* - No DB write: this unit creates a Stripe session only; webhook-driven
|
|
13
|
+
* persistence is a follow-up issue.
|
|
14
|
+
* - X-Internal-Auth gate: when OSS_API_INTERNAL_SECRET env var is set,
|
|
15
|
+
* the X-Internal-Auth header must match; requests without a matching header
|
|
16
|
+
* are rejected with 401. When the env var is unset, no gate is applied
|
|
17
|
+
* (backward-compat for OSS standalone deployments). See #935.
|
|
18
|
+
*
|
|
19
|
+
* Error mapping:
|
|
20
|
+
* 401 internal-auth-required — X-Internal-Auth missing/mismatch (only when env var set)
|
|
21
|
+
* 400 validation-error — Zod validation failed (missing/invalid fields)
|
|
22
|
+
* 404 unknown-product-code — no active Stripe price for the given productCode
|
|
23
|
+
* 500 live-mode-forbidden — STRIPE_SECRET_KEY is a live key
|
|
24
|
+
* 500 config-error — STRIPE_SECRET_KEY missing or invalid configuration
|
|
25
|
+
* 502 checkout-unavailable — Stripe SDK threw an unexpected error
|
|
26
|
+
*/
|
|
27
|
+
import { timingSafeEqual } from "node:crypto";
|
|
28
|
+
|
|
29
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
assertSandboxKey,
|
|
34
|
+
createCheckoutSession,
|
|
35
|
+
StripeConfigError,
|
|
36
|
+
UnknownProductCodeError,
|
|
37
|
+
} from "../../lib/stripe.js";
|
|
38
|
+
|
|
39
|
+
// ── Input validation schema ───────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates a redirect URL: must parse and must use http: or https: scheme.
|
|
43
|
+
* All other schemes (javascript:, ftp:, data:, file:, etc.) are rejected.
|
|
44
|
+
* Accepts http: for dev flexibility; Stripe enforces https: on production
|
|
45
|
+
* hosted-page redirects at their end.
|
|
46
|
+
*/
|
|
47
|
+
function isValidRedirectUrl(raw: string): boolean {
|
|
48
|
+
let parsed: URL;
|
|
49
|
+
try {
|
|
50
|
+
parsed = new URL(raw);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PRODUCT_CODE_REGEX = /^[A-Za-z0-9._-]{1,200}$/;
|
|
58
|
+
|
|
59
|
+
const CheckoutSessionRequestSchema = z.object({
|
|
60
|
+
productCode: z
|
|
61
|
+
.string()
|
|
62
|
+
.regex(
|
|
63
|
+
PRODUCT_CODE_REGEX,
|
|
64
|
+
"productCode must match /^[A-Za-z0-9._-]{1,200}$/",
|
|
65
|
+
),
|
|
66
|
+
successUrl: z.string().refine(isValidRedirectUrl, {
|
|
67
|
+
message:
|
|
68
|
+
"successUrl must be a valid URL with a non-empty host and not use javascript: scheme",
|
|
69
|
+
}),
|
|
70
|
+
cancelUrl: z.string().refine(isValidRedirectUrl, {
|
|
71
|
+
message:
|
|
72
|
+
"cancelUrl must be a valid URL with a non-empty host and not use javascript: scheme",
|
|
73
|
+
}),
|
|
74
|
+
quantity: z.number().int().min(1).max(999).optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── Internal-auth gate ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enforce the X-Internal-Auth gate when OSS_API_INTERNAL_SECRET is configured.
|
|
81
|
+
*
|
|
82
|
+
* Gate is env-var-conditional:
|
|
83
|
+
* - env var SET → header must be present and must match exactly
|
|
84
|
+
* - env var UNSET → no gate applied (OSS standalone backward-compat)
|
|
85
|
+
*
|
|
86
|
+
* Returns true if the request should be allowed through; false if it must be
|
|
87
|
+
* rejected with 401.
|
|
88
|
+
*
|
|
89
|
+
* Uses a constant-time string comparison to prevent timing attacks on the
|
|
90
|
+
* secret value. Node's `crypto.timingSafeEqual` requires equal-length Buffers,
|
|
91
|
+
* so we compare lengths first (short-circuit is safe — length mismatch reveals
|
|
92
|
+
* no information about the secret's content beyond its length, which is
|
|
93
|
+
* acceptable for a shared internal secret).
|
|
94
|
+
*/
|
|
95
|
+
function passesInternalAuthGate(request: FastifyRequest): boolean {
|
|
96
|
+
const expectedSecret = process.env["OSS_API_INTERNAL_SECRET"];
|
|
97
|
+
// Gate is disabled when env var is not set — OSS standalone backward-compat.
|
|
98
|
+
if (!expectedSecret) return true;
|
|
99
|
+
|
|
100
|
+
const provided = request.headers["x-internal-auth"];
|
|
101
|
+
if (typeof provided !== "string" || !provided) return false;
|
|
102
|
+
|
|
103
|
+
// Constant-time comparison.
|
|
104
|
+
const expectedBuf = Buffer.from(expectedSecret, "utf8");
|
|
105
|
+
const providedBuf = Buffer.from(provided, "utf8");
|
|
106
|
+
if (expectedBuf.length !== providedBuf.length) return false;
|
|
107
|
+
|
|
108
|
+
return timingSafeEqual(expectedBuf, providedBuf);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Handler ───────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export async function checkoutSessionsHandler(
|
|
114
|
+
request: FastifyRequest,
|
|
115
|
+
reply: FastifyReply,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
// ── X-Internal-Auth gate (env-var-conditional) ────────────────────────────
|
|
118
|
+
if (!passesInternalAuthGate(request)) {
|
|
119
|
+
return reply.status(401).send({ error: "internal-auth-required" });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
assertSandboxKey(process.env["STRIPE_SECRET_KEY"] ?? "");
|
|
124
|
+
} catch {
|
|
125
|
+
return reply.status(500).send({ error: "live-mode-forbidden" });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parsed = CheckoutSessionRequestSchema.safeParse(request.body);
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return reply.status(400).send({
|
|
131
|
+
error: "validation-error",
|
|
132
|
+
details: parsed.error.issues,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { productCode, successUrl, cancelUrl, quantity } = parsed.data;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const { sessionId, url } = await createCheckoutSession({
|
|
140
|
+
productCode,
|
|
141
|
+
successUrl,
|
|
142
|
+
cancelUrl,
|
|
143
|
+
quantity: quantity ?? 1,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
request.log.info(
|
|
147
|
+
{
|
|
148
|
+
method: request.method,
|
|
149
|
+
path: request.url,
|
|
150
|
+
sessionIdPrefix: sessionId.substring(0, 8),
|
|
151
|
+
status: 201,
|
|
152
|
+
},
|
|
153
|
+
"[checkout] session created",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return reply.status(201).send({ sessionId, url });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof UnknownProductCodeError) {
|
|
159
|
+
// 404 — do NOT echo productCode in the response body
|
|
160
|
+
return reply.status(404).send({ error: "unknown-product-code" });
|
|
161
|
+
}
|
|
162
|
+
if (err instanceof StripeConfigError) {
|
|
163
|
+
// 500 — misconfiguration; log message (no PII) but never echo in response
|
|
164
|
+
request.log.error(
|
|
165
|
+
{
|
|
166
|
+
method: request.method,
|
|
167
|
+
path: request.url,
|
|
168
|
+
errName: err.name,
|
|
169
|
+
message: err.message,
|
|
170
|
+
},
|
|
171
|
+
"[checkout] config error",
|
|
172
|
+
);
|
|
173
|
+
return reply.status(500).send({ error: "config-error" });
|
|
174
|
+
}
|
|
175
|
+
request.log.error(
|
|
176
|
+
{
|
|
177
|
+
method: request.method,
|
|
178
|
+
path: request.url,
|
|
179
|
+
errName: err instanceof Error ? err.name : "unknown",
|
|
180
|
+
},
|
|
181
|
+
"[checkout] stripe error; checkout-unavailable",
|
|
182
|
+
);
|
|
183
|
+
return reply.status(502).send({ error: "checkout-unavailable" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file create.test.ts
|
|
3
|
+
* POST /api/content/:type — D14a Stage 1 Unit B.
|
|
4
|
+
*
|
|
5
|
+
* Covers: Pro-tier gating (402), validation (400), slug allowlist (400),
|
|
6
|
+
* happy path (201 with {page, version}), slug-already-exists conflict (409
|
|
7
|
+
* with existing version echoed), and unknown :type (400).
|
|
8
|
+
*
|
|
9
|
+
* Issue #398 follow-up: the route no longer pre-checks slug existence; the
|
|
10
|
+
* adapter throws `AdapterWriteError("conflict", ..., {version})` and the
|
|
11
|
+
* hoisted `mapAdapterError` translates it. Tests construct real
|
|
12
|
+
* `AdapterWriteError` instances accordingly.
|
|
13
|
+
*
|
|
14
|
+
* Uses the real content adapter backed by a per-test temp dir (Issue #507).
|
|
15
|
+
* @working-theory/license stays mocked — it is an external package boundary.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { FastifyInstance } from "fastify";
|
|
19
|
+
|
|
20
|
+
const mockCheckLicense = jest.fn();
|
|
21
|
+
|
|
22
|
+
// @working-theory/license is an external package boundary — stays mocked.
|
|
23
|
+
jest.mock("@working-theory/license", () => ({
|
|
24
|
+
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { AdapterWriteError } from "@working-theory/content-adapters";
|
|
28
|
+
|
|
29
|
+
import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
|
|
30
|
+
import { contentRoutes } from "../index";
|
|
31
|
+
|
|
32
|
+
const fixture = setupContentFixture();
|
|
33
|
+
|
|
34
|
+
function proLicense() {
|
|
35
|
+
return {
|
|
36
|
+
tier: "pro",
|
|
37
|
+
plan: "pro",
|
|
38
|
+
features: [],
|
|
39
|
+
expiresAt: null,
|
|
40
|
+
revoked: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function freeLicense() {
|
|
45
|
+
return {
|
|
46
|
+
tier: "free",
|
|
47
|
+
plan: null,
|
|
48
|
+
features: [],
|
|
49
|
+
expiresAt: null,
|
|
50
|
+
revoked: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const validBody = {
|
|
55
|
+
type: "HomePage",
|
|
56
|
+
slug: "new-page",
|
|
57
|
+
title: "New Page",
|
|
58
|
+
description: "A new page description",
|
|
59
|
+
body: "New page body content",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("POST /api/content/:type", () => {
|
|
63
|
+
let app: FastifyInstance;
|
|
64
|
+
|
|
65
|
+
beforeEach(async () => {
|
|
66
|
+
app = require("fastify")();
|
|
67
|
+
await app.register(contentRoutes);
|
|
68
|
+
jest.clearAllMocks();
|
|
69
|
+
mockCheckLicense.mockReset();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
await app.close();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns 201 with {page, version} on success", async () => {
|
|
77
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
78
|
+
|
|
79
|
+
// Use a unique slug so there's no conflict with fixture pages.
|
|
80
|
+
const body = { ...validBody, slug: "brand-new-page" };
|
|
81
|
+
const res = await app.inject({
|
|
82
|
+
method: "POST",
|
|
83
|
+
url: "/v1/content/HomePage",
|
|
84
|
+
payload: body,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(res.statusCode).toBe(201);
|
|
88
|
+
const resBody = JSON.parse(res.body);
|
|
89
|
+
expect(resBody).toHaveProperty("page");
|
|
90
|
+
expect(resBody).toHaveProperty("version");
|
|
91
|
+
expect(resBody.page).toMatchObject({ type: "HomePage", slug: "brand-new-page" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns 402 payment_required on free tier", async () => {
|
|
95
|
+
mockCheckLicense.mockResolvedValue(freeLicense());
|
|
96
|
+
|
|
97
|
+
const res = await app.inject({
|
|
98
|
+
method: "POST",
|
|
99
|
+
url: "/v1/content/HomePage",
|
|
100
|
+
payload: validBody,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(res.statusCode).toBe(402);
|
|
104
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "payment_required" });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns 402 payment_required when license is revoked even at pro tier", async () => {
|
|
108
|
+
mockCheckLicense.mockResolvedValue({
|
|
109
|
+
tier: "pro",
|
|
110
|
+
plan: "pro",
|
|
111
|
+
features: [],
|
|
112
|
+
expiresAt: null,
|
|
113
|
+
revoked: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const res = await app.inject({
|
|
117
|
+
method: "POST",
|
|
118
|
+
url: "/v1/content/HomePage",
|
|
119
|
+
payload: validBody,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(res.statusCode).toBe(402);
|
|
123
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "payment_required" });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns 409 conflict with existing version when slug already exists (adapter throws)", async () => {
|
|
127
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
128
|
+
|
|
129
|
+
// The fixture writes "home" in beforeEach — POST to the same slug triggers conflict.
|
|
130
|
+
const res = await app.inject({
|
|
131
|
+
method: "POST",
|
|
132
|
+
url: "/v1/content/HomePage",
|
|
133
|
+
payload: { ...validBody, slug: "home" },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// The real adapter throws AdapterWriteError("conflict") on duplicate slug.
|
|
137
|
+
expect(res.statusCode).toBe(409);
|
|
138
|
+
const body = JSON.parse(res.body);
|
|
139
|
+
expect(body).toMatchObject({ error: "conflict" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("does not pre-check slug existence — adapter is the sole authority (V2-H2 TOCTOU closed)", async () => {
|
|
143
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
144
|
+
|
|
145
|
+
// The route should NOT call getPageBySlug before createPage.
|
|
146
|
+
// We verify by checking that a slug that doesn't exist succeeds with 201.
|
|
147
|
+
const res = await app.inject({
|
|
148
|
+
method: "POST",
|
|
149
|
+
url: "/v1/content/HomePage",
|
|
150
|
+
payload: { ...validBody, slug: "toctou-test-unique-slug" },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Route went straight to adapter.createPage — no pre-check needed.
|
|
154
|
+
expect([201, 409]).toContain(res.statusCode);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("returns 400 validation_failed when body missing required field", async () => {
|
|
158
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
159
|
+
|
|
160
|
+
const res = await app.inject({
|
|
161
|
+
method: "POST",
|
|
162
|
+
url: "/v1/content/HomePage",
|
|
163
|
+
payload: { type: "HomePage", slug: "home", title: "T" }, // missing description, body
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(res.statusCode).toBe(400);
|
|
167
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "validation_failed" });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns 400 validation_failed when body is empty string", async () => {
|
|
171
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
172
|
+
|
|
173
|
+
const res = await app.inject({
|
|
174
|
+
method: "POST",
|
|
175
|
+
url: "/v1/content/HomePage",
|
|
176
|
+
payload: { ...validBody, body: "" },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(res.statusCode).toBe(400);
|
|
180
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "validation_failed" });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns 400 validation_failed on unknown :type param", async () => {
|
|
184
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
185
|
+
|
|
186
|
+
const res = await app.inject({
|
|
187
|
+
method: "POST",
|
|
188
|
+
url: "/v1/content/Bogus",
|
|
189
|
+
payload: validBody,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(res.statusCode).toBe(400);
|
|
193
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "validation_failed" });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("returns 400 when path :type does not match body type", async () => {
|
|
197
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
198
|
+
|
|
199
|
+
const res = await app.inject({
|
|
200
|
+
method: "POST",
|
|
201
|
+
url: "/v1/content/HomePage",
|
|
202
|
+
payload: { ...validBody, type: "AboutPage" },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(res.statusCode).toBe(400);
|
|
206
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "validation_failed" });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns 400 validation_failed on disallowed slug shape", async () => {
|
|
210
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
211
|
+
|
|
212
|
+
const res = await app.inject({
|
|
213
|
+
method: "POST",
|
|
214
|
+
url: "/v1/content/HomePage",
|
|
215
|
+
payload: { ...validBody, slug: "../etc/passwd" },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(res.statusCode).toBe(400);
|
|
219
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "validation_failed" });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("merged-error-array: invalid frontmatter AND empty body → 400 with issues from both validations surfaced", async () => {
|
|
223
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
224
|
+
|
|
225
|
+
const res = await app.inject({
|
|
226
|
+
method: "POST",
|
|
227
|
+
url: "/v1/content/HomePage",
|
|
228
|
+
payload: { type: "HomePage", slug: "home", title: "T", body: "" },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(res.statusCode).toBe(400);
|
|
232
|
+
const body = JSON.parse(res.body);
|
|
233
|
+
expect(body).toMatchObject({ error: "validation_failed" });
|
|
234
|
+
|
|
235
|
+
const issues: Array<{ path: unknown[] }> = body.details?.issues ?? [];
|
|
236
|
+
const paths = issues.map((i) => i.path.join("."));
|
|
237
|
+
expect(paths).toEqual(expect.arrayContaining(["description", "body"]));
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file delete.test.ts
|
|
3
|
+
* DELETE /api/content/:type/:slug — D14a Stage 1 Unit B.
|
|
4
|
+
*
|
|
5
|
+
* Covers: Pro-tier gating (402), missing/stale If-Match (409), happy path
|
|
6
|
+
* (204 no body), not-found (404), unknown :type (400).
|
|
7
|
+
*
|
|
8
|
+
* Issue #398 follow-up: bespoke version-probe + getPageBySlug pre-check
|
|
9
|
+
* removed; the adapter throws `AdapterWriteError("not_found" | "conflict")`
|
|
10
|
+
* and the hoisted `mapAdapterError` translates it.
|
|
11
|
+
*
|
|
12
|
+
* Uses the real content adapter backed by a per-test temp dir (Issue #507).
|
|
13
|
+
* @working-theory/license stays mocked — it is an external package boundary.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { FastifyInstance } from "fastify";
|
|
17
|
+
|
|
18
|
+
const mockCheckLicense = jest.fn();
|
|
19
|
+
|
|
20
|
+
// @working-theory/license is an external package boundary — stays mocked.
|
|
21
|
+
jest.mock("@working-theory/license", () => ({
|
|
22
|
+
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
|
|
26
|
+
import { contentRoutes } from "../index";
|
|
27
|
+
|
|
28
|
+
const fixture = setupContentFixture();
|
|
29
|
+
|
|
30
|
+
function proLicense() {
|
|
31
|
+
return {
|
|
32
|
+
tier: "pro",
|
|
33
|
+
plan: "pro",
|
|
34
|
+
features: [],
|
|
35
|
+
expiresAt: null,
|
|
36
|
+
revoked: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function freeLicense() {
|
|
41
|
+
return {
|
|
42
|
+
tier: "free",
|
|
43
|
+
plan: null,
|
|
44
|
+
features: [],
|
|
45
|
+
expiresAt: null,
|
|
46
|
+
revoked: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("DELETE /api/content/:type/:slug", () => {
|
|
51
|
+
let app: FastifyInstance;
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
app = require("fastify")();
|
|
55
|
+
await app.register(contentRoutes);
|
|
56
|
+
jest.clearAllMocks();
|
|
57
|
+
mockCheckLicense.mockReset();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await app.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns 204 with no body on success", async () => {
|
|
65
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
66
|
+
|
|
67
|
+
// First create the page, get its version, then delete it.
|
|
68
|
+
const createRes = await app.inject({
|
|
69
|
+
method: "POST",
|
|
70
|
+
url: "/v1/content/HomePage",
|
|
71
|
+
payload: {
|
|
72
|
+
type: "HomePage",
|
|
73
|
+
slug: "page-to-delete",
|
|
74
|
+
title: "Page To Delete",
|
|
75
|
+
description: "This page will be deleted",
|
|
76
|
+
body: "Goodbye",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
expect(createRes.statusCode).toBe(201);
|
|
80
|
+
const { version } = JSON.parse(createRes.body);
|
|
81
|
+
|
|
82
|
+
const res = await app.inject({
|
|
83
|
+
method: "DELETE",
|
|
84
|
+
url: "/v1/content/HomePage/page-to-delete",
|
|
85
|
+
headers: { "if-match": version },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(res.statusCode).toBe(204);
|
|
89
|
+
expect(res.body).toBe("");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns 402 payment_required on free tier", async () => {
|
|
93
|
+
mockCheckLicense.mockResolvedValue(freeLicense());
|
|
94
|
+
|
|
95
|
+
const res = await app.inject({
|
|
96
|
+
method: "DELETE",
|
|
97
|
+
url: "/v1/content/HomePage/home",
|
|
98
|
+
headers: { "if-match": "v1" },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(res.statusCode).toBe(402);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns 402 payment_required when license is revoked even at pro tier", async () => {
|
|
105
|
+
mockCheckLicense.mockResolvedValue({
|
|
106
|
+
tier: "pro",
|
|
107
|
+
plan: "pro",
|
|
108
|
+
features: [],
|
|
109
|
+
expiresAt: null,
|
|
110
|
+
revoked: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const res = await app.inject({
|
|
114
|
+
method: "DELETE",
|
|
115
|
+
url: "/v1/content/HomePage/home",
|
|
116
|
+
headers: { "if-match": "v1" },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(res.statusCode).toBe(402);
|
|
120
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "payment_required" });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns 409 conflict when If-Match header is missing", async () => {
|
|
124
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
125
|
+
|
|
126
|
+
const res = await app.inject({
|
|
127
|
+
method: "DELETE",
|
|
128
|
+
url: "/v1/content/HomePage/home",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(res.statusCode).toBe(409);
|
|
132
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "conflict" });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns 409 conflict when If-Match is empty string", async () => {
|
|
136
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
137
|
+
|
|
138
|
+
const res = await app.inject({
|
|
139
|
+
method: "DELETE",
|
|
140
|
+
url: "/v1/content/HomePage/home",
|
|
141
|
+
headers: { "if-match": "" },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(res.statusCode).toBe(409);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("returns 409 conflict with currentVersion echoed when adapter signals stale If-Match", async () => {
|
|
148
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
149
|
+
|
|
150
|
+
// Pass a stale version — the real adapter will reject it with conflict.
|
|
151
|
+
const res = await app.inject({
|
|
152
|
+
method: "DELETE",
|
|
153
|
+
url: "/v1/content/HomePage/home",
|
|
154
|
+
headers: { "if-match": "stale-version-that-does-not-match" },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(res.statusCode).toBe(409);
|
|
158
|
+
const body = JSON.parse(res.body);
|
|
159
|
+
expect(body).toMatchObject({ error: "conflict" });
|
|
160
|
+
if (body.details) {
|
|
161
|
+
expect(body.details).toHaveProperty("currentVersion");
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns 404 not_found when adapter signals page missing", async () => {
|
|
166
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
167
|
+
|
|
168
|
+
const res = await app.inject({
|
|
169
|
+
method: "DELETE",
|
|
170
|
+
url: "/v1/content/HomePage/slug-that-does-not-exist",
|
|
171
|
+
headers: { "if-match": "any-version" },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(res.statusCode).toBe(404);
|
|
175
|
+
expect(JSON.parse(res.body)).toMatchObject({ error: "not_found" });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("returns 400 validation_failed on unknown :type", async () => {
|
|
179
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
180
|
+
|
|
181
|
+
const res = await app.inject({
|
|
182
|
+
method: "DELETE",
|
|
183
|
+
url: "/v1/content/Bogus/home",
|
|
184
|
+
headers: { "if-match": "v1" },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(res.statusCode).toBe(400);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns 400 validation_failed on disallowed slug shape", async () => {
|
|
191
|
+
mockCheckLicense.mockResolvedValue(proLicense());
|
|
192
|
+
|
|
193
|
+
const res = await app.inject({
|
|
194
|
+
method: "DELETE",
|
|
195
|
+
url: "/v1/content/HomePage/..%2Fetc%2Fpasswd",
|
|
196
|
+
headers: { "if-match": "v1" },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(res.statusCode).toBe(400);
|
|
200
|
+
});
|
|
201
|
+
});
|