create-ailk 0.4.0 → 0.6.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 +55 -8
- package/component-catalog.json +12248 -0
- package/component-catalog.md +193 -68
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +60 -8
- package/dist/module-architecture.js +587 -242
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +209 -36
- package/dist/template-manifest.d.ts +88 -3
- package/dist/template-manifest.js +281 -29
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +2 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
- package/site-templates/minimalism-site/ailk-template.json +22 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +102 -12
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +29 -2
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +7 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +521 -55
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +300 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +160 -0
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +284 -0
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +58 -25
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +8 -2
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +61 -37
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +9 -7
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +74 -8
- package/templates/apps/web/CLAUDE.md +46 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +308 -65
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +15 -19
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
- package/templates/apps/web/messages/en.json +216 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +8 -3
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file revalidation.test.ts
|
|
3
|
+
* @description Proves the publish-revalidation webhook (#5851) against a REAL
|
|
4
|
+
* loopback HTTP server (spec D11), not a `global.fetch` double: the contract
|
|
5
|
+
* under test is the wire format, and only a real server proves that the bytes
|
|
6
|
+
* on the wire are the bytes the signature covers. The server recomputes the
|
|
7
|
+
* HMAC inside its own handler — the receiver's verification, performed for
|
|
8
|
+
* real — and compares with `timingSafeEqual`, exactly as a Phase-3 receiver
|
|
9
|
+
* will (spec FR7).
|
|
10
|
+
*
|
|
11
|
+
* The server binds 127.0.0.1, which `ssrfBlocked` rejects; D7's
|
|
12
|
+
* `allowPrivate: true` is what makes this substrate runnable at all.
|
|
13
|
+
*
|
|
14
|
+
* `CONTENT_REVALIDATE_*` are read through `config`, which snapshots
|
|
15
|
+
* `process.env` at import time — so every case sets env first and then
|
|
16
|
+
* `require`s the service through `jest.resetModules()`.
|
|
17
|
+
*/
|
|
18
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
19
|
+
import { createServer, type Server } from "node:http";
|
|
20
|
+
import type { AddressInfo } from "node:net";
|
|
21
|
+
|
|
22
|
+
// Obviously-fake signing key — never a real secret (repo rule).
|
|
23
|
+
const TEST_SECRET = "not-a-real-secret-fixture-value";
|
|
24
|
+
const SIGNATURE_HEADER = "x-ailk-revalidate-signature";
|
|
25
|
+
const EVENT_HEADER = "x-ailk-revalidate-event";
|
|
26
|
+
|
|
27
|
+
const TARGET = {
|
|
28
|
+
workspaceId: "ws_test_0001",
|
|
29
|
+
type: "page",
|
|
30
|
+
slug: "pricing",
|
|
31
|
+
locale: "en",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface LogLine {
|
|
35
|
+
level: "info" | "warn" | "error";
|
|
36
|
+
obj: Record<string, unknown>;
|
|
37
|
+
msg: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Captured through the observability boundary so assertions don't depend on
|
|
41
|
+
// pino's stdout serialization — the idiom of config/__tests__/index.test.ts.
|
|
42
|
+
const mockLogLines: LogLine[] = [];
|
|
43
|
+
const mockRecord =
|
|
44
|
+
(level: LogLine["level"]) => (obj: Record<string, unknown>, msg: string) =>
|
|
45
|
+
void mockLogLines.push({ level, obj, msg });
|
|
46
|
+
|
|
47
|
+
jest.mock("dotenv", () => ({ config: jest.fn() }));
|
|
48
|
+
jest.mock("@working-theory/observability", () => ({
|
|
49
|
+
createLogger: jest.fn(() => ({
|
|
50
|
+
info: mockRecord("info"),
|
|
51
|
+
warn: mockRecord("warn"),
|
|
52
|
+
error: mockRecord("error"),
|
|
53
|
+
debug: jest.fn(),
|
|
54
|
+
})),
|
|
55
|
+
logger: {
|
|
56
|
+
error: jest.fn(),
|
|
57
|
+
warn: jest.fn(),
|
|
58
|
+
info: jest.fn(),
|
|
59
|
+
debug: jest.fn(),
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
interface Recorded {
|
|
64
|
+
rawBody: string;
|
|
65
|
+
signatureValid: boolean;
|
|
66
|
+
signatureHeader: string;
|
|
67
|
+
eventHeader: string;
|
|
68
|
+
contentType: string;
|
|
69
|
+
receivedAt: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let server: Server;
|
|
73
|
+
let serverUrl: string;
|
|
74
|
+
let recorded: Recorded[] = [];
|
|
75
|
+
/** Status codes the server replies with, one per request, in order. */
|
|
76
|
+
let responseStatuses: number[] = [];
|
|
77
|
+
|
|
78
|
+
beforeAll(async () => {
|
|
79
|
+
server = createServer((req, res) => {
|
|
80
|
+
const chunks: Buffer[] = [];
|
|
81
|
+
req.on("data", (c: Buffer) => void chunks.push(c));
|
|
82
|
+
req.on("end", () => {
|
|
83
|
+
const raw = Buffer.concat(chunks);
|
|
84
|
+
|
|
85
|
+
// The receiver's own verification (FR7): recompute HMAC-SHA256 over the
|
|
86
|
+
// RAW request body bytes and compare in constant time.
|
|
87
|
+
const expected = Buffer.from(
|
|
88
|
+
createHmac("sha256", TEST_SECRET).update(raw).digest("hex"),
|
|
89
|
+
"utf8",
|
|
90
|
+
);
|
|
91
|
+
const received = Buffer.from(
|
|
92
|
+
String(req.headers[SIGNATURE_HEADER] ?? "").replace(/^sha256=/, ""),
|
|
93
|
+
"utf8",
|
|
94
|
+
);
|
|
95
|
+
const signatureValid =
|
|
96
|
+
expected.length === received.length &&
|
|
97
|
+
timingSafeEqual(expected, received);
|
|
98
|
+
|
|
99
|
+
recorded.push({
|
|
100
|
+
rawBody: raw.toString("utf8"),
|
|
101
|
+
signatureValid,
|
|
102
|
+
signatureHeader: String(req.headers[SIGNATURE_HEADER] ?? ""),
|
|
103
|
+
eventHeader: String(req.headers[EVENT_HEADER] ?? ""),
|
|
104
|
+
contentType: String(req.headers["content-type"] ?? ""),
|
|
105
|
+
receivedAt: Date.now(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
res.writeHead(responseStatuses[recorded.length - 1] ?? 200);
|
|
109
|
+
res.end();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
114
|
+
const { port } = server.address() as AddressInfo;
|
|
115
|
+
serverUrl = `http://127.0.0.1:${port}/api/revalidate`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterAll(async () => {
|
|
119
|
+
await new Promise<void>((resolve, reject) =>
|
|
120
|
+
server.close((err) => (err ? reject(err) : resolve())),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const originalEnv = process.env;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
jest.resetModules();
|
|
128
|
+
process.env = { ...originalEnv };
|
|
129
|
+
process.env.CONTENT_REVALIDATE_URL = serverUrl;
|
|
130
|
+
process.env.CONTENT_REVALIDATE_SECRET = TEST_SECRET;
|
|
131
|
+
recorded = [];
|
|
132
|
+
responseStatuses = [];
|
|
133
|
+
mockLogLines.length = 0;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
process.env = originalEnv;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
function loadService(): typeof import("../revalidation") {
|
|
141
|
+
return require("../revalidation") as typeof import("../revalidation");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe("notifyRevalidate", () => {
|
|
145
|
+
it("POSTs the four-key body once with a signature the receiver verifies", async () => {
|
|
146
|
+
responseStatuses = [200];
|
|
147
|
+
|
|
148
|
+
await loadService().notifyRevalidate(TARGET, { event: "publish" });
|
|
149
|
+
|
|
150
|
+
expect(recorded).toHaveLength(1);
|
|
151
|
+
const [req] = recorded;
|
|
152
|
+
expect(JSON.parse(req.rawBody)).toEqual(TARGET);
|
|
153
|
+
expect(Object.keys(JSON.parse(req.rawBody))).toEqual([
|
|
154
|
+
"workspaceId",
|
|
155
|
+
"type",
|
|
156
|
+
"slug",
|
|
157
|
+
"locale",
|
|
158
|
+
]);
|
|
159
|
+
expect(req.signatureValid).toBe(true);
|
|
160
|
+
expect(req.signatureHeader).toMatch(/^sha256=[0-9a-f]{64}$/);
|
|
161
|
+
expect(req.eventHeader).toBe("publish");
|
|
162
|
+
expect(req.contentType).toBe("application/json");
|
|
163
|
+
|
|
164
|
+
const sent = mockLogLines.filter((l) => l.msg === "revalidate.sent");
|
|
165
|
+
expect(sent).toHaveLength(1);
|
|
166
|
+
expect(sent[0]).toMatchObject({
|
|
167
|
+
level: "info",
|
|
168
|
+
obj: { ...TARGET, event: "publish", attempt: 1, status: 200 },
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("retries a 5xx with D3 backoff and succeeds on the third attempt", async () => {
|
|
173
|
+
responseStatuses = [500, 500, 200];
|
|
174
|
+
|
|
175
|
+
await loadService().notifyRevalidate(TARGET, { event: "unpublish" });
|
|
176
|
+
|
|
177
|
+
expect(recorded).toHaveLength(3);
|
|
178
|
+
// D3 — 500 ms before attempt 2, 2000 ms before attempt 3. Bounded on BOTH
|
|
179
|
+
// sides: a lower bound alone would pass an arbitrarily long backoff, and
|
|
180
|
+
// the whole point of D3's schedule is that it fits the 60 s publish budget.
|
|
181
|
+
// The upper slack absorbs loopback scheduling jitter, nothing more.
|
|
182
|
+
const gapA = recorded[1].receivedAt - recorded[0].receivedAt;
|
|
183
|
+
const gapB = recorded[2].receivedAt - recorded[1].receivedAt;
|
|
184
|
+
expect(gapA).toBeGreaterThanOrEqual(500);
|
|
185
|
+
expect(gapA).toBeLessThan(2000);
|
|
186
|
+
expect(gapB).toBeGreaterThanOrEqual(2000);
|
|
187
|
+
expect(gapB).toBeLessThan(4000);
|
|
188
|
+
expect(recorded.every((r) => r.signatureValid)).toBe(true);
|
|
189
|
+
|
|
190
|
+
expect(
|
|
191
|
+
mockLogLines.filter((l) => l.msg === "revalidate.sent"),
|
|
192
|
+
).toHaveLength(1);
|
|
193
|
+
expect(mockLogLines.some((l) => l.level === "error")).toBe(false);
|
|
194
|
+
}, 20_000);
|
|
195
|
+
|
|
196
|
+
it("stops at three attempts, logs the failure at error, and still resolves", async () => {
|
|
197
|
+
responseStatuses = [500, 500, 500];
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
loadService().notifyRevalidate(TARGET, { event: "publish" }),
|
|
201
|
+
).resolves.toBeUndefined();
|
|
202
|
+
|
|
203
|
+
expect(recorded).toHaveLength(3);
|
|
204
|
+
|
|
205
|
+
const perAttempt = mockLogLines.filter(
|
|
206
|
+
(l) => l.level === "warn" && l.msg === "revalidate.failed",
|
|
207
|
+
);
|
|
208
|
+
expect(perAttempt.map((l) => l.obj.attempt)).toEqual([1, 2, 3]);
|
|
209
|
+
|
|
210
|
+
const terminal = mockLogLines.filter(
|
|
211
|
+
(l) => l.level === "error" && l.msg === "revalidate.failed",
|
|
212
|
+
);
|
|
213
|
+
expect(terminal).toHaveLength(1);
|
|
214
|
+
expect(terminal[0].obj).toMatchObject({
|
|
215
|
+
...TARGET,
|
|
216
|
+
event: "publish",
|
|
217
|
+
attempts: 3,
|
|
218
|
+
lastStatus: 500,
|
|
219
|
+
});
|
|
220
|
+
}, 20_000);
|
|
221
|
+
|
|
222
|
+
it("is a single-warning no-op with CONTENT_REVALIDATE_URL unset", async () => {
|
|
223
|
+
delete process.env.CONTENT_REVALIDATE_URL;
|
|
224
|
+
delete process.env.CONTENT_REVALIDATE_SECRET;
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
loadService().notifyRevalidate(TARGET, { event: "publish" }),
|
|
228
|
+
).resolves.toBeUndefined();
|
|
229
|
+
|
|
230
|
+
expect(recorded).toHaveLength(0);
|
|
231
|
+
expect(mockLogLines).toHaveLength(1);
|
|
232
|
+
expect(mockLogLines[0]).toMatchObject({
|
|
233
|
+
level: "warn",
|
|
234
|
+
msg: "revalidate.skipped",
|
|
235
|
+
obj: { ...TARGET, event: "publish", reason: "url_unset" },
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file verified-email.test.ts
|
|
3
|
+
*
|
|
4
|
+
* The shared verified-email read (#5701 D4) — the one `User {id, email,
|
|
5
|
+
* emailVerified}` read every email-keyed authorization path goes through.
|
|
6
|
+
*
|
|
7
|
+
* What is asserted: the read's exact shape (the `where` and the three
|
|
8
|
+
* selected fields, which is what makes `tools/lint-email-authz.ts`'s
|
|
9
|
+
* predicate satisfiable and what the five migrated call sites' own tests
|
|
10
|
+
* still expect); the two distinct refusals, which the callers map to
|
|
11
|
+
* different outcomes on the wire; and — the invariant itself — that the
|
|
12
|
+
* address never leaves this function unless `emailVerified === true`.
|
|
13
|
+
*/
|
|
14
|
+
import { findVerifiedEmail } from "../verified-email";
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
|
|
17
|
+
jest.mock("../../lib/prisma.js", () => ({
|
|
18
|
+
prisma: { user: { findUnique: jest.fn() } },
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { prisma } from "../../lib/prisma.js";
|
|
22
|
+
|
|
23
|
+
const USER_ID = "user_00000000-0000-7000-8000-0000000000a1";
|
|
24
|
+
const EMAIL = "founder@example.com";
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("findVerifiedEmail", () => {
|
|
31
|
+
it("reads the User by id, selecting exactly id, email and emailVerified", async () => {
|
|
32
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
33
|
+
id: USER_ID,
|
|
34
|
+
email: EMAIL,
|
|
35
|
+
emailVerified: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await findVerifiedEmail(USER_ID);
|
|
39
|
+
|
|
40
|
+
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
|
41
|
+
where: { id: USER_ID },
|
|
42
|
+
select: { id: true, email: true, emailVerified: true },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns the address when the User exists and the address is verified", async () => {
|
|
47
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
48
|
+
id: USER_ID,
|
|
49
|
+
email: EMAIL,
|
|
50
|
+
emailVerified: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = await findVerifiedEmail(USER_ID);
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual({ ok: true, value: { id: USER_ID, email: EMAIL } });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("refuses with user_not_found when there is no User row", async () => {
|
|
59
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
|
|
60
|
+
|
|
61
|
+
const result = await findVerifiedEmail(USER_ID);
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({ ok: false, reason: "user_not_found" });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("refuses with email_unverified — and never yields the address — when the address is unverified", async () => {
|
|
67
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
68
|
+
id: USER_ID,
|
|
69
|
+
email: EMAIL,
|
|
70
|
+
emailVerified: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await findVerifiedEmail(USER_ID);
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({ ok: false, reason: "email_unverified" });
|
|
76
|
+
expect(JSON.stringify(result)).not.toContain(EMAIL);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -275,9 +275,15 @@ export async function runAbandonEmailSweep(
|
|
|
275
275
|
if (dbError) return { ok: false, error: dbError };
|
|
276
276
|
|
|
277
277
|
const now = opts.now ?? new Date();
|
|
278
|
+
// #4740: `||`, not `??` — an accidentally-set empty-string override
|
|
279
|
+
// (AILK_ABANDON_EMAIL_FROM="") is an env GAP, not a sender, so it falls
|
|
280
|
+
// through to the owner-wide default; both empty resolves to `undefined` and
|
|
281
|
+
// the `!fromAddress` guard leaves the candidate unstamped. The same shape
|
|
282
|
+
// is applied identically in waitlist-followup.ts's sibling sweep.
|
|
278
283
|
const fromAddress =
|
|
279
|
-
process.env.AILK_ABANDON_EMAIL_FROM
|
|
280
|
-
process.env.AILK_LEAD_NOTIFICATION_FROM
|
|
284
|
+
process.env.AILK_ABANDON_EMAIL_FROM ||
|
|
285
|
+
process.env.AILK_LEAD_NOTIFICATION_FROM ||
|
|
286
|
+
undefined;
|
|
281
287
|
|
|
282
288
|
const summary: AbandonEmailSweepSummary = {
|
|
283
289
|
scanned: 0,
|
|
@@ -34,6 +34,20 @@ import type {
|
|
|
34
34
|
AeoScoreResult,
|
|
35
35
|
} from "@working-theory/validation";
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The readiness rules (#5949) are deliberately NOT in this set. They grade
|
|
39
|
+
* frontmatter and site-set facts (`AuditInput.readiness`); `buildAuditPayload`
|
|
40
|
+
* produces no frontmatter, so on a crawl input all three pass vacuously
|
|
41
|
+
* (packages/aeo FR5). Adding them would put three always-100 dimensions into a
|
|
42
|
+
* closed four-member public enum. `/v1/aeo/score` has no MCP twin
|
|
43
|
+
* (apps/api/CLAUDE.md § API ↔ MCP parity), so this is not a parity divergence.
|
|
44
|
+
*/
|
|
45
|
+
export const AEO_SCORE_EXCLUDED_READINESS_RULES = [
|
|
46
|
+
"policies-present",
|
|
47
|
+
"offer-on-priced-page",
|
|
48
|
+
"tax-code-when-stripe-tax",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
37
51
|
/** The service's fixed rule set — the four content rules only (§ 2.1 FR4). */
|
|
38
52
|
export const AEO_SCORE_RULES: Rule[] = [
|
|
39
53
|
schemaCoverage,
|
|
@@ -43,7 +57,10 @@ export const AEO_SCORE_RULES: Rule[] = [
|
|
|
43
57
|
];
|
|
44
58
|
|
|
45
59
|
/** Severity table, verbatim from prospect-audit-site's "Compose the score-led sales report". */
|
|
46
|
-
export const AEO_DIMENSION_SEVERITY: Record<
|
|
60
|
+
export const AEO_DIMENSION_SEVERITY: Record<
|
|
61
|
+
AeoDimensionId,
|
|
62
|
+
"high" | "medium" | "low"
|
|
63
|
+
> = {
|
|
47
64
|
"schema-coverage": "high",
|
|
48
65
|
"entity-markup": "high",
|
|
49
66
|
"jsonld-validity": "medium",
|
|
@@ -77,7 +94,8 @@ export function resolveScoreBudgetMs(
|
|
|
77
94
|
): number {
|
|
78
95
|
if (!rawEnv) return AEO_SCORE_DEFAULT_BUDGET_MS;
|
|
79
96
|
const parsed = Number(rawEnv);
|
|
80
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
97
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
98
|
+
return AEO_SCORE_DEFAULT_BUDGET_MS;
|
|
81
99
|
if (parsed > AEO_SCORE_MAX_BUDGET_MS) {
|
|
82
100
|
log?.warn(
|
|
83
101
|
`AEO_SCORE_BUDGET_MS=${rawEnv} exceeds the ${AEO_SCORE_MAX_BUDGET_MS}ms cap; falling back to the ${AEO_SCORE_DEFAULT_BUDGET_MS}ms default.`,
|
|
@@ -108,7 +126,10 @@ function faqHowtoOpportunityFinding(slug: string): string {
|
|
|
108
126
|
return `${slug}: no FAQPage or HowTo structured data (a visibility opportunity, not a defect)`;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
function buildDimension(
|
|
129
|
+
function buildDimension(
|
|
130
|
+
ruleId: AeoDimensionId,
|
|
131
|
+
report: AuditReport,
|
|
132
|
+
): AeoDimension {
|
|
112
133
|
const perRule = report.perRule.find((r) => r.id === ruleId) as PerRuleResult;
|
|
113
134
|
const perPage = perRule.perPage ?? [];
|
|
114
135
|
const fraction = perRule.score ?? (perRule.pass ? 1 : 0);
|
|
@@ -121,11 +142,20 @@ function buildDimension(ruleId: AeoDimensionId, report: AuditReport): AeoDimensi
|
|
|
121
142
|
if (ruleId === FAQ_HOWTO_INCENTIVE_ID) {
|
|
122
143
|
pagesPassing = perPage.filter((p) => p.bonus === true).length;
|
|
123
144
|
const firstMissing = perPage.find((p) => p.bonus !== true);
|
|
124
|
-
finding = firstMissing
|
|
145
|
+
finding = firstMissing
|
|
146
|
+
? faqHowtoOpportunityFinding(firstMissing.slug)
|
|
147
|
+
: null;
|
|
125
148
|
} else {
|
|
126
|
-
pagesPassing =
|
|
149
|
+
pagesPassing =
|
|
150
|
+
perPage.length > 0
|
|
151
|
+
? perPage.filter((p) => p.pass).length
|
|
152
|
+
: perRule.pass
|
|
153
|
+
? 1
|
|
154
|
+
: 0;
|
|
127
155
|
const firstFailing = perPage.find((p) => !p.pass);
|
|
128
|
-
finding = firstFailing
|
|
156
|
+
finding = firstFailing
|
|
157
|
+
? (firstFailing.evidence ?? perRule.evidence ?? null)
|
|
158
|
+
: null;
|
|
129
159
|
}
|
|
130
160
|
|
|
131
161
|
return {
|
|
@@ -168,11 +198,14 @@ const SSRF_GUARD_REASON_PATTERN =
|
|
|
168
198
|
*/
|
|
169
199
|
function toClientSkippedEntry(entry: SkippedEntry): SkippedEntry {
|
|
170
200
|
if (!SSRF_GUARD_REASON_PATTERN.test(entry.reason)) return entry;
|
|
171
|
-
console.warn(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
console.warn(
|
|
202
|
+
"aeo-score: per-path SSRF-guard rejection (redacted on the wire)",
|
|
203
|
+
{
|
|
204
|
+
path: entry.path,
|
|
205
|
+
url: entry.url,
|
|
206
|
+
reason: entry.reason,
|
|
207
|
+
},
|
|
208
|
+
);
|
|
176
209
|
return { ...entry, reason: "ssrf_blocked" };
|
|
177
210
|
}
|
|
178
211
|
|
|
@@ -233,7 +266,11 @@ export async function scoreUrl(
|
|
|
233
266
|
...(deps.resolver !== undefined && { resolver: deps.resolver }),
|
|
234
267
|
});
|
|
235
268
|
if (!safe.ok) {
|
|
236
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
status: "unavailable",
|
|
271
|
+
reason: "ssrf_blocked",
|
|
272
|
+
message: safe.reason,
|
|
273
|
+
};
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
const budgetMs = deps.budgetMs ?? AEO_SCORE_DEFAULT_BUDGET_MS;
|