create-ailk 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -14
- package/component-catalog.json +735 -95
- package/component-catalog.md +13 -7
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +89 -29
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +56 -1
- package/dist/lib/extract-module-bundle.js +170 -14
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +34 -3
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +85 -10
- package/dist/parse-args.js +227 -15
- package/dist/programmatic.d.ts +81 -5
- package/dist/programmatic.js +291 -32
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +499 -0
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +204 -24
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +94 -19
- package/dist/template-manifest.js +170 -289
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +65 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +67 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +17 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +167 -0
- package/site-templates/marketing-site/DESIGN.md +59 -40
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +376 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +230 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +59 -40
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +323 -63
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +323 -113
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/DESIGN.md +54 -40
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +173 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/saas-product/DESIGN.md +59 -40
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/local-business-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +10 -9
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +115 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +73 -41
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +21 -30
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +204 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +8 -2
- package/templates/DESIGN.md +56 -44
- package/templates/README.md +116 -46
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +146 -10
- package/templates/apps/api/src/config/index.ts +46 -14
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +13 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +67 -29
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +94 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +228 -86
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +115 -7
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +146 -52
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +142 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{cancellation → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +82 -18
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +1 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +148 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +78 -37
- 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 +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/content/_site.mdx +25 -0
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +126 -0
- package/templates/content/en/ContactPage/contact.mdx +21 -92
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +268 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +181 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/site-templates/simple-service/DESIGN.md +0 -148
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +0 -240
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -278
- package/site-templates/simple-service/ailk-template.json +0 -10
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -67
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/provision-config/references/runbook-template.md +0 -248
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -150
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/shipping/page.tsx +0 -44
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
|
@@ -5,10 +5,11 @@ jest.mock('dotenv', () => ({
|
|
|
5
5
|
|
|
6
6
|
// Mock the observability logger so assertions don't depend on pino's stdout serialization.
|
|
7
7
|
const mockLoggerError = jest.fn();
|
|
8
|
+
const mockLoggerWarn = jest.fn();
|
|
8
9
|
jest.mock('@working-theory/observability', () => ({
|
|
9
10
|
logger: {
|
|
10
11
|
error: (...args: unknown[]) => mockLoggerError(...args),
|
|
11
|
-
warn:
|
|
12
|
+
warn: (...args: unknown[]) => mockLoggerWarn(...args),
|
|
12
13
|
info: jest.fn(),
|
|
13
14
|
debug: jest.fn(),
|
|
14
15
|
},
|
|
@@ -41,22 +42,74 @@ describe('Config', () => {
|
|
|
41
42
|
expect(dotenv.config).toHaveBeenCalled();
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// #6730 security review (M1): a production build whose enabled modules keep
|
|
46
|
+
// their state in the database still refuses to boot without DATABASE_URL —
|
|
47
|
+
// a missing database there is a broken deploy (e.g. a commerce licence read
|
|
48
|
+
// as absent would ack-and-drop signed Stripe events), not a Free site.
|
|
49
|
+
it.each(['commerce', 'content-write', 'multi-tenant', 'scheduling', 'signal-collectors', 'workspace'])(
|
|
50
|
+
'should exit on missing DATABASE_URL in production when the %s module is enabled',
|
|
51
|
+
(moduleName) => {
|
|
52
|
+
process.env.NODE_ENV = 'production';
|
|
53
|
+
process.env.AILK_ENABLED_MODULES = `marketing,${moduleName}`;
|
|
54
|
+
delete process.env.DATABASE_URL;
|
|
55
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
56
|
+
throw new Error('Process exit called');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(() => require('../index')).toThrow('Process exit called');
|
|
60
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
61
|
+
expect(mockLoggerError).toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
exitSpy.mockRestore();
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
it('should start database-less in production with only non-database modules enabled', () => {
|
|
68
|
+
process.env.NODE_ENV = 'production';
|
|
69
|
+
process.env.AILK_ENABLED_MODULES = 'marketing,docs-blog';
|
|
46
70
|
delete process.env.DATABASE_URL;
|
|
71
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
72
|
+
throw new Error('Process exit called');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const { config } = require('../index');
|
|
76
|
+
|
|
77
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
78
|
+
expect(config.DATABASE_URL).toBeUndefined();
|
|
79
|
+
exitSpy.mockRestore();
|
|
80
|
+
});
|
|
47
81
|
|
|
82
|
+
// #6482 — a missing DATABASE_URL starts the API database-less (one
|
|
83
|
+
// warning) instead of exiting, so a fresh scaffold's `pnpm dev` keeps its
|
|
84
|
+
// "no database needed" promise. #6730 (operator decision (a)) extends that
|
|
85
|
+
// to production: a Free site delivers leads by email with no Postgres.
|
|
86
|
+
it.each([
|
|
87
|
+
['production', 'production'],
|
|
88
|
+
['development', 'development'],
|
|
89
|
+
['unset', undefined],
|
|
90
|
+
['test', 'test'],
|
|
91
|
+
])('should start database-less with DATABASE_URL unset when NODE_ENV is %s', (_label, nodeEnv) => {
|
|
92
|
+
// ARRANGE
|
|
93
|
+
if (nodeEnv === undefined) {
|
|
94
|
+
delete process.env.NODE_ENV;
|
|
95
|
+
} else {
|
|
96
|
+
process.env.NODE_ENV = nodeEnv;
|
|
97
|
+
}
|
|
98
|
+
delete process.env.DATABASE_URL;
|
|
48
99
|
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
49
100
|
throw new Error('Process exit called');
|
|
50
101
|
});
|
|
51
|
-
mockLoggerError.mockClear();
|
|
52
102
|
|
|
53
|
-
// ACT
|
|
54
|
-
|
|
55
|
-
require('../index');
|
|
56
|
-
}).toThrow('Process exit called');
|
|
103
|
+
// ACT
|
|
104
|
+
const { config } = require('../index');
|
|
57
105
|
|
|
58
|
-
|
|
59
|
-
expect(
|
|
106
|
+
// ASSERT
|
|
107
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
108
|
+
expect(config.DATABASE_URL).toBeUndefined();
|
|
109
|
+
expect(config.NODE_ENV).toBe(nodeEnv ?? 'development');
|
|
110
|
+
expect(mockLoggerError).not.toHaveBeenCalled();
|
|
111
|
+
expect(mockLoggerWarn).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(mockLoggerWarn.mock.calls[0]?.[1]).toContain('database_not_configured');
|
|
60
113
|
|
|
61
114
|
exitSpy.mockRestore();
|
|
62
115
|
});
|
|
@@ -95,3 +148,86 @@ describe('Config', () => {
|
|
|
95
148
|
expect(config.FRONTEND_URL).toBe('http://localhost:3000');
|
|
96
149
|
});
|
|
97
150
|
});
|
|
151
|
+
|
|
152
|
+
// #6730 (operator decision (a)) — the REAL server boots in production with
|
|
153
|
+
// no database: an email-only Free site must be able to run its API, answer
|
|
154
|
+
// `GET /v1/leads/capture`, and refuse authenticated routes with a 503 rather
|
|
155
|
+
// than exit at startup.
|
|
156
|
+
describe('production boot without a database (#6730)', () => {
|
|
157
|
+
const originalEnv = process.env;
|
|
158
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the Fastify instance comes from an isolated require; its generics add nothing here
|
|
159
|
+
let app: any;
|
|
160
|
+
|
|
161
|
+
const EMAIL_ENV = {
|
|
162
|
+
RESEND_API_KEY: 're_test',
|
|
163
|
+
AILK_LEAD_NOTIFICATION_TO: 'owner@example.com',
|
|
164
|
+
AILK_LEAD_NOTIFICATION_FROM: 'noreply@example.com',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// External boundaries only, as src/__tests__/server.test.ts mocks them.
|
|
168
|
+
beforeAll(() => {
|
|
169
|
+
jest.doMock('@fastify/cors', () => jest.fn().mockResolvedValue(undefined));
|
|
170
|
+
jest.doMock('@fastify/rate-limit', () => jest.fn().mockResolvedValue(undefined));
|
|
171
|
+
jest.doMock('@working-theory/email', () => ({
|
|
172
|
+
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: 'm' }),
|
|
173
|
+
sendRequestConfirmEmail: jest.fn(),
|
|
174
|
+
sendRequestPaidLinkEmail: jest.fn(),
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
afterEach(async () => {
|
|
179
|
+
if (app) await app.close();
|
|
180
|
+
app = undefined;
|
|
181
|
+
process.env = originalEnv;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
async function bootProduction(extra: Record<string, string> = {}) {
|
|
185
|
+
jest.resetModules();
|
|
186
|
+
const env: NodeJS.ProcessEnv = { ...originalEnv, NODE_ENV: 'production', ...extra };
|
|
187
|
+
delete env.DATABASE_URL;
|
|
188
|
+
delete env.DATABASE_URL_NON_POOLED;
|
|
189
|
+
delete env.RESEND_API_KEY;
|
|
190
|
+
delete env.AILK_LEAD_NOTIFICATION_TO;
|
|
191
|
+
delete env.AILK_LEAD_NOTIFICATION_FROM;
|
|
192
|
+
Object.assign(env, extra);
|
|
193
|
+
process.env = env;
|
|
194
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
195
|
+
throw new Error('Process exit called');
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
const { build } = require('../../server');
|
|
199
|
+
app = await build();
|
|
200
|
+
} finally {
|
|
201
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
202
|
+
exitSpy.mockRestore();
|
|
203
|
+
}
|
|
204
|
+
return app;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
it('with only the email vars, starts and reports contact accepted, newsletter not', async () => {
|
|
208
|
+
const server = await bootProduction(EMAIL_ENV);
|
|
209
|
+
|
|
210
|
+
const res = await server.inject({ method: 'GET', url: '/v1/leads/capture' });
|
|
211
|
+
|
|
212
|
+
expect(res.statusCode).toBe(200);
|
|
213
|
+
expect(res.json()).toEqual({ ok: true, data: { contact: true, newsletter: false } });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('with nothing configured, starts and reports nothing accepted', async () => {
|
|
217
|
+
const server = await bootProduction();
|
|
218
|
+
|
|
219
|
+
const res = await server.inject({ method: 'GET', url: '/v1/leads/capture' });
|
|
220
|
+
|
|
221
|
+
expect(res.statusCode).toBe(200);
|
|
222
|
+
expect(res.json()).toEqual({ ok: true, data: { contact: false, newsletter: false } });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('refuses an authenticated route with 503 database_not_configured', async () => {
|
|
226
|
+
const server = await bootProduction(EMAIL_ENV);
|
|
227
|
+
|
|
228
|
+
const res = await server.inject({ method: 'GET', url: '/v1/leads' });
|
|
229
|
+
|
|
230
|
+
expect(res.statusCode).toBe(503);
|
|
231
|
+
expect(res.json()).toEqual({ error: 'database_not_configured' });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { logger } from '@working-theory/observability';
|
|
8
8
|
import * as dotenv from 'dotenv';
|
|
9
9
|
|
|
10
|
+
import { isModuleEnabled } from './modules.js';
|
|
11
|
+
|
|
10
12
|
dotenv.config();
|
|
11
13
|
|
|
12
14
|
export interface Config {
|
|
@@ -15,8 +17,9 @@ export interface Config {
|
|
|
15
17
|
PORT: number;
|
|
16
18
|
FRONTEND_URL: string;
|
|
17
19
|
|
|
18
|
-
// Local Database (for auth: User, Session, ApiToken, etc.)
|
|
19
|
-
|
|
20
|
+
// Local Database (for auth: User, Session, ApiToken, etc.). Required in
|
|
21
|
+
// production; unset elsewhere starts the API database-less (#6482).
|
|
22
|
+
DATABASE_URL?: string;
|
|
20
23
|
|
|
21
24
|
// Stripe — consumed by apps/api/src/lib/stripe.ts via @working-theory/stripe-client wrapper
|
|
22
25
|
STRIPE_SECRET_KEY?: string;
|
|
@@ -91,26 +94,55 @@ export interface Config {
|
|
|
91
94
|
CAL_WEBHOOK_SECRET?: string;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
/** Enabled modules whose state lives in the database (see validateConfig). */
|
|
98
|
+
const DATABASE_MODULES = [
|
|
99
|
+
'commerce',
|
|
100
|
+
'content-write',
|
|
101
|
+
'multi-tenant',
|
|
102
|
+
'scheduling',
|
|
103
|
+
'signal-collectors',
|
|
104
|
+
'workspace',
|
|
105
|
+
] as const;
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
function validateConfig(): Config {
|
|
108
|
+
const nodeEnv = (process.env.NODE_ENV || 'development') as Config['NODE_ENV'];
|
|
109
|
+
|
|
110
|
+
// No database is a supported shape in every environment, production
|
|
111
|
+
// included (#6730 operator decision (a)): a Free site delivers leads by
|
|
112
|
+
// email and needs no Postgres. Start database-less and let the auth
|
|
113
|
+
// preHandler answer 503 database_not_configured (#6482) — it reads
|
|
114
|
+
// DATABASE_URL per request, whatever NODE_ENV says, so no authenticated
|
|
115
|
+
// route runs without a database.
|
|
116
|
+
//
|
|
117
|
+
// Except a production build whose ENABLED modules keep their state in the
|
|
118
|
+
// database. There a missing DATABASE_URL is a broken deploy, not a Free
|
|
119
|
+
// site: e.g. a commerce site's licence lives in the database-backed store,
|
|
120
|
+
// so booting without it would read the site as unlicensed and ack-and-drop
|
|
121
|
+
// every signed Stripe event (#6730 security review, M1). Those builds keep
|
|
122
|
+
// the loud startup failure.
|
|
98
123
|
if (!process.env.DATABASE_URL) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
const needsDatabase = DATABASE_MODULES.filter((m) => isModuleEnabled(m));
|
|
125
|
+
if (nodeEnv === 'production' && needsDatabase.length > 0) {
|
|
126
|
+
logger.error(
|
|
127
|
+
{ modules: needsDatabase },
|
|
128
|
+
'❌ DATABASE_URL is required in production when these modules are enabled',
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
logger.warn(
|
|
133
|
+
{ nodeEnv },
|
|
134
|
+
'⚠️ DATABASE_URL not set — starting database-less; authenticated routes answer 503 database_not_configured',
|
|
135
|
+
);
|
|
106
136
|
}
|
|
107
137
|
|
|
108
138
|
return {
|
|
109
|
-
NODE_ENV:
|
|
139
|
+
NODE_ENV: nodeEnv,
|
|
110
140
|
PORT: parseInt(process.env.PORT || '3002', 10),
|
|
111
141
|
FRONTEND_URL: process.env.FRONTEND_URL || 'http://localhost:3000',
|
|
112
142
|
|
|
113
|
-
|
|
143
|
+
...(process.env.DATABASE_URL !== undefined && {
|
|
144
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
145
|
+
}),
|
|
114
146
|
|
|
115
147
|
...(process.env.STRIPE_SECRET_KEY !== undefined && {
|
|
116
148
|
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
|
@@ -96,6 +96,36 @@ describe("appendLeadRecord", () => {
|
|
|
96
96
|
expect(filePath).toMatch(/leads\.jsonl$/);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
// #6730 D3 — the default file is local-only: in production it does not
|
|
100
|
+
// outlive a serverless instance, so it is skipped unless explicitly pointed
|
|
101
|
+
// at a durable path.
|
|
102
|
+
describe("in production (#6730 D3)", () => {
|
|
103
|
+
const savedNodeEnv = process.env.NODE_ENV;
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
process.env.NODE_ENV = savedNodeEnv;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("skips the write when LEAD_CAPTURE_FILE is unset", async () => {
|
|
109
|
+
process.env.NODE_ENV = "production";
|
|
110
|
+
|
|
111
|
+
const result = await appendLeadRecord(record);
|
|
112
|
+
|
|
113
|
+
expect(result).toEqual({ ok: true, skipped: true });
|
|
114
|
+
expect(mockAppendFile).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("writes when LEAD_CAPTURE_FILE is set explicitly", async () => {
|
|
118
|
+
process.env.NODE_ENV = "production";
|
|
119
|
+
process.env.LEAD_CAPTURE_FILE = "/data/leads.jsonl";
|
|
120
|
+
|
|
121
|
+
const result = await appendLeadRecord(record);
|
|
122
|
+
|
|
123
|
+
expect(result).toEqual({ ok: true });
|
|
124
|
+
expect(mockAppendFile).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(mockAppendFile.mock.calls[0][0]).toBe("/data/leads.jsonl");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
99
129
|
// AC4 — LEAD_CAPTURE_FILE_DISABLED=true skips the write
|
|
100
130
|
it("short-circuits write when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
|
|
101
131
|
process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lead-delivery.test.ts
|
|
3
|
+
* @description The #6730 D1 delivery table, row by row.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { acceptsKind, resolveLeadDelivery } from "../lead-delivery";
|
|
7
|
+
|
|
8
|
+
const EMAIL = {
|
|
9
|
+
RESEND_API_KEY: "re_test",
|
|
10
|
+
AILK_LEAD_NOTIFICATION_TO: "owner@example.com",
|
|
11
|
+
AILK_LEAD_NOTIFICATION_FROM: "noreply@example.com",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("resolveLeadDelivery", () => {
|
|
15
|
+
it("DATABASE_URL alone → inbox", () => {
|
|
16
|
+
expect(resolveLeadDelivery({ DATABASE_URL: "postgres://x", NODE_ENV: "production" })).toBe(
|
|
17
|
+
"inbox",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("DATABASE_URL wins over the email vars", () => {
|
|
22
|
+
expect(resolveLeadDelivery({ DATABASE_URL: "postgres://x", ...EMAIL })).toBe("inbox");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("the three email vars, no database → email", () => {
|
|
26
|
+
expect(resolveLeadDelivery({ ...EMAIL, NODE_ENV: "production" })).toBe("email");
|
|
27
|
+
expect(resolveLeadDelivery({ ...EMAIL, NODE_ENV: "development" })).toBe("email");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("a bare RESEND_API_KEY is not email delivery — local in dev, none in production", () => {
|
|
31
|
+
expect(resolveLeadDelivery({ RESEND_API_KEY: "re_test", NODE_ENV: "development" })).toBe(
|
|
32
|
+
"local",
|
|
33
|
+
);
|
|
34
|
+
expect(resolveLeadDelivery({ RESEND_API_KEY: "re_test", NODE_ENV: "production" })).toBe(
|
|
35
|
+
"none",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("the key and one address only → not email", () => {
|
|
40
|
+
expect(
|
|
41
|
+
resolveLeadDelivery({
|
|
42
|
+
RESEND_API_KEY: "re_test",
|
|
43
|
+
AILK_LEAD_NOTIFICATION_TO: "owner@example.com",
|
|
44
|
+
NODE_ENV: "production",
|
|
45
|
+
}),
|
|
46
|
+
).toBe("none");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("nothing configured in production → none", () => {
|
|
50
|
+
expect(resolveLeadDelivery({ NODE_ENV: "production" })).toBe("none");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("nothing configured outside production → local", () => {
|
|
54
|
+
expect(resolveLeadDelivery({ NODE_ENV: "test" })).toBe("local");
|
|
55
|
+
expect(resolveLeadDelivery({})).toBe("local");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("LEAD_CAPTURE_FILE_DISABLED=true removes the local path → none", () => {
|
|
59
|
+
expect(
|
|
60
|
+
resolveLeadDelivery({ NODE_ENV: "development", LEAD_CAPTURE_FILE_DISABLED: "true" }),
|
|
61
|
+
).toBe("none");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("reads process.env by default", () => {
|
|
65
|
+
const saved = process.env.DATABASE_URL;
|
|
66
|
+
process.env.DATABASE_URL = "postgres://x";
|
|
67
|
+
try {
|
|
68
|
+
expect(resolveLeadDelivery()).toBe("inbox");
|
|
69
|
+
} finally {
|
|
70
|
+
if (saved === undefined) delete process.env.DATABASE_URL;
|
|
71
|
+
else process.env.DATABASE_URL = saved;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("acceptsKind", () => {
|
|
77
|
+
it("none accepts nothing", () => {
|
|
78
|
+
expect(acceptsKind("none", "submit")).toBe(false);
|
|
79
|
+
expect(acceptsKind("none", "subscribe")).toBe(false);
|
|
80
|
+
expect(acceptsKind("none", "callback")).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("email accepts submit and callback, never subscribe", () => {
|
|
84
|
+
expect(acceptsKind("email", "submit")).toBe(true);
|
|
85
|
+
expect(acceptsKind("email", "callback")).toBe(true);
|
|
86
|
+
expect(acceptsKind("email", "subscribe")).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("inbox and local accept every kind", () => {
|
|
90
|
+
for (const d of ["inbox", "local"] as const) {
|
|
91
|
+
expect(acceptsKind(d, "submit")).toBe(true);
|
|
92
|
+
expect(acceptsKind(d, "subscribe")).toBe(true);
|
|
93
|
+
expect(acceptsKind(d, "callback")).toBe(true);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Env vars (all read inside function bodies — module is import-side-effect-free):
|
|
14
14
|
* LEAD_CAPTURE_FILE — output path (default: apps/api/leads.jsonl relative to cwd)
|
|
15
|
+
*
|
|
16
|
+
* The default path is LOCAL-ONLY (#6730 D3). In production
|
|
17
|
+
* (`NODE_ENV=production`) with `LEAD_CAPTURE_FILE` unset, the write is skipped:
|
|
18
|
+
* on serverless hosting a file under the process cwd does not outlive the
|
|
19
|
+
* instance, so it is not a record of anything. An operator with a durable
|
|
20
|
+
* volume can keep a side copy by setting `LEAD_CAPTURE_FILE` explicitly —
|
|
21
|
+
* a copy only: in production it never makes a lead count as delivered
|
|
22
|
+
* (`lib/lead-delivery.ts` needs a database or the email vars).
|
|
23
|
+
*
|
|
15
24
|
* LEAD_CAPTURE_FILE_DISABLED — set to "true" to skip writes (opt-out)
|
|
16
25
|
* LEAD_CAPTURE_INCLUDE_IP — set to "true" to include metadata.ip in the record
|
|
17
26
|
*
|
|
@@ -47,6 +56,9 @@ export async function appendLeadRecord(
|
|
|
47
56
|
if (disabled) {
|
|
48
57
|
return { ok: true, skipped: true };
|
|
49
58
|
}
|
|
59
|
+
if (process.env.NODE_ENV === "production" && !process.env.LEAD_CAPTURE_FILE) {
|
|
60
|
+
return { ok: true, skipped: true };
|
|
61
|
+
}
|
|
50
62
|
|
|
51
63
|
const includeIp = process.env.LEAD_CAPTURE_INCLUDE_IP === "true";
|
|
52
64
|
const outputPath = resolveOutputPath();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lead-delivery.ts
|
|
3
|
+
* @description Where a captured lead goes, resolved from the environment (#6730 D1).
|
|
4
|
+
*
|
|
5
|
+
* inbox — `DATABASE_URL` is set: the database is the destination (the lead
|
|
6
|
+
* inbox at `/leads`); the owner email is still sent best-effort.
|
|
7
|
+
* email — no database, but `RESEND_API_KEY` plus both notification
|
|
8
|
+
* addresses are set: the owner email IS the delivery.
|
|
9
|
+
* local — neither, outside production (`pnpm dev`, tests): the local file
|
|
10
|
+
* fallback + `leads.jsonl`, so local work needs nothing configured.
|
|
11
|
+
* none — neither, in production: nothing is accepted, so no visitor is
|
|
12
|
+
* ever told a message was sent when it reached nobody.
|
|
13
|
+
*
|
|
14
|
+
* A key alone delivers nothing (`sendEmail()` also needs both addresses), so
|
|
15
|
+
* it is not counted as configured. `LEAD_CAPTURE_FILE_DISABLED=true` removes
|
|
16
|
+
* the local file path, so outside production it resolves to `none` too.
|
|
17
|
+
*
|
|
18
|
+
* Import-side-effect-free; the environment is read at call time.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type LeadDelivery = "inbox" | "email" | "local" | "none";
|
|
22
|
+
|
|
23
|
+
export type LeadDeliveryKind = "submit" | "subscribe" | "callback";
|
|
24
|
+
|
|
25
|
+
export function resolveLeadDelivery(
|
|
26
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
27
|
+
): LeadDelivery {
|
|
28
|
+
if (env.DATABASE_URL) return "inbox";
|
|
29
|
+
if (
|
|
30
|
+
env.RESEND_API_KEY &&
|
|
31
|
+
env.AILK_LEAD_NOTIFICATION_TO &&
|
|
32
|
+
env.AILK_LEAD_NOTIFICATION_FROM
|
|
33
|
+
) {
|
|
34
|
+
return "email";
|
|
35
|
+
}
|
|
36
|
+
if (env.NODE_ENV !== "production" && env.LEAD_CAPTURE_FILE_DISABLED !== "true") {
|
|
37
|
+
return "local";
|
|
38
|
+
}
|
|
39
|
+
return "none";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a delivery mode accepts a lead of this kind. A newsletter
|
|
44
|
+
* subscription is a list entry, not a message — emailing the owner one
|
|
45
|
+
* address per signup is not a list — so `subscribe` needs somewhere to keep
|
|
46
|
+
* it (the database, or the local file in development).
|
|
47
|
+
*/
|
|
48
|
+
export function acceptsKind(d: LeadDelivery, kind: LeadDeliveryKind): boolean {
|
|
49
|
+
if (d === "none") return false;
|
|
50
|
+
if (kind === "subscribe") return d === "inbox" || d === "local";
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the durable home of a renewed licence key (#6677 DD-6).
|
|
3
|
+
*
|
|
4
|
+
* `@working-theory/license` renews the deployment's key before it lapses and
|
|
5
|
+
* keeps the renewed key in a `LicenseKeyStore`. Its default store is a file,
|
|
6
|
+
* which a Vercel redeploy wipes — so a redeployed site would fall back to the
|
|
7
|
+
* older `AILK_LICENSE_KEY` and, past the renewal grace, to free. With a
|
|
8
|
+
* database configured, this registers the store backed by ONE deployment-wide
|
|
9
|
+
* `SiteSetting` row, `(DEFAULT_WORKSPACE_ID, "license.renewed_key")`, shared
|
|
10
|
+
* by this app, apps/mcp and apps/web.
|
|
11
|
+
*
|
|
12
|
+
* The row is deployment-wide, not tenant data: the licence is the
|
|
13
|
+
* deployment's, so it sits under the seeded default workspace in single- and
|
|
14
|
+
* multi-tenant builds alike (the named tenant-scope exclusion in
|
|
15
|
+
* architecture.yaml). A DB-less scaffold keeps the file store.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { DEFAULT_WORKSPACE_ID } from "@working-theory/database/tenancy";
|
|
19
|
+
import {
|
|
20
|
+
createSiteSettingLicenseKeyStore,
|
|
21
|
+
setLicenseKeyStore,
|
|
22
|
+
} from "@working-theory/license";
|
|
23
|
+
import { logger } from "@working-theory/observability";
|
|
24
|
+
|
|
25
|
+
import { prisma } from "./prisma.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register the SiteSetting-backed store when `DATABASE_URL` is set. An
|
|
29
|
+
* explicitly set `AILK_LICENSE_STORAGE_PATH` is the operator choosing the
|
|
30
|
+
* file store, and is honoured. Never throws: a failure logs and leaves the
|
|
31
|
+
* file store in place.
|
|
32
|
+
*/
|
|
33
|
+
export async function registerLicenseKeyStore(): Promise<void> {
|
|
34
|
+
if (!process.env.DATABASE_URL || process.env.AILK_LICENSE_STORAGE_PATH) return;
|
|
35
|
+
try {
|
|
36
|
+
setLicenseKeyStore(
|
|
37
|
+
createSiteSettingLicenseKeyStore(prisma.siteSetting, DEFAULT_WORKSPACE_ID),
|
|
38
|
+
);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logger.warn(
|
|
41
|
+
{ errName: err instanceof Error ? err.name : "UnknownError" },
|
|
42
|
+
"[license] could not register the SiteSetting licence-key store — the file store stays in place",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -144,9 +144,11 @@ export async function resolveWorkspaceIdForAlias(
|
|
|
144
144
|
* 1. **No `DATABASE_URL` configured** — the literal `requireDb()` guard
|
|
145
145
|
* `apps/api/src/services/flow-engine.ts` uses for the flow services:
|
|
146
146
|
* presence, not reachability. (`apps/api/src/config/index.ts`'s own
|
|
147
|
-
* eager `validateConfig()`
|
|
148
|
-
* `DATABASE_URL` unset before `build()` is ever reached
|
|
149
|
-
*
|
|
147
|
+
* eager `validateConfig()` refuses a **production** `apps/api` boot with
|
|
148
|
+
* `DATABASE_URL` unset before `build()` is ever reached; outside
|
|
149
|
+
* production a real boot starts database-less (#6482), so this arm is
|
|
150
|
+
* reachable from a real dev boot as well as from any other caller of
|
|
151
|
+
* `build()`.)
|
|
150
152
|
* 2. **`prisma.workspace.findUnique` is not even a function** — the case
|
|
151
153
|
* that surfaced this arm: `apps/api`'s shared/global prisma mocks
|
|
152
154
|
* (`lib/__mocks__/prisma-client.js`, the module-map-level double every
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file auth.database-not-configured.test.ts
|
|
3
|
+
*
|
|
4
|
+
* DATABASE_URL unset → 503 database_not_configured (#6482).
|
|
5
|
+
*
|
|
6
|
+
* Outside production apps/api boots database-less. The preHandler must say
|
|
7
|
+
* so before it reads any credential: placed after the lookups, the existing
|
|
8
|
+
* `.catch(() => undefined)` would turn the connection failure into a
|
|
9
|
+
* misleading 401.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Fastify from "fastify";
|
|
13
|
+
import type { FastifyInstance } from "fastify";
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
|
|
16
|
+
jest.mock("../../lib/prisma.js", () => ({
|
|
17
|
+
prisma: {
|
|
18
|
+
session: { findUnique: jest.fn() },
|
|
19
|
+
apiToken: {
|
|
20
|
+
findUnique: jest.fn(),
|
|
21
|
+
update: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { prisma } from "../../lib/prisma.js";
|
|
27
|
+
import { authPreHandlerPlugin } from "../auth.js";
|
|
28
|
+
|
|
29
|
+
const echoHandler = jest.fn(async () => ({ ok: true }));
|
|
30
|
+
|
|
31
|
+
function buildApp(): FastifyInstance {
|
|
32
|
+
const app = Fastify();
|
|
33
|
+
app.register(authPreHandlerPlugin, {
|
|
34
|
+
frontendOrigin: "https://app.example.com",
|
|
35
|
+
});
|
|
36
|
+
app.get("/echo", echoHandler);
|
|
37
|
+
app.post("/echo", echoHandler);
|
|
38
|
+
return app;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("auth preHandler — DATABASE_URL not configured", () => {
|
|
42
|
+
const originalDatabaseUrl = process.env.DATABASE_URL;
|
|
43
|
+
let app: FastifyInstance;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
delete process.env.DATABASE_URL;
|
|
48
|
+
app = buildApp();
|
|
49
|
+
await app.ready();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await app.close();
|
|
54
|
+
if (originalDatabaseUrl === undefined) {
|
|
55
|
+
delete process.env.DATABASE_URL;
|
|
56
|
+
} else {
|
|
57
|
+
process.env.DATABASE_URL = originalDatabaseUrl;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each([
|
|
62
|
+
["no credentials", "GET", {}],
|
|
63
|
+
["a bearer token", "GET", { authorization: "Bearer ailk_abc123" }],
|
|
64
|
+
["a session cookie", "GET", { cookie: "better-auth.session_token=tok" }],
|
|
65
|
+
[
|
|
66
|
+
"a non-GET with a session cookie",
|
|
67
|
+
"POST",
|
|
68
|
+
{
|
|
69
|
+
cookie: "better-auth.session_token=tok",
|
|
70
|
+
origin: "https://app.example.com",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
] as const)(
|
|
74
|
+
'returns 503 { error: "database_not_configured" } for %s without any lookup',
|
|
75
|
+
async (_label, method, headers) => {
|
|
76
|
+
const res = await app.inject({ method, url: "/echo", headers });
|
|
77
|
+
|
|
78
|
+
expect(res.statusCode).toBe(503);
|
|
79
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
80
|
+
error: "database_not_configured",
|
|
81
|
+
});
|
|
82
|
+
expect(prisma.session.findUnique).not.toHaveBeenCalled();
|
|
83
|
+
expect(prisma.apiToken.findUnique).not.toHaveBeenCalled();
|
|
84
|
+
expect(echoHandler).not.toHaveBeenCalled();
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
it("control: with DATABASE_URL set and no credentials, answers 401 unauthorized", async () => {
|
|
89
|
+
process.env.DATABASE_URL = "postgresql://test:test@localhost/test";
|
|
90
|
+
|
|
91
|
+
const res = await app.inject({ method: "GET", url: "/echo" });
|
|
92
|
+
|
|
93
|
+
expect(res.statusCode).toBe(401);
|
|
94
|
+
expect(JSON.parse(res.body)).toEqual({ error: "unauthorized" });
|
|
95
|
+
expect(echoHandler).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - Both modes invalid → 401 unauthorized.
|
|
23
23
|
* - Cookie expired + bearer valid → 401 (no silent fallback).
|
|
24
24
|
* - Cookie absent, bearer malformed → 401 (no silent fallback).
|
|
25
|
+
* - DATABASE_URL unset → 503 database_not_configured (#6482).
|
|
25
26
|
*
|
|
26
27
|
* Public routes (health, content, leads, webhooks, openapi) are NOT registered
|
|
27
28
|
* with this preHandler — server.ts narrows scope via a sub-plugin.
|
|
@@ -48,6 +49,7 @@ const COOKIE_NAMES = [
|
|
|
48
49
|
] as const;
|
|
49
50
|
|
|
50
51
|
const UNAUTHORIZED = { error: "unauthorized" } as const;
|
|
52
|
+
const DATABASE_NOT_CONFIGURED = { error: "database_not_configured" } as const;
|
|
51
53
|
|
|
52
54
|
declare module "fastify" {
|
|
53
55
|
interface FastifyRequest {
|
|
@@ -201,6 +203,13 @@ export function makeAuthPreHandler(opts: AuthPreHandlerOptions = {}) {
|
|
|
201
203
|
request: FastifyRequest,
|
|
202
204
|
reply: FastifyReply,
|
|
203
205
|
): Promise<void> {
|
|
206
|
+
// A database-less dev boot (#6482). First, and read per request: after
|
|
207
|
+
// the lookups below, their `.catch` would already have answered 401.
|
|
208
|
+
if (!process.env.DATABASE_URL) {
|
|
209
|
+
reply.code(503).send(DATABASE_NOT_CONFIGURED);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
204
213
|
const now = getClock().now();
|
|
205
214
|
const cookieToken = readSessionCookie(request);
|
|
206
215
|
|