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,6 +5,16 @@
|
|
|
5
5
|
* POST /v1/leads/submit — lead submission (contact form / inquiry)
|
|
6
6
|
* POST /v1/leads/subscribe — newsletter subscription
|
|
7
7
|
* POST /v1/leads/callback — callback request (phone-back)
|
|
8
|
+
* GET /v1/leads/capture — whether this deployment accepts a contact
|
|
9
|
+
* message / a newsletter signup (#6730 D4)
|
|
10
|
+
*
|
|
11
|
+
* API↔MCP PARITY — `GET /v1/leads/capture` has NO MCP twin, a recorded
|
|
12
|
+
* exception on the same footing as the anonymous form-lifecycle routes. It
|
|
13
|
+
* exists so a web page can choose between the form and the contact email
|
|
14
|
+
* before rendering; an agent never needs that preflight, because the three
|
|
15
|
+
* write tools (`capture_lead`, `subscribe_newsletter`, `request_callback`)
|
|
16
|
+
* already return a typed `api_error` carrying the 503 when capture is not
|
|
17
|
+
* accepted — the same answer, at the moment it matters.
|
|
8
18
|
*
|
|
9
19
|
* TENANT RESOLUTION (#6203, spec §10.4 operator ruling — reverses this
|
|
10
20
|
* file's former byte-unchanged posture). `siteId` used to be taken VERBATIM
|
|
@@ -24,6 +34,7 @@
|
|
|
24
34
|
import { track } from "@working-theory/analytics/server";
|
|
25
35
|
import type { FastifyPluginAsync } from "fastify";
|
|
26
36
|
|
|
37
|
+
import { acceptsKind, resolveLeadDelivery } from "../../lib/lead-delivery.js";
|
|
27
38
|
import { currentSiteId, resolveSiteWorkspaceId } from "../../lib/site.js";
|
|
28
39
|
import {
|
|
29
40
|
type LeadKind,
|
|
@@ -175,7 +186,20 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
175
186
|
);
|
|
176
187
|
return reply.status(503).send({ error: "persist_failed" });
|
|
177
188
|
}
|
|
178
|
-
|
|
189
|
+
if (result.error.kind === "delivery_failed") {
|
|
190
|
+
// Email was the delivery and the provider did not accept it —
|
|
191
|
+
// never reported as sent (#6730 D5). Detail stays server-side.
|
|
192
|
+
// `{ kind, siteId }` only (spec D5): provider error text can echo
|
|
193
|
+
// an address.
|
|
194
|
+
request.log.error({ kind, siteId: workspaceId }, "lead delivery_failed");
|
|
195
|
+
return reply.status(503).send({
|
|
196
|
+
error: "delivery_failed",
|
|
197
|
+
message: "The message could not be delivered.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// config_missing — this deployment has nowhere to put this kind of
|
|
201
|
+
// lead (#6730 D1: production with neither a database nor the email
|
|
202
|
+
// vars, or a newsletter signup with no database).
|
|
179
203
|
return reply.status(503).send({
|
|
180
204
|
error: "lead_capture_not_configured",
|
|
181
205
|
message: "Lead capture is not configured.",
|
|
@@ -249,3 +273,24 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
249
273
|
export const leadSubmitRoute = makeLeadsRoute("submit");
|
|
250
274
|
export const leadSubscribeRoute = makeLeadsRoute("subscribe");
|
|
251
275
|
export const leadCallbackRoute = makeLeadsRoute("callback");
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* `GET /v1/leads/capture` (#6730 D4) — two booleans and nothing else: never
|
|
279
|
+
* an address or which env var is missing. (The pair implies as much of the
|
|
280
|
+
* delivery mode as the rendered page already shows.) Registered at
|
|
281
|
+
* the top level in `server.ts`, outside `leadsScope`'s strict per-IP limit
|
|
282
|
+
* (a web server's single egress IP already spends that on form submissions),
|
|
283
|
+
* so only the server-wide unauthenticated ceiling applies.
|
|
284
|
+
*/
|
|
285
|
+
export const leadCaptureStatusRoute: FastifyPluginAsync = async (fastify) => {
|
|
286
|
+
fastify.get("/v1/leads/capture", async (_request, reply) => {
|
|
287
|
+
const delivery = resolveLeadDelivery();
|
|
288
|
+
return reply.header("Cache-Control", "public, max-age=60").send({
|
|
289
|
+
ok: true,
|
|
290
|
+
data: {
|
|
291
|
+
contact: acceptsKind(delivery, "submit"),
|
|
292
|
+
newsletter: acceptsKind(delivery, "subscribe"),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
};
|
|
@@ -23,6 +23,7 @@ import { config } from "./config/index.js";
|
|
|
23
23
|
import { isModuleEnabled } from "./config/modules.js";
|
|
24
24
|
import { registerErrorSerializer } from "./lib/error-serializer.js";
|
|
25
25
|
import { validateLeadCaptureDir } from "./lib/lead-capture-store.js";
|
|
26
|
+
import { registerLicenseKeyStore } from "./lib/license-key-store.js";
|
|
26
27
|
import { requireCommerceClaim } from "./lib/require-commerce-claim.js";
|
|
27
28
|
import { assertSiteAliasResolves } from "./lib/site.js";
|
|
28
29
|
import { loadSlice } from "./lib/slice-load.js";
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
import { searchContentRoute } from "./routes/content/search.js";
|
|
58
59
|
import {
|
|
59
60
|
leadCallbackRoute,
|
|
61
|
+
leadCaptureStatusRoute,
|
|
60
62
|
leadSubmitRoute,
|
|
61
63
|
leadSubscribeRoute,
|
|
62
64
|
} from "./routes/leads/index.js";
|
|
@@ -522,6 +524,11 @@ export async function build() {
|
|
|
522
524
|
// database-less dev boot).
|
|
523
525
|
await assertSiteAliasResolves();
|
|
524
526
|
|
|
527
|
+
// #6677 DD-6 — the renewed licence key's durable home: with a database,
|
|
528
|
+
// one deployment-wide SiteSetting row, so a redeploy keeps the key this
|
|
529
|
+
// site renewed. Never throws; without DATABASE_URL the file store stays.
|
|
530
|
+
await registerLicenseKeyStore();
|
|
531
|
+
|
|
525
532
|
// Runtime sandbox-mode guard: reject live-mode Stripe keys before any
|
|
526
533
|
// webhook or checkout call can exercise them. Idempotent — safe even if
|
|
527
534
|
// assertSandboxKey is called again inside a route handler. UNCONDITIONAL
|
|
@@ -1139,6 +1146,12 @@ export async function build() {
|
|
|
1139
1146
|
await authEmailScope.register(transactionalEmailRoute);
|
|
1140
1147
|
});
|
|
1141
1148
|
|
|
1149
|
+
// Lead-capture status (#6730 D4) — a cheap read the web page makes before
|
|
1150
|
+
// choosing the form or the contact email. Top level, NOT in `leadsScope`:
|
|
1151
|
+
// the web server's single egress IP already spends that scope's strict
|
|
1152
|
+
// 10/min on form submissions. The global unauth ceiling above applies.
|
|
1153
|
+
await server.register(leadCaptureStatusRoute);
|
|
1154
|
+
|
|
1142
1155
|
// Leads routes — rate-limit middleware scoped to this sub-router. Own
|
|
1143
1156
|
// default (10/min, pre-#4730) rather than inheriting the wider
|
|
1144
1157
|
// flows-sized default (#5111, DEF-004 — #4730's shared-default bump was
|
|
@@ -553,3 +553,197 @@ describe("#6257 — the duplicate-subscriber lookup carries workspaceId", () =>
|
|
|
553
553
|
});
|
|
554
554
|
});
|
|
555
555
|
});
|
|
556
|
+
|
|
557
|
+
// ─── #6730 D1/D2 — success only for a durable row or an accepted email ───────
|
|
558
|
+
|
|
559
|
+
describe("#6730 — routeLead delivery modes", () => {
|
|
560
|
+
const ENV_KEYS = [
|
|
561
|
+
"DATABASE_URL",
|
|
562
|
+
"NODE_ENV",
|
|
563
|
+
"RESEND_API_KEY",
|
|
564
|
+
"AILK_LEAD_NOTIFICATION_TO",
|
|
565
|
+
"AILK_LEAD_NOTIFICATION_FROM",
|
|
566
|
+
"AILK_INBOX_FALLBACK_DIR",
|
|
567
|
+
"LEAD_CAPTURE_FILE",
|
|
568
|
+
] as const;
|
|
569
|
+
let saved: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>>;
|
|
570
|
+
let tmpDir: string;
|
|
571
|
+
const listFiles = () => fs.readdir(tmpDir);
|
|
572
|
+
|
|
573
|
+
const EMAIL_ENV = {
|
|
574
|
+
RESEND_API_KEY: "re_test",
|
|
575
|
+
AILK_LEAD_NOTIFICATION_TO: "owner@example.com",
|
|
576
|
+
AILK_LEAD_NOTIFICATION_FROM: "noreply@example.com",
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
beforeEach(async () => {
|
|
580
|
+
jest.clearAllMocks();
|
|
581
|
+
saved = {};
|
|
582
|
+
for (const k of ENV_KEYS) saved[k] = process.env[k];
|
|
583
|
+
for (const k of ENV_KEYS) delete process.env[k];
|
|
584
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "ailk-delivery-test-"));
|
|
585
|
+
process.env.AILK_INBOX_FALLBACK_DIR = tmpDir;
|
|
586
|
+
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: "lead_db_1" });
|
|
587
|
+
mockSendLeadNotification.mockResolvedValue({ ok: true, messageId: "msg-1" });
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
afterEach(async () => {
|
|
591
|
+
for (const k of ENV_KEYS) {
|
|
592
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
593
|
+
else process.env[k] = saved[k];
|
|
594
|
+
}
|
|
595
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe("email delivery (the three email vars, no DATABASE_URL, production)", () => {
|
|
599
|
+
beforeEach(() => {
|
|
600
|
+
Object.assign(process.env, EMAIL_ENV, { NODE_ENV: "production" });
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("an accepted email → ok:true persisted:email with an email- id; no Prisma, no file fallback", async () => {
|
|
604
|
+
const result = (await routeLead(leadInput, sourceMeta)) as Extract<
|
|
605
|
+
RouteLeadResult,
|
|
606
|
+
{ ok: true }
|
|
607
|
+
>;
|
|
608
|
+
|
|
609
|
+
expect(result).toMatchObject({
|
|
610
|
+
ok: true,
|
|
611
|
+
persisted: "email",
|
|
612
|
+
email: { ok: true, messageId: "msg-1" },
|
|
613
|
+
});
|
|
614
|
+
expect(result.id).toMatch(/^email-/);
|
|
615
|
+
expect(result.id).toMatch(/^[a-z0-9_-]{8,64}$/i);
|
|
616
|
+
expect(prisma.lead.create).not.toHaveBeenCalled();
|
|
617
|
+
expect(await listFiles()).toEqual([]);
|
|
618
|
+
expect(mockSendLeadNotification).toHaveBeenCalledTimes(1);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("a refused email → ok:false delivery_failed (never reported as sent)", async () => {
|
|
622
|
+
mockSendLeadNotification.mockResolvedValue({
|
|
623
|
+
ok: false,
|
|
624
|
+
error: { kind: "provider_error", message: "rejected" },
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const result = (await routeLead(leadInput, sourceMeta)) as Extract<
|
|
628
|
+
RouteLeadResult,
|
|
629
|
+
{ ok: false }
|
|
630
|
+
>;
|
|
631
|
+
|
|
632
|
+
expect(result.ok).toBe(false);
|
|
633
|
+
expect(result.error.kind).toBe("delivery_failed");
|
|
634
|
+
expect(prisma.lead.create).not.toHaveBeenCalled();
|
|
635
|
+
expect(await listFiles()).toEqual([]);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("a skipped email (provider reports the key missing) → delivery_failed", async () => {
|
|
639
|
+
mockSendLeadNotification.mockResolvedValue({
|
|
640
|
+
ok: false,
|
|
641
|
+
error: { kind: "config_missing", message: "RESEND_API_KEY not set" },
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const result = await routeLead(leadInput, sourceMeta);
|
|
645
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "delivery_failed" } });
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("a callback is delivered by email too — carrying the name and number to call", async () => {
|
|
649
|
+
const result = await routeLead(
|
|
650
|
+
{ ...callbackInput, preferredTime: "mornings" },
|
|
651
|
+
sourceMeta,
|
|
652
|
+
);
|
|
653
|
+
expect(result).toMatchObject({ ok: true, persisted: "email" });
|
|
654
|
+
expect(prisma.callback.create).not.toHaveBeenCalled();
|
|
655
|
+
const arg = mockSendLeadNotification.mock.calls[0][0];
|
|
656
|
+
expect(arg.lead.payload).toMatchObject({
|
|
657
|
+
name: "Alice",
|
|
658
|
+
phone: "+15551234567",
|
|
659
|
+
preferredTime: "mornings",
|
|
660
|
+
notes: "urgent",
|
|
661
|
+
});
|
|
662
|
+
// No visitor address to reply to — never an empty Reply-To.
|
|
663
|
+
expect(arg.replyToOverride).toBe("owner@example.com");
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("a newsletter subscribe is refused with config_missing — a signup needs a database", async () => {
|
|
667
|
+
const result = await routeLead(subscribeInput, sourceMeta);
|
|
668
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "config_missing" } });
|
|
669
|
+
expect(prisma.subscriber.create).not.toHaveBeenCalled();
|
|
670
|
+
expect(mockSendLeadNotification).not.toHaveBeenCalled();
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
describe("production with neither a database nor the email vars", () => {
|
|
675
|
+
beforeEach(() => {
|
|
676
|
+
process.env.NODE_ENV = "production";
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("refuses with config_missing before any write", async () => {
|
|
680
|
+
const result = await routeLead(leadInput, sourceMeta);
|
|
681
|
+
|
|
682
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "config_missing" } });
|
|
683
|
+
expect(prisma.lead.create).not.toHaveBeenCalled();
|
|
684
|
+
expect(mockSendLeadNotification).not.toHaveBeenCalled();
|
|
685
|
+
expect(await listFiles()).toEqual([]);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
describe("inbox delivery in production with the database failing (not a constraint)", () => {
|
|
690
|
+
beforeEach(() => {
|
|
691
|
+
process.env.DATABASE_URL = "postgres://test";
|
|
692
|
+
process.env.NODE_ENV = "production";
|
|
693
|
+
(prisma.lead.create as jest.Mock).mockRejectedValue(new Error("connection refused"));
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("no email vars → persist_failed, and the non-durable file fallback is not attempted", async () => {
|
|
697
|
+
const result = await routeLead(leadInput, sourceMeta);
|
|
698
|
+
|
|
699
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "persist_failed" } });
|
|
700
|
+
expect(await listFiles()).toEqual([]);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("email vars set and the send accepted → persisted:email", async () => {
|
|
704
|
+
Object.assign(process.env, EMAIL_ENV);
|
|
705
|
+
|
|
706
|
+
const result = (await routeLead(leadInput, sourceMeta)) as Extract<
|
|
707
|
+
RouteLeadResult,
|
|
708
|
+
{ ok: true }
|
|
709
|
+
>;
|
|
710
|
+
|
|
711
|
+
expect(result).toMatchObject({ ok: true, persisted: "email" });
|
|
712
|
+
expect(result.id).toMatch(/^email-/);
|
|
713
|
+
expect(await listFiles()).toEqual([]);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("a newsletter signup is never emailed — persist_failed", async () => {
|
|
717
|
+
Object.assign(process.env, EMAIL_ENV);
|
|
718
|
+
(prisma.subscriber.create as jest.Mock).mockRejectedValue(new Error("connection refused"));
|
|
719
|
+
|
|
720
|
+
const result = await routeLead(subscribeInput, sourceMeta);
|
|
721
|
+
|
|
722
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "persist_failed" } });
|
|
723
|
+
expect(mockSendLeadNotification).not.toHaveBeenCalled();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("email vars set but the send refused → persist_failed", async () => {
|
|
727
|
+
Object.assign(process.env, EMAIL_ENV);
|
|
728
|
+
mockSendLeadNotification.mockResolvedValue({
|
|
729
|
+
ok: false,
|
|
730
|
+
error: { kind: "provider_error", message: "rejected" },
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const result = await routeLead(leadInput, sourceMeta);
|
|
734
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "persist_failed" } });
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
describe("inbox delivery outside production keeps today's file fallback", () => {
|
|
739
|
+
it("DB down in test → persisted:file", async () => {
|
|
740
|
+
process.env.DATABASE_URL = "postgres://test";
|
|
741
|
+
process.env.NODE_ENV = "test";
|
|
742
|
+
(prisma.lead.create as jest.Mock).mockRejectedValue(new Error("connection refused"));
|
|
743
|
+
|
|
744
|
+
const result = await routeLead(leadInput, sourceMeta);
|
|
745
|
+
expect(result).toMatchObject({ ok: true, persisted: "file" });
|
|
746
|
+
expect((await listFiles()).length).toBeGreaterThan(0);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
});
|
|
@@ -327,3 +327,127 @@ describe("listOwnedLeads", () => {
|
|
|
327
327
|
expect(second.value.nextCursor).toBeNull();
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
// ─── #6730 D7 — the single-tenant inbox, pinned to the operator ──────────────
|
|
332
|
+
|
|
333
|
+
describe("listOwnedLeads — single-tenant build (#6730 D7)", () => {
|
|
334
|
+
const { DEFAULT_ORG_ID, DEFAULT_WORKSPACE_ID } = jest.requireActual(
|
|
335
|
+
"@working-theory/database/tenancy",
|
|
336
|
+
) as { DEFAULT_ORG_ID: string; DEFAULT_WORKSPACE_ID: string };
|
|
337
|
+
const OPERATOR_EMAIL = "owner@example.com";
|
|
338
|
+
const prior = {
|
|
339
|
+
modules: process.env.AILK_ENABLED_MODULES,
|
|
340
|
+
pin: process.env.AILK_OPERATOR_EMAIL,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/** One workspace — the seeded default — and a caller whose verified email is `email`. */
|
|
344
|
+
function oneWorkspaceDeployment(opts: {
|
|
345
|
+
email: string;
|
|
346
|
+
verified?: boolean;
|
|
347
|
+
workspaceCount?: number;
|
|
348
|
+
role?: string;
|
|
349
|
+
}) {
|
|
350
|
+
(prisma.workspace.count as jest.Mock).mockResolvedValue(opts.workspaceCount ?? 1);
|
|
351
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
|
|
352
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
353
|
+
organizationId: DEFAULT_ORG_ID,
|
|
354
|
+
});
|
|
355
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
356
|
+
id: USER_ID,
|
|
357
|
+
email: opts.email,
|
|
358
|
+
emailVerified: opts.verified ?? true,
|
|
359
|
+
});
|
|
360
|
+
(prisma.membership.create as jest.Mock).mockResolvedValue({});
|
|
361
|
+
(prisma.membership.findUnique as jest.Mock).mockResolvedValue({
|
|
362
|
+
role: opts.role ?? "owner",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
beforeEach(() => {
|
|
367
|
+
jest.clearAllMocks();
|
|
368
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
369
|
+
process.env.AILK_OPERATOR_EMAIL = OPERATOR_EMAIL;
|
|
370
|
+
seedLeadTable([
|
|
371
|
+
lead({ id: "lead_own", siteId: DEFAULT_WORKSPACE_ID }),
|
|
372
|
+
lead({ id: "lead_elsewhere", siteId: OTHER_SITE_ID }),
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
afterAll(() => {
|
|
377
|
+
for (const [key, value] of [
|
|
378
|
+
["AILK_ENABLED_MODULES", prior.modules],
|
|
379
|
+
["AILK_OPERATOR_EMAIL", prior.pin],
|
|
380
|
+
] as const) {
|
|
381
|
+
if (value === undefined) delete process.env[key];
|
|
382
|
+
else process.env[key] = value;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("the pinned, verified owner sees the default workspace's leads — and only those", async () => {
|
|
387
|
+
oneWorkspaceDeployment({ email: OPERATOR_EMAIL });
|
|
388
|
+
|
|
389
|
+
const result = await listOwnedLeads({ userId: USER_ID, limit: 50 });
|
|
390
|
+
|
|
391
|
+
expect(result.ok).toBe(true);
|
|
392
|
+
if (!result.ok) return;
|
|
393
|
+
expect(result.value.items.map((r) => r.id)).toEqual(["lead_own"]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("the pin unset → empty", async () => {
|
|
397
|
+
delete process.env.AILK_OPERATOR_EMAIL;
|
|
398
|
+
oneWorkspaceDeployment({ email: OPERATOR_EMAIL });
|
|
399
|
+
|
|
400
|
+
const result = await listOwnedLeads({ userId: USER_ID, limit: 50 });
|
|
401
|
+
|
|
402
|
+
expect(result).toEqual({ ok: true, value: { items: [], nextCursor: null } });
|
|
403
|
+
expect(prisma.lead.findMany).not.toHaveBeenCalled();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("a different verified email → empty", async () => {
|
|
407
|
+
oneWorkspaceDeployment({ email: "stranger@example.com" });
|
|
408
|
+
|
|
409
|
+
const result = await listOwnedLeads({ userId: USER_ID, limit: 50 });
|
|
410
|
+
|
|
411
|
+
expect(result).toEqual({ ok: true, value: { items: [], nextCursor: null } });
|
|
412
|
+
expect(prisma.lead.findMany).not.toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("the pinned email, unverified → empty", async () => {
|
|
416
|
+
oneWorkspaceDeployment({ email: OPERATOR_EMAIL, verified: false });
|
|
417
|
+
|
|
418
|
+
const result = await listOwnedLeads({ userId: USER_ID, limit: 50 });
|
|
419
|
+
|
|
420
|
+
expect(result).toEqual({ ok: true, value: { items: [], nextCursor: null } });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("a second workspace → empty (not unambiguously one-workspace)", async () => {
|
|
424
|
+
oneWorkspaceDeployment({ email: OPERATOR_EMAIL, workspaceCount: 2 });
|
|
425
|
+
|
|
426
|
+
const result = await listOwnedLeads({ userId: USER_ID, limit: 50 });
|
|
427
|
+
|
|
428
|
+
expect(result).toEqual({ ok: true, value: { items: [], nextCursor: null } });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("a `member` role → empty — the role floor still holds", async () => {
|
|
432
|
+
oneWorkspaceDeployment({ email: OPERATOR_EMAIL, role: "member" });
|
|
433
|
+
|
|
434
|
+
const result = await listOwnedLeads({ userId: USER_ID, limit: 50 });
|
|
435
|
+
|
|
436
|
+
expect(result).toEqual({ ok: true, value: { items: [], nextCursor: null } });
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("with multi-tenant on, resolveOwnerWorkspaces is the path — the default-tenant resolver is not called", async () => {
|
|
440
|
+
process.env.AILK_ENABLED_MODULES = "multi-tenant";
|
|
441
|
+
oneWorkspaceDeployment({ email: OPERATOR_EMAIL });
|
|
442
|
+
(prisma.membership.findMany as jest.Mock).mockResolvedValue([]);
|
|
443
|
+
const ownerWorkspaces = jest.requireActual("../owner-workspaces.js");
|
|
444
|
+
const spy = jest.spyOn(ownerWorkspaces, "resolveOwnerWorkspaces");
|
|
445
|
+
|
|
446
|
+
const result = await listOwnedLeads({ userId: USER_ID, limit: 50 });
|
|
447
|
+
|
|
448
|
+
expect(spy).toHaveBeenCalledWith(USER_ID);
|
|
449
|
+
expect(prisma.user.findUnique).not.toHaveBeenCalled();
|
|
450
|
+
expect(result).toEqual({ ok: true, value: { items: [], nextCursor: null } });
|
|
451
|
+
spy.mockRestore();
|
|
452
|
+
});
|
|
453
|
+
});
|