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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the require-paid-tier Fastify preHandler.
|
|
3
|
+
*
|
|
4
|
+
* Mocks @working-theory/license `checkLicense` and asserts:
|
|
5
|
+
* - free tier → reply.status(402).send(payment_required canonical body)
|
|
6
|
+
* - pro tier → preHandler returns void (handler proceeds)
|
|
7
|
+
* - checkLicense throws → safe-by-default 402 (no false-positive proceed)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const mockCheckLicense = jest.fn();
|
|
11
|
+
|
|
12
|
+
jest.mock("@working-theory/license", () => ({
|
|
13
|
+
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
17
|
+
|
|
18
|
+
import { requirePaidTier } from "../require-paid-tier.js";
|
|
19
|
+
|
|
20
|
+
// ── Typed reply builder ───────────────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// A minimal Fastify reply stub that satisfies the FastifyReply interface surface
|
|
23
|
+
// used by requirePaidTier (status + send chain). Using a properly typed factory
|
|
24
|
+
// avoids the `as unknown as FastifyReply` double assertion pattern.
|
|
25
|
+
|
|
26
|
+
interface StubReply {
|
|
27
|
+
statusCalled: number | null;
|
|
28
|
+
sendCalled: unknown;
|
|
29
|
+
status: jest.MockedFunction<(code: number) => StubReply>;
|
|
30
|
+
send: jest.MockedFunction<(body: unknown) => StubReply>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeReply(): StubReply {
|
|
34
|
+
const reply: StubReply = {
|
|
35
|
+
statusCalled: null,
|
|
36
|
+
sendCalled: undefined,
|
|
37
|
+
status: jest.fn(),
|
|
38
|
+
send: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
reply.status.mockImplementation((code: number) => {
|
|
41
|
+
reply.statusCalled = code;
|
|
42
|
+
return reply;
|
|
43
|
+
});
|
|
44
|
+
reply.send.mockImplementation((body: unknown) => {
|
|
45
|
+
reply.sendCalled = body;
|
|
46
|
+
return reply;
|
|
47
|
+
});
|
|
48
|
+
return reply;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Cast the stub to FastifyReply once, at a typed boundary, so all call-sites
|
|
52
|
+
// use the actual StubReply type rather than repeating the double assertion.
|
|
53
|
+
function asReply(stub: StubReply): FastifyReply {
|
|
54
|
+
// @ts-expect-error: StubReply satisfies the status+send surface used by requirePaidTier;
|
|
55
|
+
// a full Fastify reply cannot be constructed without a live server instance in unit tests.
|
|
56
|
+
return stub;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fakeRequest = {} as FastifyRequest;
|
|
60
|
+
|
|
61
|
+
describe("requirePaidTier", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
mockCheckLicense.mockReset();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("free tier — replies 402 with canonical payment_required body", async () => {
|
|
67
|
+
mockCheckLicense.mockResolvedValue({
|
|
68
|
+
tier: "free",
|
|
69
|
+
plan: null,
|
|
70
|
+
features: [],
|
|
71
|
+
expiresAt: null,
|
|
72
|
+
revoked: false,
|
|
73
|
+
});
|
|
74
|
+
const reply = makeReply();
|
|
75
|
+
|
|
76
|
+
await requirePaidTier(fakeRequest, asReply(reply));
|
|
77
|
+
|
|
78
|
+
expect(reply.statusCalled).toBe(402);
|
|
79
|
+
expect(reply.sendCalled).toEqual({
|
|
80
|
+
error: "payment_required",
|
|
81
|
+
message: expect.stringContaining("paid license"),
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("pro tier — proceeds without calling reply.status/send", async () => {
|
|
86
|
+
mockCheckLicense.mockResolvedValue({
|
|
87
|
+
tier: "pro",
|
|
88
|
+
plan: "pro",
|
|
89
|
+
features: [],
|
|
90
|
+
expiresAt: null,
|
|
91
|
+
revoked: false,
|
|
92
|
+
});
|
|
93
|
+
const reply = makeReply();
|
|
94
|
+
|
|
95
|
+
await requirePaidTier(fakeRequest, asReply(reply));
|
|
96
|
+
|
|
97
|
+
expect(reply.status).not.toHaveBeenCalled();
|
|
98
|
+
expect(reply.send).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("checkLicense throws — degrades to 402 (safe default)", async () => {
|
|
102
|
+
mockCheckLicense.mockRejectedValue(new Error("boom"));
|
|
103
|
+
const reply = makeReply();
|
|
104
|
+
|
|
105
|
+
await requirePaidTier(fakeRequest, asReply(reply));
|
|
106
|
+
|
|
107
|
+
expect(reply.statusCalled).toBe(402);
|
|
108
|
+
expect(reply.sendCalled).toMatchObject({ error: "payment_required" });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("revoked paid license — replies 402 even though tier is pro (issue #398)", async () => {
|
|
112
|
+
// After a 403/revoked refresh, checkLicense() returns tier:'pro' until
|
|
113
|
+
// the existing JWT exp (up to 24h). The revoked flag is the early
|
|
114
|
+
// signal — we must close the gate now, not wait for exp.
|
|
115
|
+
mockCheckLicense.mockResolvedValue({
|
|
116
|
+
tier: "pro",
|
|
117
|
+
plan: "pro",
|
|
118
|
+
features: [],
|
|
119
|
+
expiresAt: null,
|
|
120
|
+
revoked: true,
|
|
121
|
+
});
|
|
122
|
+
const reply = makeReply();
|
|
123
|
+
|
|
124
|
+
await requirePaidTier(fakeRequest, asReply(reply));
|
|
125
|
+
|
|
126
|
+
expect(reply.statusCalled).toBe(402);
|
|
127
|
+
expect(reply.sendCalled).toMatchObject({ error: "payment_required" });
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apps/api stripe shim — surface-drift smoke tests.
|
|
3
|
+
*
|
|
4
|
+
* Ensures apps/api/src/lib/stripe.ts re-exports exactly what
|
|
5
|
+
* @working-theory/stripe-client exports, with no surface drift.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
jest.mock("@working-theory/stripe-client", () => ({
|
|
9
|
+
getStripeClient: jest.fn(),
|
|
10
|
+
assertSandboxKey: jest.fn(),
|
|
11
|
+
verifyWebhookSignature: jest.fn(),
|
|
12
|
+
createCheckoutSession: jest.fn(),
|
|
13
|
+
STRIPE_API_VERSION: "2023-10-16",
|
|
14
|
+
WEBHOOK_TOLERANCE_SECONDS: 300,
|
|
15
|
+
StripeConfigError: class StripeConfigError extends Error {},
|
|
16
|
+
StripeWebhookSignatureError: class StripeWebhookSignatureError extends Error {},
|
|
17
|
+
UnknownProductCodeError: class UnknownProductCodeError extends Error {},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Import the shim (not the wrapper directly)
|
|
21
|
+
import * as stripeShim from "../stripe.js";
|
|
22
|
+
import * as stripeClient from "@working-theory/stripe-client";
|
|
23
|
+
|
|
24
|
+
describe("apps/api/src/lib/stripe — surface re-exports", () => {
|
|
25
|
+
it("exports getStripeClient as a function", () => {
|
|
26
|
+
expect(typeof stripeShim.getStripeClient).toBe("function");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("exports assertSandboxKey as a function", () => {
|
|
30
|
+
expect(typeof stripeShim.assertSandboxKey).toBe("function");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("exports verifyWebhookSignature as a function", () => {
|
|
34
|
+
expect(typeof stripeShim.verifyWebhookSignature).toBe("function");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("exports createCheckoutSession as a function", () => {
|
|
38
|
+
expect(typeof stripeShim.createCheckoutSession).toBe("function");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("exports STRIPE_API_VERSION", () => {
|
|
42
|
+
expect(stripeShim.STRIPE_API_VERSION).toBe("2023-10-16");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("exports WEBHOOK_TOLERANCE_SECONDS", () => {
|
|
46
|
+
expect(stripeShim.WEBHOOK_TOLERANCE_SECONDS).toBe(300);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("exports StripeConfigError as a constructor producing Error instances", () => {
|
|
50
|
+
expect(typeof stripeShim.StripeConfigError).toBe("function");
|
|
51
|
+
expect(new stripeShim.StripeConfigError()).toBeInstanceOf(Error);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("exports StripeWebhookSignatureError as a constructor producing Error instances", () => {
|
|
55
|
+
expect(typeof stripeShim.StripeWebhookSignatureError).toBe("function");
|
|
56
|
+
expect(new stripeShim.StripeWebhookSignatureError()).toBeInstanceOf(Error);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("exports UnknownProductCodeError as a constructor producing Error instances", () => {
|
|
60
|
+
expect(typeof stripeShim.UnknownProductCodeError).toBe("function");
|
|
61
|
+
expect(new stripeShim.UnknownProductCodeError()).toBeInstanceOf(Error);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("shim functions are the same references as @working-theory/stripe-client exports", () => {
|
|
65
|
+
expect(stripeShim.getStripeClient).toBe(stripeClient.getStripeClient);
|
|
66
|
+
expect(stripeShim.assertSandboxKey).toBe(stripeClient.assertSandboxKey);
|
|
67
|
+
expect(stripeShim.verifyWebhookSignature).toBe(
|
|
68
|
+
stripeClient.verifyWebhookSignature,
|
|
69
|
+
);
|
|
70
|
+
expect(stripeShim.createCheckoutSession).toBe(
|
|
71
|
+
stripeClient.createCheckoutSession,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared content adapter singleton for apps/api.
|
|
3
|
+
*
|
|
4
|
+
* The MDX adapter is the v1 default. Set `CONTENT_ADAPTER=projection` to opt
|
|
5
|
+
* into the build-time projection adapter backed by a compiled manifest.
|
|
6
|
+
*
|
|
7
|
+
* Adapter selection:
|
|
8
|
+
* - (unset or any value other than "projection") → MDX adapter
|
|
9
|
+
* - "projection" → projection adapter (requires compiled manifest)
|
|
10
|
+
*
|
|
11
|
+
* Content-root resolution order:
|
|
12
|
+
* 1. `CONTENT_ROOT` env var (explicit override — recommended for production)
|
|
13
|
+
* 2. `<cwd>/content` (works when the server is started from the repo root)
|
|
14
|
+
* 3. `<cwd>/../../content` (works when started from `apps/api/` via pnpm)
|
|
15
|
+
*
|
|
16
|
+
* Manifest-path resolution (projection only):
|
|
17
|
+
* 1. `MANIFEST_PATH` env var (explicit override)
|
|
18
|
+
* 2. `<content-root>/.compiled/manifest.json` (default)
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { resolve } from "node:path";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
createMdxAdapter,
|
|
25
|
+
type ContentAdapter,
|
|
26
|
+
type MdxContentAdapter,
|
|
27
|
+
} from "@working-theory/content-adapters";
|
|
28
|
+
import { createProjectionAdapter } from "@working-theory/content-projection";
|
|
29
|
+
import { loadManifestFromDisk } from "@working-theory/content-projection/node";
|
|
30
|
+
|
|
31
|
+
import { readDefaultLocale } from "./locales.js";
|
|
32
|
+
|
|
33
|
+
let cachedAdapter: ContentAdapter | undefined;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the content root. Honours `CONTENT_ROOT` env first, then probes the
|
|
37
|
+
* cwd-relative candidates above.
|
|
38
|
+
*/
|
|
39
|
+
export function getContentRoot(): string {
|
|
40
|
+
if (process.env.CONTENT_ROOT) return process.env.CONTENT_ROOT;
|
|
41
|
+
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
const cwdContent = resolve(cwd, "content");
|
|
44
|
+
const repoContent = resolve(cwd, "../../content");
|
|
45
|
+
if (existsSync(cwdContent)) return cwdContent;
|
|
46
|
+
if (existsSync(repoContent)) return repoContent;
|
|
47
|
+
// Last resort — cwd-relative, even if missing. The adapter will return
|
|
48
|
+
// null/empty for missing pages; routes will surface 404s.
|
|
49
|
+
return cwdContent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns the process-wide content adapter, lazily constructed on first use.
|
|
54
|
+
* Safe to call repeatedly. Adapter type is determined by `CONTENT_ADAPTER` env.
|
|
55
|
+
*/
|
|
56
|
+
export function getContentAdapter(): ContentAdapter {
|
|
57
|
+
if (!cachedAdapter) {
|
|
58
|
+
cachedAdapter = buildAdapter();
|
|
59
|
+
}
|
|
60
|
+
return cachedAdapter;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildAdapter(): ContentAdapter {
|
|
64
|
+
const contentRoot = getContentRoot();
|
|
65
|
+
|
|
66
|
+
if (process.env.CONTENT_ADAPTER === "projection") {
|
|
67
|
+
const manifestPath =
|
|
68
|
+
process.env.MANIFEST_PATH ??
|
|
69
|
+
resolve(contentRoot, ".compiled", "manifest.json");
|
|
70
|
+
|
|
71
|
+
const manifest = loadManifestFromDisk(manifestPath);
|
|
72
|
+
return createProjectionAdapter({ manifest });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return createMdxAdapter({ contentRoot, defaultLocale: readDefaultLocale() });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Test/utility helper — drop the cached adapter so the next call rebuilds
|
|
80
|
+
* with current env. Not used in production.
|
|
81
|
+
*/
|
|
82
|
+
export function resetContentAdapter(): void {
|
|
83
|
+
cachedAdapter = undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type { MdxContentAdapter };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify error-serializer — prevents internal error detail from leaking to clients.
|
|
3
|
+
*
|
|
4
|
+
* Fastify's default error handler echoes `err.message` (and in dev,
|
|
5
|
+
* `err.stack`) in the response body. For internal errors (DB failures, SDK
|
|
6
|
+
* exceptions, unhandled throws) that's sensitive: connection strings, host
|
|
7
|
+
* names, schema hints, internal paths. This handler:
|
|
8
|
+
*
|
|
9
|
+
* - Maps any 5xx-class internal Error to `{ error: "internal_error" }`
|
|
10
|
+
* with no detail
|
|
11
|
+
* - Logs the full Error (message, stack, request context) to pino at
|
|
12
|
+
* error level
|
|
13
|
+
* - Lets Fastify's built-in 4xx validation errors flow through
|
|
14
|
+
* (their `validation` payload is safe to surface — it describes the
|
|
15
|
+
* client's input, not server internals)
|
|
16
|
+
* - Leaves replies that handlers already committed (explicit status + body)
|
|
17
|
+
* untouched
|
|
18
|
+
*/
|
|
19
|
+
import type { FastifyError, FastifyInstance } from "fastify";
|
|
20
|
+
|
|
21
|
+
const GENERIC_INTERNAL_BODY = { error: "internal_error" } as const;
|
|
22
|
+
|
|
23
|
+
export function registerErrorSerializer(server: FastifyInstance): void {
|
|
24
|
+
server.setErrorHandler(async (err: FastifyError, request, reply) => {
|
|
25
|
+
// Validation errors (Fastify's schema validator) — keep structured detail.
|
|
26
|
+
// These describe the client request, not server internals.
|
|
27
|
+
if (err.validation) {
|
|
28
|
+
const status = err.statusCode ?? 400;
|
|
29
|
+
request.log.warn(
|
|
30
|
+
{
|
|
31
|
+
method: request.method,
|
|
32
|
+
path: request.url,
|
|
33
|
+
validation: err.validation,
|
|
34
|
+
},
|
|
35
|
+
"request validation failed",
|
|
36
|
+
);
|
|
37
|
+
return reply.code(status).send({
|
|
38
|
+
error: "validation-error",
|
|
39
|
+
details: err.validation,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Explicit 4xx errors thrown by handlers (e.g. fastify.httpErrors.notFound)
|
|
44
|
+
// also flow through — they're a deliberate boundary signal, not an
|
|
45
|
+
// internal leak.
|
|
46
|
+
const status = err.statusCode ?? 500;
|
|
47
|
+
if (status >= 400 && status < 500) {
|
|
48
|
+
request.log.info(
|
|
49
|
+
{
|
|
50
|
+
method: request.method,
|
|
51
|
+
path: request.url,
|
|
52
|
+
statusCode: status,
|
|
53
|
+
errName: err.name,
|
|
54
|
+
},
|
|
55
|
+
"request returned 4xx",
|
|
56
|
+
);
|
|
57
|
+
// Don't echo err.message — handlers that want a structured body should
|
|
58
|
+
// call reply.code().send() directly. This branch is the safe fallback.
|
|
59
|
+
return reply.code(status).send({ error: err.code ?? "client_error" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5xx — log everything, return nothing identifying.
|
|
63
|
+
request.log.error(
|
|
64
|
+
{
|
|
65
|
+
method: request.method,
|
|
66
|
+
path: request.url,
|
|
67
|
+
errName: err.name,
|
|
68
|
+
errMessage: err.message,
|
|
69
|
+
errStack: err.stack,
|
|
70
|
+
errCode: err.code,
|
|
71
|
+
},
|
|
72
|
+
"internal server error",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return reply.code(500).send(GENERIC_INTERNAL_BODY);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical error shape for apps/api content-write routes (D14a Stage 0).
|
|
3
|
+
*
|
|
4
|
+
* Every error response from create/update/delete content routes returns this
|
|
5
|
+
* shape as the JSON body, paired with the appropriate HTTP status. The MCP
|
|
6
|
+
* twin (`apps/mcp/src/lib/errors.ts`) returns the same shape sans `status` —
|
|
7
|
+
* MCP carries the error in the tool result body itself.
|
|
8
|
+
*
|
|
9
|
+
* Codes are a closed set; consumers can switch on them to drive UI / retry
|
|
10
|
+
* logic without parsing free-form `message` text. New codes get added here
|
|
11
|
+
* before any route uses them.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const err = validationError("Invalid type or slug", { field: "type" });
|
|
15
|
+
* return reply.status(err.status).send(err.body);
|
|
16
|
+
*/
|
|
17
|
+
import { mapAdapterError } from "@working-theory/content-adapters";
|
|
18
|
+
|
|
19
|
+
export type ErrorCode =
|
|
20
|
+
| "validation_failed"
|
|
21
|
+
| "not_found"
|
|
22
|
+
| "conflict"
|
|
23
|
+
| "payment_required"
|
|
24
|
+
| "unauthorized";
|
|
25
|
+
|
|
26
|
+
export interface ErrorBody {
|
|
27
|
+
error: ErrorCode;
|
|
28
|
+
message: string;
|
|
29
|
+
details?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ErrorResponse {
|
|
33
|
+
status: number;
|
|
34
|
+
body: ErrorBody;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeBody(
|
|
38
|
+
code: ErrorCode,
|
|
39
|
+
message: string,
|
|
40
|
+
details?: Record<string, unknown>,
|
|
41
|
+
): ErrorBody {
|
|
42
|
+
if (details === undefined) {
|
|
43
|
+
return { error: code, message };
|
|
44
|
+
}
|
|
45
|
+
return { error: code, message, details };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validationError(
|
|
49
|
+
message: string,
|
|
50
|
+
details?: Record<string, unknown>,
|
|
51
|
+
): ErrorResponse {
|
|
52
|
+
return { status: 400, body: makeBody("validation_failed", message, details) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function unauthorizedError(message: string): ErrorResponse {
|
|
56
|
+
return { status: 401, body: makeBody("unauthorized", message) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function paymentRequiredError(message: string): ErrorResponse {
|
|
60
|
+
return { status: 402, body: makeBody("payment_required", message) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function notFoundError(message: string): ErrorResponse {
|
|
64
|
+
return { status: 404, body: makeBody("not_found", message) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function conflictError(
|
|
68
|
+
message: string,
|
|
69
|
+
details?: Record<string, unknown>,
|
|
70
|
+
): ErrorResponse {
|
|
71
|
+
return { status: 409, body: makeBody("conflict", message, details) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Route-layer adapter for `mapAdapterError` from `@working-theory/content-adapters`.
|
|
76
|
+
* The package emits a canonical `{error, message, details?}` body but
|
|
77
|
+
* cannot know the HTTP status (it has no app dependency). This helper
|
|
78
|
+
* pairs the body with the right status. Returns `null` if the error
|
|
79
|
+
* wasn't an `AdapterWriteError` (caller should treat as 500).
|
|
80
|
+
*/
|
|
81
|
+
export function adapterErrorResponse(
|
|
82
|
+
err: unknown,
|
|
83
|
+
ctx: { type: string; slug: string },
|
|
84
|
+
): ErrorResponse | null {
|
|
85
|
+
const body = mapAdapterError(err, ctx);
|
|
86
|
+
if (body === null) return null;
|
|
87
|
+
switch (body.error) {
|
|
88
|
+
case "conflict":
|
|
89
|
+
return { status: 409, body };
|
|
90
|
+
case "not_found":
|
|
91
|
+
return { status: 404, body };
|
|
92
|
+
case "validation_failed":
|
|
93
|
+
return { status: 400, body };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-fallback writer for the AILK inbox.
|
|
3
|
+
*
|
|
4
|
+
* Writes a Lead to ${AILK_INBOX_FALLBACK_DIR ?? '.ailk/inbox'}/<ISO-ts>-<lead.id>.json
|
|
5
|
+
* when the database is unavailable. Engaged by B6's lead-routing service on DB failure.
|
|
6
|
+
*
|
|
7
|
+
* Path-traversal-safe by construction:
|
|
8
|
+
* 1. lead.id is validated against /^[a-z0-9_-]{8,64}$/i before any fs operation.
|
|
9
|
+
* 2. The resolved write path is checked to be strictly inside targetDir.
|
|
10
|
+
*
|
|
11
|
+
* No quota / count cap. Operators running with this fallback engaged for extended periods
|
|
12
|
+
* should monitor disk usage and replay leads back to DB once it returns.
|
|
13
|
+
*
|
|
14
|
+
* Module is import-side-effect-free. No env reads at module-load time.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from "fs/promises";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
|
|
20
|
+
import type { Lead } from "@working-theory/validation";
|
|
21
|
+
|
|
22
|
+
export type FallbackLead = Lead;
|
|
23
|
+
|
|
24
|
+
export type FallbackResult =
|
|
25
|
+
| { ok: true; path: string }
|
|
26
|
+
| {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: {
|
|
29
|
+
kind: "invalid_id" | "path_escape" | "fs_error";
|
|
30
|
+
message: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const LEAD_ID_RE = /^[a-z0-9_-]{8,64}$/i;
|
|
35
|
+
|
|
36
|
+
export async function writeLeadFallback(
|
|
37
|
+
lead: FallbackLead,
|
|
38
|
+
): Promise<FallbackResult> {
|
|
39
|
+
if (!LEAD_ID_RE.test(lead.id)) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: { kind: "invalid_id", message: `Invalid lead id: ${lead.id}` },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve target directory — read env inside the function (no module-level side effects)
|
|
47
|
+
const envDir = process.env.AILK_INBOX_FALLBACK_DIR ?? ".ailk/inbox";
|
|
48
|
+
const targetDir = path.resolve(process.cwd(), envDir);
|
|
49
|
+
|
|
50
|
+
const rawTs = isValidIso(lead.createdAt)
|
|
51
|
+
? lead.createdAt
|
|
52
|
+
: new Date().toISOString();
|
|
53
|
+
// Replace colons with hyphens for Windows filesystem compatibility
|
|
54
|
+
const ts = rawTs.replace(/:/g, "-");
|
|
55
|
+
|
|
56
|
+
const filename = `${ts}-${lead.id}.json`;
|
|
57
|
+
const resolved = path.resolve(targetDir, filename);
|
|
58
|
+
|
|
59
|
+
// Path-escape check: relative path must not start with '..' or be absolute
|
|
60
|
+
const rel = path.relative(targetDir, resolved);
|
|
61
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error: {
|
|
65
|
+
kind: "path_escape",
|
|
66
|
+
message: `Path escape detected: ${resolved}`,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
73
|
+
await fs.writeFile(resolved, JSON.stringify(lead, null, 2));
|
|
74
|
+
return { ok: true, path: resolved };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return { ok: false, error: { kind: "fs_error", message } };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Require YYYY-MM-DD prefix before accepting as ISO — rejects slash-dates, traversal strings, etc.
|
|
82
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/;
|
|
83
|
+
|
|
84
|
+
function isValidIso(value: string): boolean {
|
|
85
|
+
if (!value || !ISO_RE.test(value)) return false;
|
|
86
|
+
return !isNaN(new Date(value).getTime());
|
|
87
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lead capture store — always-on JSONL writer (Issue #27 Unit 1).
|
|
3
|
+
*
|
|
4
|
+
* Appends one JSON line per successful lead submission to apps/api/leads.jsonl
|
|
5
|
+
* (or LEAD_CAPTURE_FILE if set). Uses fs.appendFile with flag:'a' which maps to
|
|
6
|
+
* POSIX O_APPEND — the kernel guarantees atomicity for writes ≤ PIPE_BUF on
|
|
7
|
+
* local filesystems, making this safe for concurrent processes/requests.
|
|
8
|
+
*
|
|
9
|
+
* Semantic difference from B6 file-fallback:
|
|
10
|
+
* B6 file-fallback (.ailk/inbox/) — engaged only when the DB throws (salvage path)
|
|
11
|
+
* This module — always-on canonical AX surface; disabled explicitly via env, not by DB presence
|
|
12
|
+
*
|
|
13
|
+
* Env vars (all read inside function bodies — module is import-side-effect-free):
|
|
14
|
+
* LEAD_CAPTURE_FILE — output path (default: apps/api/leads.jsonl relative to cwd)
|
|
15
|
+
* LEAD_CAPTURE_FILE_DISABLED — set to "true" to skip writes (opt-out)
|
|
16
|
+
* LEAD_CAPTURE_INCLUDE_IP — set to "true" to include metadata.ip in the record
|
|
17
|
+
*
|
|
18
|
+
* PII discipline:
|
|
19
|
+
* IP is excluded from the written record by default.
|
|
20
|
+
* No other PII is filtered — `data` carries whatever the route gave us.
|
|
21
|
+
* Callers are responsible for what they put in `data`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from "fs/promises";
|
|
25
|
+
import * as path from "path";
|
|
26
|
+
|
|
27
|
+
import type { LeadCaptureRecord } from "@working-theory/validation";
|
|
28
|
+
|
|
29
|
+
// Re-export so consumers can import from one place.
|
|
30
|
+
export type { LeadCaptureRecord };
|
|
31
|
+
|
|
32
|
+
export type AppendResult =
|
|
33
|
+
| { ok: true; skipped?: true }
|
|
34
|
+
| { ok: false; error: string };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Append one lead record as a JSONL line.
|
|
38
|
+
*
|
|
39
|
+
* Always resolves — never throws. Returns ok:false on fs errors so the caller
|
|
40
|
+
* can log and continue without crashing the request.
|
|
41
|
+
*/
|
|
42
|
+
export async function appendLeadRecord(
|
|
43
|
+
record: LeadCaptureRecord,
|
|
44
|
+
): Promise<AppendResult> {
|
|
45
|
+
// Read env inside the function — no module-level side effects.
|
|
46
|
+
const disabled = process.env.LEAD_CAPTURE_FILE_DISABLED === "true";
|
|
47
|
+
if (disabled) {
|
|
48
|
+
return { ok: true, skipped: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const includeIp = process.env.LEAD_CAPTURE_INCLUDE_IP === "true";
|
|
52
|
+
const outputPath = resolveOutputPath();
|
|
53
|
+
|
|
54
|
+
// Strip IP from metadata unless explicitly opted in.
|
|
55
|
+
const cleanMeta = includeIp
|
|
56
|
+
? record.metadata
|
|
57
|
+
: Object.fromEntries(
|
|
58
|
+
Object.entries(record.metadata).filter(([k]) => k !== "ip"),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const line = JSON.stringify({ ...record, metadata: cleanMeta }) + "\n";
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await fs.appendFile(outputPath, line, { flag: "a", encoding: "utf8" });
|
|
65
|
+
return { ok: true };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
+
return { ok: false, error: message };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Startup-time validation: if LEAD_CAPTURE_FILE points at a path whose parent
|
|
74
|
+
* directory does not exist, throw immediately so the server fails fast rather
|
|
75
|
+
* than erroring on first write.
|
|
76
|
+
*
|
|
77
|
+
* Call from server.ts build() before registering routes.
|
|
78
|
+
* No-op when LEAD_CAPTURE_FILE is unset or LEAD_CAPTURE_FILE_DISABLED=true.
|
|
79
|
+
*/
|
|
80
|
+
export async function validateLeadCaptureDir(): Promise<void> {
|
|
81
|
+
const disabled = process.env.LEAD_CAPTURE_FILE_DISABLED === "true";
|
|
82
|
+
const filePath = process.env.LEAD_CAPTURE_FILE;
|
|
83
|
+
|
|
84
|
+
if (!filePath || disabled) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const dir = path.dirname(path.resolve(filePath));
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await fs.access(dir);
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`LEAD_CAPTURE_FILE directory does not exist: "${dir}". ` +
|
|
95
|
+
`Create the directory or unset LEAD_CAPTURE_FILE.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Internal ─────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function resolveOutputPath(): string {
|
|
103
|
+
const envPath = process.env.LEAD_CAPTURE_FILE;
|
|
104
|
+
if (envPath) {
|
|
105
|
+
return path.resolve(envPath);
|
|
106
|
+
}
|
|
107
|
+
// Default: apps/api/leads.jsonl relative to the process cwd.
|
|
108
|
+
// In production (Vercel function or Node standalone), cwd is the project root.
|
|
109
|
+
return path.resolve(process.cwd(), "apps/api/leads.jsonl");
|
|
110
|
+
}
|