create-ailk 0.1.0 → 0.2.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 +124 -3
- package/component-catalog.md +456 -0
- package/dist/cli.js +92 -16
- package/dist/component-catalog.d.ts +11 -0
- package/dist/component-catalog.js +39 -0
- package/dist/copy.d.ts +7 -0
- package/dist/copy.js +24 -0
- package/dist/derive-architecture.d.ts +13 -0
- package/dist/derive-architecture.js +238 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +19 -2
- package/dist/lib/extract-module-bundle.d.ts +16 -0
- package/dist/lib/extract-module-bundle.js +184 -0
- package/dist/lib/fetch-module.d.ts +55 -0
- package/dist/lib/fetch-module.js +125 -0
- package/dist/lib/module-license-gate.d.ts +48 -0
- package/dist/lib/module-license-gate.js +67 -0
- package/dist/lib/module-tier.d.ts +35 -0
- package/dist/lib/module-tier.js +37 -0
- package/dist/module-architecture.d.ts +175 -0
- package/dist/module-architecture.js +663 -0
- package/dist/parse-args.d.ts +47 -1
- package/dist/parse-args.js +143 -2
- package/dist/programmatic.d.ts +160 -3
- package/dist/programmatic.js +196 -7
- package/dist/surfaces.d.ts +211 -0
- package/dist/surfaces.js +532 -0
- package/dist/sync-routes.d.ts +92 -0
- package/dist/sync-routes.js +350 -0
- package/package.json +12 -5
- package/templates/.claude/rules/architecture.md +6 -2
- package/templates/.claude/skills/README.md +9 -9
- package/templates/.claude/skills/add-schema/SKILL.md +14 -7
- package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
- package/templates/.claude/skills/provision-config/SKILL.md +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
- package/templates/.env.example +126 -2
- package/templates/CLAUDE.md +11 -5
- package/templates/README.md +1 -1
- package/templates/apps/api/.env.example +38 -0
- package/templates/apps/api/CLAUDE.md +78 -1
- package/templates/apps/api/LICENSE +201 -0
- package/templates/apps/api/api/index.ts +6 -141
- package/templates/apps/api/jest.config.cjs +16 -0
- package/templates/apps/api/package.json +22 -6
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
- package/templates/apps/api/src/__tests__/server.test.ts +46 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
- package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
- package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
- package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
- package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
- package/templates/apps/api/src/bin/seed-presets.ts +58 -0
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
- package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
- package/templates/apps/api/src/cli.ts +5 -11
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/config/modules.ts +43 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
- package/templates/apps/api/src/lib/site.ts +16 -0
- package/templates/apps/api/src/lib/stripe.ts +22 -0
- package/templates/apps/api/src/lib/tenant-db.ts +225 -0
- package/templates/apps/api/src/lib/ws-token.ts +91 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
- package/templates/apps/api/src/middleware/auth.ts +43 -8
- package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
- package/templates/apps/api/src/middleware/tenant.ts +99 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
- package/templates/apps/api/src/openapi/spec.ts +312 -3
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
- package/templates/apps/api/src/routes/aeo/index.ts +107 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
- package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
- package/templates/apps/api/src/routes/billing/index.ts +36 -0
- package/templates/apps/api/src/routes/billing/portal.ts +182 -0
- package/templates/apps/api/src/routes/billing/read.ts +75 -0
- package/templates/apps/api/src/routes/billing/usage.ts +153 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
- package/templates/apps/api/src/routes/content/create.ts +1 -0
- package/templates/apps/api/src/routes/content/delete.ts +1 -0
- package/templates/apps/api/src/routes/content/index.ts +42 -6
- package/templates/apps/api/src/routes/content/update.ts +1 -0
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
- package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
- package/templates/apps/api/src/routes/flows/README.md +147 -0
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
- package/templates/apps/api/src/routes/flows/index.ts +202 -0
- package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
- package/templates/apps/api/src/routes/leads/index.ts +43 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
- package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
- package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
- package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
- package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
- package/templates/apps/api/src/routes/schedule/index.ts +249 -0
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
- package/templates/apps/api/src/routes/slack/actions.ts +177 -0
- package/templates/apps/api/src/routes/slack/index.ts +38 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
- package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
- package/templates/apps/api/src/routes/webhooks/README.md +35 -12
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
- package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
- package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
- package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
- package/templates/apps/api/src/server.ts +307 -7
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
- package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
- package/templates/apps/api/src/services/abandon-email.ts +307 -0
- package/templates/apps/api/src/services/aeo-score.ts +288 -0
- package/templates/apps/api/src/services/answer-distributions.ts +138 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
- package/templates/apps/api/src/services/entitlements.ts +102 -0
- package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
- package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
- package/templates/apps/api/src/services/flow-engine.ts +1296 -0
- package/templates/apps/api/src/services/lead-promotion.ts +176 -0
- package/templates/apps/api/src/services/lead-routing.ts +3 -3
- package/templates/apps/api/src/services/listing-config.ts +102 -0
- package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
- package/templates/apps/api/src/services/listing-promotion.ts +342 -0
- package/templates/apps/api/src/services/playbook-compile.ts +398 -0
- package/templates/apps/api/src/services/playbook-render.ts +263 -0
- package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
- package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
- package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
- package/templates/apps/api/src/services/project-listings.ts +1472 -0
- package/templates/apps/api/src/services/recommender-capture.ts +835 -0
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
- package/templates/apps/api/src/services/scheduling/index.ts +63 -0
- package/templates/apps/api/src/services/scheduling/types.ts +88 -0
- package/templates/apps/api/src/services/slack-notify.ts +240 -0
- package/templates/apps/api/src/services/tenant-context.ts +451 -0
- package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
- package/templates/apps/api/src/services/usage-credits.ts +340 -0
- package/templates/apps/api/src/services/usage-metering.ts +699 -0
- package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
- package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
- package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
- package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
- package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
- package/templates/apps/api/src/vercel-handler.ts +165 -0
- package/templates/apps/api/tsconfig.json +0 -12
- package/templates/apps/mcp/CLAUDE.md +19 -9
- package/templates/apps/mcp/LICENSE +201 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
- package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
- package/templates/apps/mcp/package.json +6 -4
- package/templates/apps/mcp/src/config/modules.ts +37 -0
- package/templates/apps/mcp/src/server.ts +109 -18
- package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
- package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
- package/templates/apps/mcp/src/tools/create_page.ts +1 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
- package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
- package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
- package/templates/apps/mcp/src/tools/index.ts +25 -0
- package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
- package/templates/apps/mcp/src/tools/purchase.ts +3 -0
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
- package/templates/apps/mcp/src/tools/update_page.ts +1 -0
- package/templates/apps/mcp/tsconfig.json +1 -9
- package/templates/apps/web/.env.example +44 -0
- package/templates/apps/web/CLAUDE.md +28 -0
- package/templates/apps/web/LICENSE +201 -0
- package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
- package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
- package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
- package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
- package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
- package/templates/apps/web/__tests__/setup.ts +22 -32
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/layout.tsx +127 -16
- package/templates/apps/web/app/[locale]/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
- package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
- package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
- package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/globals.css +35 -0
- package/templates/apps/web/app/llms.txt/route.ts +2 -1
- package/templates/apps/web/app/og/route.tsx +10 -2
- package/templates/apps/web/auth.ts +8 -0
- package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
- package/templates/apps/web/content/en/blog/README.txt +25 -0
- package/templates/apps/web/content/en/docs/index.mdx +28 -0
- package/templates/apps/web/jest.config.cjs +90 -27
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
- package/templates/apps/web/lib/account-config.ts +13 -0
- package/templates/apps/web/lib/server-api.ts +85 -0
- package/templates/apps/web/lib/site-brand.tsx +121 -0
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/messages/en.json +275 -7
- package/templates/apps/web/middleware.ts +11 -3
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/apps/web/package.json +17 -7
- package/templates/apps/web/postcss.config.mjs +23 -0
- package/templates/apps/web/public/.well-known/security.txt +3 -3
- package/templates/apps/web/src/lib/routes.ts +15 -2
- package/templates/apps/web/tsconfig.json +1 -16
- package/templates/content/_site.mdx +36 -25
- package/templates/content/en/AboutPage/about.mdx +24 -14
- package/templates/content/en/ContactPage/contact.mdx +23 -14
- package/templates/content/en/FAQPage/faq.mdx +10 -392
- package/templates/content/en/HomePage/home.mdx +14 -285
- package/templates/database/CHANGELOG.md +443 -0
- package/templates/database/LICENSE +201 -0
- package/templates/database/README.md +3 -1
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
- package/templates/database/__tests__/flows-presets.test.ts +173 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
- package/templates/database/__tests__/seed.test.ts +88 -0
- package/templates/database/__tests__/user-cascade.test.ts +19 -4
- package/templates/database/auth/schema.prisma +521 -0
- package/templates/database/config/schema.prisma +234 -0
- package/templates/database/content/schema.prisma +263 -0
- package/templates/database/flows/listing-diagnostic.ts +182 -0
- package/templates/database/flows/presets.ts +315 -0
- package/templates/database/inbox/__tests__/schema.test.ts +4 -0
- package/templates/database/inbox/schema.prisma +574 -3
- package/templates/database/jest.config.cjs +8 -0
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/ops/schema.prisma +27 -0
- package/templates/database/package.json +42 -4
- package/templates/database/prisma.config.ts +1 -1
- package/templates/database/schema.prisma +19 -435
- package/templates/database/seed-test.ts +14 -4
- package/templates/database/seed.ts +148 -0
- package/templates/database/tenancy.ts +45 -0
- package/templates/database/tsconfig.flows.json +15 -0
- package/templates/database/tsconfig.json +7 -1
- package/templates/database/tsconfig.tenancy.json +14 -0
- package/templates/package.json +30 -55
- package/templates/pnpm-workspace.yaml +0 -3
- package/templates/project.yaml +1 -1
- package/templates/tsconfig.json +1 -37
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
- package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
- package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
- package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
- package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
- package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
- package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
- package/templates/apps/web/__tests__/page.test.tsx +0 -230
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
- package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
- package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
- package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
- package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
- package/templates/apps/web/content/en/blog/index.mdx +0 -15
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
- package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +0 -85
- package/templates/content/ar/HomePage/home.mdx +0 -123
- package/templates/content/ar/_site.mdx +0 -35
- package/templates/content/de/_site.mdx +0 -29
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.schema.json +0 -82
- package/templates/content/en/BookCall/book-call.mdx +0 -19
- package/templates/content/en/BookCall/book-call.schema.json +0 -121
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
- package/templates/content/en/CareersList/careers.mdx +0 -24
- package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
- package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
- package/templates/content/en/ClientsList/clients.mdx +0 -38
- package/templates/content/en/ContactPage/contact.schema.json +0 -100
- package/templates/content/en/ContactPoint/primary.mdx +0 -14
- package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
- package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
- package/templates/content/en/CoursesList/courses.mdx +0 -22
- package/templates/content/en/DownloadsList/downloads.mdx +0 -22
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
- package/templates/content/en/EventsList/events.mdx +0 -26
- package/templates/content/en/FAQPage/faq.schema.json +0 -231
- package/templates/content/en/GlossaryList/glossary.mdx +0 -19
- package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
- package/templates/content/en/HomePage/home.schema.json +0 -88
- package/templates/content/en/HomePage/launch-service.mdx +0 -475
- package/templates/content/en/HomePage/launch-service.schema.json +0 -138
- package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
- package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
- package/templates/content/en/Integration/github-actions.mdx +0 -31
- package/templates/content/en/Integration/openai-api.mdx +0 -26
- package/templates/content/en/Integration/vercel.mdx +0 -32
- package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
- package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
- package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
- package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
- package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
- package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
- package/templates/content/en/LandingPage/developers.mdx +0 -19
- package/templates/content/en/LandingPage/developers.schema.json +0 -109
- package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
- package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
- package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
- package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
- package/templates/content/en/LandingPage/platform.mdx +0 -369
- package/templates/content/en/LandingPage/platform.schema.json +0 -166
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
- package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
- package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
- package/templates/content/en/Organization/site-owner.mdx +0 -16
- package/templates/content/en/Partner/prisma.mdx +0 -31
- package/templates/content/en/Partner/supabase.mdx +0 -30
- package/templates/content/en/Partner/vercel.mdx +0 -28
- package/templates/content/en/PartnersList/partners.mdx +0 -22
- package/templates/content/en/Person/founder.mdx +0 -18
- package/templates/content/en/Pricing/pricing.mdx +0 -538
- package/templates/content/en/Pricing/pricing.schema.json +0 -209
- package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
- package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
- package/templates/content/en/Product/ailk.mdx +0 -19
- package/templates/content/en/Product/ailk.schema.json +0 -94
- package/templates/content/en/Product/api-launch-kit.mdx +0 -26
- package/templates/content/en/Product/launch-kit.mdx +0 -28
- package/templates/content/en/ProductsList/products.mdx +0 -26
- package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
- package/templates/content/en/ProjectsList/projects.mdx +0 -25
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
- package/templates/content/en/ResourcesHub/resources.mdx +0 -22
- package/templates/content/en/Service/consulting.mdx +0 -22
- package/templates/content/en/Service/implementation.mdx +0 -30
- package/templates/content/en/Service/launch-service.mdx +0 -232
- package/templates/content/en/Service/launch.mdx +0 -228
- package/templates/content/en/ServicesList/services.mdx +0 -84
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
- package/templates/content/en/TeamList/team.mdx +0 -63
- package/templates/content/en/TeamMember/founder.mdx +0 -72
- package/templates/content/en/TermsPage/terms.mdx +0 -19
- package/templates/content/en/TermsPage/terms.schema.json +0 -99
- package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
- package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
- package/templates/content/en-XA/AboutPage/about.mdx +0 -8
- package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
- package/templates/content/en-XA/HomePage/home.mdx +0 -8
- package/templates/content/es/_site.mdx +0 -30
- package/templates/content/fr/_site.mdx +0 -30
- package/templates/content/ja/_site.mdx +0 -30
- package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
- package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
- package/templates/content/pt-BR/HomePage/home.mdx +0 -23
|
@@ -52,6 +52,19 @@ import {
|
|
|
52
52
|
} from "../index";
|
|
53
53
|
import { prisma } from "../../../lib/prisma.js";
|
|
54
54
|
import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
|
|
55
|
+
// #3979 AC1 — the single shared fixture (packages/validation, which apps/api
|
|
56
|
+
// may_import_from per architecture.yaml) for the NewsletterFormInline
|
|
57
|
+
// emitted-body shape, also consumed by the layer-a block test
|
|
58
|
+
// (packages/ui/src/blocks/__tests__/NewsletterFormInline.test.tsx), so a
|
|
59
|
+
// change to the block's emitted keys can't silently leave this layer
|
|
60
|
+
// asserting a stale contract. #4017 D4 extends the same fixture module with
|
|
61
|
+
// the ContactForm `submit`-kind body, consumed by the layer-a section test
|
|
62
|
+
// (packages/ui/src/sections/__tests__/ContactForm.test.tsx).
|
|
63
|
+
import {
|
|
64
|
+
AGENCY_WAITLIST_LEAD_BODY,
|
|
65
|
+
CONTACT_FORM_LEAD_BODY,
|
|
66
|
+
NEWSLETTER_DEFAULT_LEAD_BODY,
|
|
67
|
+
} from "../../../../../../packages/validation/src/__tests__/fixtures/newsletter-lead-body.fixture.js";
|
|
55
68
|
|
|
56
69
|
const fileEnv = setupLeadFileEnv("index");
|
|
57
70
|
|
|
@@ -82,16 +95,16 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
82
95
|
delete process.env.LEAD_CAPTURE_INCLUDE_IP;
|
|
83
96
|
});
|
|
84
97
|
|
|
85
|
-
it("POST /api/leads/
|
|
86
|
-
const serverGeneratedId = "
|
|
98
|
+
it("POST /api/leads/submit — attacker-supplied id is not returned (server uuid used)", async () => {
|
|
99
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a1";
|
|
87
100
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
88
101
|
|
|
89
102
|
const res = await app.inject({
|
|
90
103
|
method: "POST",
|
|
91
|
-
url: "/v1/leads/
|
|
104
|
+
url: "/v1/leads/submit",
|
|
92
105
|
payload: {
|
|
93
106
|
id: "attacker",
|
|
94
|
-
kind: "
|
|
107
|
+
kind: "submit",
|
|
95
108
|
siteId: "site-abc",
|
|
96
109
|
source: "contact",
|
|
97
110
|
email: "test@example.com",
|
|
@@ -110,7 +123,7 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
110
123
|
});
|
|
111
124
|
|
|
112
125
|
it("POST /api/leads/subscribe — attacker-supplied id is not returned (server uuid used)", async () => {
|
|
113
|
-
const serverGeneratedId = "
|
|
126
|
+
const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000a2";
|
|
114
127
|
(prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
115
128
|
|
|
116
129
|
const res = await app.inject({
|
|
@@ -133,8 +146,114 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
133
146
|
}
|
|
134
147
|
});
|
|
135
148
|
|
|
149
|
+
// #3965 D4 layer (b) — the block's exact emitted shape (layer (a) pins it
|
|
150
|
+
// as { siteId, source, email } via application/json) persists through the
|
|
151
|
+
// REAL registered /v1/leads/subscribe plugin. #3979 AC1 retargets the body
|
|
152
|
+
// literal at the shared fixture (packages/validation) — it had already
|
|
153
|
+
// drifted from layer-a's own hand-copied literal (different `email`
|
|
154
|
+
// values), with nothing catching it.
|
|
155
|
+
it("POST /v1/leads/subscribe — the block's emitted JSON shape ({ siteId, source, email }) is accepted and persisted", async () => {
|
|
156
|
+
const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000b1";
|
|
157
|
+
(prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
158
|
+
|
|
159
|
+
// The exact body NewsletterFormInline emits at its defaults (#3965 D1) —
|
|
160
|
+
// no `kind` key: buildInput() injects `kind` server-side from the route,
|
|
161
|
+
// never from the body.
|
|
162
|
+
const res = await app.inject({
|
|
163
|
+
method: "POST",
|
|
164
|
+
url: "/v1/leads/subscribe",
|
|
165
|
+
headers: { "content-type": "application/json" },
|
|
166
|
+
payload: JSON.stringify(NEWSLETTER_DEFAULT_LEAD_BODY),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(res.statusCode).toBe(201);
|
|
170
|
+
const body = res.json();
|
|
171
|
+
expect(body.id).toBe(serverGeneratedId);
|
|
172
|
+
expect(body.persisted).toBe("db");
|
|
173
|
+
expect(prisma.subscriber.create).toHaveBeenCalledTimes(1);
|
|
174
|
+
const record = await fileEnv.readLastCaptureRecord();
|
|
175
|
+
expect(record.id).toBe(serverGeneratedId);
|
|
176
|
+
expect(record.source).toBe(NEWSLETTER_DEFAULT_LEAD_BODY.source);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// #3979 AC2 — the agency-waitlist instance (content/en/LandingPage/
|
|
180
|
+
// agency-waitlist.mdx: action "/v1/leads/submit", source "agency-waitlist",
|
|
181
|
+
// siteId unset → "default") had NO layer-b case at all before this issue —
|
|
182
|
+
// only /v1/leads/subscribe was covered. Unconditional 201 (not the
|
|
183
|
+
// permissive [201, 400] idiom used by the id-isolation tests above), since
|
|
184
|
+
// this asserts the block's real emitted shape is accepted, not merely that
|
|
185
|
+
// the id-isolation walls hold regardless of validation outcome.
|
|
186
|
+
it("POST /v1/leads/submit — the agency-waitlist instance's emitted JSON shape ({ siteId, source, email }) is accepted and persisted", async () => {
|
|
187
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b2";
|
|
188
|
+
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
189
|
+
|
|
190
|
+
// The exact body NewsletterFormInline emits for the agency-waitlist
|
|
191
|
+
// instance — no `kind`, no `payload` key: buildInput() injects `kind`
|
|
192
|
+
// server-side from the route and defaults `payload` to {} when absent.
|
|
193
|
+
const res = await app.inject({
|
|
194
|
+
method: "POST",
|
|
195
|
+
url: "/v1/leads/submit",
|
|
196
|
+
headers: { "content-type": "application/json" },
|
|
197
|
+
payload: JSON.stringify(AGENCY_WAITLIST_LEAD_BODY),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(res.statusCode).toBe(201);
|
|
201
|
+
const body = res.json();
|
|
202
|
+
expect(body.id).toBe(serverGeneratedId);
|
|
203
|
+
expect(body.persisted).toBe("db");
|
|
204
|
+
expect(prisma.lead.create).toHaveBeenCalledTimes(1);
|
|
205
|
+
const record = await fileEnv.readLastCaptureRecord();
|
|
206
|
+
expect(record.id).toBe(serverGeneratedId);
|
|
207
|
+
expect(record.source).toBe(AGENCY_WAITLIST_LEAD_BODY.source);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// #4017 D4 layer (b) — the ContactForm section's exact emitted shape
|
|
211
|
+
// (layer (a) pins it as { siteId, source, email, payload } via
|
|
212
|
+
// application/json) persists through the REAL registered
|
|
213
|
+
// /v1/leads/submit plugin. `email` is lifted by field name; every other
|
|
214
|
+
// declared field lands in `payload` — the shared fixture (packages/validation)
|
|
215
|
+
// is the single definition both layers assert against (#3979 AC1's
|
|
216
|
+
// pattern), so a change to the section's emitted keys can't silently leave
|
|
217
|
+
// this layer asserting a stale contract.
|
|
218
|
+
it("POST /v1/leads/submit — the ContactForm section's emitted JSON shape ({ siteId, source, email, payload }) is accepted and persisted", async () => {
|
|
219
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b3";
|
|
220
|
+
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
221
|
+
|
|
222
|
+
// The exact body ContactForm emits at its component defaults (#4017 D1) —
|
|
223
|
+
// no `kind` key: buildInput() injects `kind` server-side from the route,
|
|
224
|
+
// never from the body.
|
|
225
|
+
const res = await app.inject({
|
|
226
|
+
method: "POST",
|
|
227
|
+
url: "/v1/leads/submit",
|
|
228
|
+
headers: { "content-type": "application/json" },
|
|
229
|
+
payload: JSON.stringify(CONTACT_FORM_LEAD_BODY),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(res.statusCode).toBe(201);
|
|
233
|
+
const body = res.json();
|
|
234
|
+
expect(body.id).toBe(serverGeneratedId);
|
|
235
|
+
expect(body.persisted).toBe("db");
|
|
236
|
+
expect(prisma.lead.create).toHaveBeenCalledTimes(1);
|
|
237
|
+
expect(prisma.lead.create).toHaveBeenCalledWith({
|
|
238
|
+
data: {
|
|
239
|
+
siteId: CONTACT_FORM_LEAD_BODY.siteId,
|
|
240
|
+
source: CONTACT_FORM_LEAD_BODY.source,
|
|
241
|
+
email: CONTACT_FORM_LEAD_BODY.email,
|
|
242
|
+
payload: CONTACT_FORM_LEAD_BODY.payload,
|
|
243
|
+
},
|
|
244
|
+
select: { id: true },
|
|
245
|
+
});
|
|
246
|
+
const record = await fileEnv.readLastCaptureRecord();
|
|
247
|
+
expect(record.id).toBe(serverGeneratedId);
|
|
248
|
+
expect(record.source).toBe(CONTACT_FORM_LEAD_BODY.source);
|
|
249
|
+
expect(record.data).toEqual({
|
|
250
|
+
email: CONTACT_FORM_LEAD_BODY.email,
|
|
251
|
+
...CONTACT_FORM_LEAD_BODY.payload,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
136
255
|
it("POST /api/leads/callback — attacker-supplied id is not returned (server uuid used)", async () => {
|
|
137
|
-
const serverGeneratedId = "
|
|
256
|
+
const serverGeneratedId = "callbk_00000000-0000-7000-8000-0000000000a3";
|
|
138
257
|
(prisma.callback.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
139
258
|
|
|
140
259
|
const res = await app.inject({
|
|
@@ -161,14 +280,14 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
161
280
|
// ── Lead capture store integration cases ──────────────────────────────────
|
|
162
281
|
|
|
163
282
|
it("file-on-success — appendLeadRecord writes to LEAD_CAPTURE_FILE on successful DB persist", async () => {
|
|
164
|
-
const serverGeneratedId = "
|
|
283
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a4";
|
|
165
284
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
166
285
|
|
|
167
286
|
const res = await app.inject({
|
|
168
287
|
method: "POST",
|
|
169
|
-
url: "/v1/leads/
|
|
288
|
+
url: "/v1/leads/submit",
|
|
170
289
|
payload: {
|
|
171
|
-
kind: "
|
|
290
|
+
kind: "submit",
|
|
172
291
|
siteId: "site-abc",
|
|
173
292
|
source: "contact",
|
|
174
293
|
email: "test@example.com",
|
|
@@ -186,14 +305,14 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
186
305
|
});
|
|
187
306
|
|
|
188
307
|
it("dual-write — real lead-capture-store AND DB are both invoked on success", async () => {
|
|
189
|
-
const serverGeneratedId = "
|
|
308
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a5";
|
|
190
309
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
191
310
|
|
|
192
311
|
const res = await app.inject({
|
|
193
312
|
method: "POST",
|
|
194
|
-
url: "/v1/leads/
|
|
313
|
+
url: "/v1/leads/submit",
|
|
195
314
|
payload: {
|
|
196
|
-
kind: "
|
|
315
|
+
kind: "submit",
|
|
197
316
|
siteId: "site-abc",
|
|
198
317
|
source: "contact",
|
|
199
318
|
email: "dual@example.com",
|
|
@@ -211,14 +330,14 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
211
330
|
it("disabled — appendLeadRecord is skipped when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
|
|
212
331
|
process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
|
|
213
332
|
|
|
214
|
-
const serverGeneratedId = "
|
|
333
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a6";
|
|
215
334
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
216
335
|
|
|
217
336
|
const res = await app.inject({
|
|
218
337
|
method: "POST",
|
|
219
|
-
url: "/v1/leads/
|
|
338
|
+
url: "/v1/leads/submit",
|
|
220
339
|
payload: {
|
|
221
|
-
kind: "
|
|
340
|
+
kind: "submit",
|
|
222
341
|
siteId: "site-abc",
|
|
223
342
|
source: "contact",
|
|
224
343
|
email: "disabled@example.com",
|
|
@@ -242,9 +361,9 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
242
361
|
|
|
243
362
|
const res = await app.inject({
|
|
244
363
|
method: "POST",
|
|
245
|
-
url: "/v1/leads/
|
|
364
|
+
url: "/v1/leads/submit",
|
|
246
365
|
payload: {
|
|
247
|
-
kind: "
|
|
366
|
+
kind: "submit",
|
|
248
367
|
siteId: "site-abc",
|
|
249
368
|
source: "contact",
|
|
250
369
|
email: "nocapture@example.com",
|
|
@@ -261,15 +380,15 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
261
380
|
|
|
262
381
|
it("IP-include — metadata.ip present in capture record when LEAD_CAPTURE_INCLUDE_IP=true", async () => {
|
|
263
382
|
process.env.LEAD_CAPTURE_INCLUDE_IP = "true";
|
|
264
|
-
const serverGeneratedId = "
|
|
383
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a7";
|
|
265
384
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
266
385
|
|
|
267
386
|
const res = await app.inject({
|
|
268
387
|
method: "POST",
|
|
269
|
-
url: "/v1/leads/
|
|
388
|
+
url: "/v1/leads/submit",
|
|
270
389
|
headers: { "x-forwarded-for": "1.2.3.4" },
|
|
271
390
|
payload: {
|
|
272
|
-
kind: "
|
|
391
|
+
kind: "submit",
|
|
273
392
|
siteId: "site-abc",
|
|
274
393
|
source: "contact",
|
|
275
394
|
email: "ip@example.com",
|
|
@@ -286,11 +405,11 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
286
405
|
// ── buildInput branch coverage ─────────────────────────────────────────────
|
|
287
406
|
|
|
288
407
|
it("lead — payload absent from body defaults to empty object via ?? (payload ?? {} branch)", async () => {
|
|
289
|
-
const serverGeneratedId = "
|
|
408
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a8";
|
|
290
409
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
291
410
|
|
|
292
411
|
const bodyWithoutPayload: Record<string, unknown> = {
|
|
293
|
-
kind: "
|
|
412
|
+
kind: "submit",
|
|
294
413
|
siteId: "site-abc",
|
|
295
414
|
source: "contact",
|
|
296
415
|
email: "nopayload@example.com",
|
|
@@ -299,7 +418,7 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
299
418
|
|
|
300
419
|
const res = await app.inject({
|
|
301
420
|
method: "POST",
|
|
302
|
-
url: "/v1/leads/
|
|
421
|
+
url: "/v1/leads/submit",
|
|
303
422
|
payload: bodyWithoutPayload,
|
|
304
423
|
});
|
|
305
424
|
|
|
@@ -310,7 +429,7 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
310
429
|
});
|
|
311
430
|
|
|
312
431
|
it("callback — preferredTime included when provided (preferredTime spread branch)", async () => {
|
|
313
|
-
const serverGeneratedId = "
|
|
432
|
+
const serverGeneratedId = "callbk_00000000-0000-7000-8000-0000000000a9";
|
|
314
433
|
(prisma.callback.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
315
434
|
|
|
316
435
|
const res = await app.inject({
|
|
@@ -333,7 +452,7 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
333
452
|
});
|
|
334
453
|
|
|
335
454
|
it("callback — payload absent from body defaults to empty object via ?? (callback payload ?? {} branch)", async () => {
|
|
336
|
-
const serverGeneratedId = "
|
|
455
|
+
const serverGeneratedId = "callbk_00000000-0000-7000-8000-0000000000aa";
|
|
337
456
|
(prisma.callback.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
338
457
|
|
|
339
458
|
const bodyWithoutPayload: Record<string, unknown> = {
|
|
@@ -361,14 +480,14 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
361
480
|
it("email skipped — emailKind=skipped logged when env vars are unset", async () => {
|
|
362
481
|
delete process.env.AILK_LEAD_NOTIFICATION_TO;
|
|
363
482
|
delete process.env.AILK_LEAD_NOTIFICATION_FROM;
|
|
364
|
-
const serverGeneratedId = "
|
|
483
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000ab";
|
|
365
484
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
366
485
|
|
|
367
486
|
const res = await app.inject({
|
|
368
487
|
method: "POST",
|
|
369
|
-
url: "/v1/leads/
|
|
488
|
+
url: "/v1/leads/submit",
|
|
370
489
|
payload: {
|
|
371
|
-
kind: "
|
|
490
|
+
kind: "submit",
|
|
372
491
|
siteId: "site-abc",
|
|
373
492
|
source: "contact",
|
|
374
493
|
email: "skipped@example.com",
|
|
@@ -384,20 +503,178 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
384
503
|
}
|
|
385
504
|
});
|
|
386
505
|
|
|
506
|
+
// ── #4985 — step-flow envelope ({ siteId, source, payload: <record> }) ────
|
|
507
|
+
// StepFlow (packages/ui/src/sections/StepFlow.tsx L548) posts email INSIDE
|
|
508
|
+
// `payload`, not top-level. buildInput() now lifts `email` from `payload`
|
|
509
|
+
// when the top-level field is absent.
|
|
510
|
+
|
|
511
|
+
it("envelope submit — { siteId, source, payload: { email, ... } } returns 201, writes one Lead with that email and the full payload, and routes the notification email", async () => {
|
|
512
|
+
const { sendLeadNotification } = jest.requireMock("@working-theory/email");
|
|
513
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000e1";
|
|
514
|
+
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
515
|
+
|
|
516
|
+
const envelopePayload = {
|
|
517
|
+
email: "envelope@example.com",
|
|
518
|
+
name: "Envelope Visitor",
|
|
519
|
+
message: "hello from the step-flow",
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const res = await app.inject({
|
|
523
|
+
method: "POST",
|
|
524
|
+
url: "/v1/leads/submit",
|
|
525
|
+
payload: {
|
|
526
|
+
siteId: "site-abc",
|
|
527
|
+
source: "step-flow",
|
|
528
|
+
payload: envelopePayload,
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
expect(res.statusCode).toBe(201);
|
|
533
|
+
const body = res.json();
|
|
534
|
+
expect(body.id).toBe(serverGeneratedId);
|
|
535
|
+
expect(prisma.lead.create).toHaveBeenCalledTimes(1);
|
|
536
|
+
expect(prisma.lead.create).toHaveBeenCalledWith({
|
|
537
|
+
data: {
|
|
538
|
+
siteId: "site-abc",
|
|
539
|
+
source: "step-flow",
|
|
540
|
+
email: "envelope@example.com",
|
|
541
|
+
payload: envelopePayload,
|
|
542
|
+
},
|
|
543
|
+
select: { id: true },
|
|
544
|
+
});
|
|
545
|
+
expect(sendLeadNotification).toHaveBeenCalledTimes(1);
|
|
546
|
+
expect(body.email).toHaveProperty("ok", true);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("envelope subscribe — { siteId, source, payload: { email } } returns 201 and writes one Subscriber", async () => {
|
|
550
|
+
const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000e2";
|
|
551
|
+
(prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
552
|
+
|
|
553
|
+
const res = await app.inject({
|
|
554
|
+
method: "POST",
|
|
555
|
+
url: "/v1/leads/subscribe",
|
|
556
|
+
payload: {
|
|
557
|
+
siteId: "site-abc",
|
|
558
|
+
source: "step-flow",
|
|
559
|
+
payload: { email: "sub-envelope@example.com" },
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
expect(res.statusCode).toBe(201);
|
|
564
|
+
const body = res.json();
|
|
565
|
+
expect(body.id).toBe(serverGeneratedId);
|
|
566
|
+
expect(prisma.subscriber.create).toHaveBeenCalledTimes(1);
|
|
567
|
+
expect(prisma.subscriber.create).toHaveBeenCalledWith({
|
|
568
|
+
data: {
|
|
569
|
+
siteId: "site-abc",
|
|
570
|
+
email: "sub-envelope@example.com",
|
|
571
|
+
source: "step-flow",
|
|
572
|
+
},
|
|
573
|
+
select: { id: true },
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("envelope submit — email absent in both the top level and payload → the existing 400 invalid_input", async () => {
|
|
578
|
+
const res = await app.inject({
|
|
579
|
+
method: "POST",
|
|
580
|
+
url: "/v1/leads/submit",
|
|
581
|
+
payload: {
|
|
582
|
+
siteId: "site-abc",
|
|
583
|
+
source: "step-flow",
|
|
584
|
+
payload: { message: "no email anywhere" },
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
expect(res.statusCode).toBe(400);
|
|
589
|
+
const body = res.json();
|
|
590
|
+
expect(body.error).toBe("invalid_input");
|
|
591
|
+
expect(prisma.lead.create).not.toHaveBeenCalled();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("envelope subscribe — email absent in both the top level and payload → the existing 400 invalid_input", async () => {
|
|
595
|
+
const res = await app.inject({
|
|
596
|
+
method: "POST",
|
|
597
|
+
url: "/v1/leads/subscribe",
|
|
598
|
+
payload: {
|
|
599
|
+
siteId: "site-abc",
|
|
600
|
+
source: "step-flow",
|
|
601
|
+
payload: {},
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
expect(res.statusCode).toBe(400);
|
|
606
|
+
const body = res.json();
|
|
607
|
+
expect(body.error).toBe("invalid_input");
|
|
608
|
+
expect(prisma.subscriber.create).not.toHaveBeenCalled();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("submit — a top-level email wins over a payload email", async () => {
|
|
612
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000e3";
|
|
613
|
+
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
614
|
+
|
|
615
|
+
const res = await app.inject({
|
|
616
|
+
method: "POST",
|
|
617
|
+
url: "/v1/leads/submit",
|
|
618
|
+
payload: {
|
|
619
|
+
siteId: "site-abc",
|
|
620
|
+
source: "step-flow",
|
|
621
|
+
email: "top-level@example.com",
|
|
622
|
+
payload: { email: "shadowed-payload@example.com", message: "hi" },
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
expect(res.statusCode).toBe(201);
|
|
627
|
+
expect(prisma.lead.create).toHaveBeenCalledWith({
|
|
628
|
+
data: {
|
|
629
|
+
siteId: "site-abc",
|
|
630
|
+
source: "step-flow",
|
|
631
|
+
email: "top-level@example.com",
|
|
632
|
+
payload: { email: "shadowed-payload@example.com", message: "hi" },
|
|
633
|
+
},
|
|
634
|
+
select: { id: true },
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("subscribe — a top-level email wins over a payload email", async () => {
|
|
639
|
+
const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000e4";
|
|
640
|
+
(prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
641
|
+
|
|
642
|
+
const res = await app.inject({
|
|
643
|
+
method: "POST",
|
|
644
|
+
url: "/v1/leads/subscribe",
|
|
645
|
+
payload: {
|
|
646
|
+
siteId: "site-abc",
|
|
647
|
+
source: "step-flow",
|
|
648
|
+
email: "top-level-sub@example.com",
|
|
649
|
+
payload: { email: "shadowed-sub@example.com" },
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(res.statusCode).toBe(201);
|
|
654
|
+
expect(prisma.subscriber.create).toHaveBeenCalledWith({
|
|
655
|
+
data: {
|
|
656
|
+
siteId: "site-abc",
|
|
657
|
+
email: "top-level-sub@example.com",
|
|
658
|
+
source: "step-flow",
|
|
659
|
+
},
|
|
660
|
+
select: { id: true },
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
387
664
|
it("email error — emailKind=error logged when sendLeadNotification returns ok:false", async () => {
|
|
388
665
|
const { sendLeadNotification } = jest.requireMock("@working-theory/email");
|
|
389
666
|
(sendLeadNotification as jest.Mock).mockResolvedValueOnce({
|
|
390
667
|
ok: false,
|
|
391
668
|
error: { kind: "send_error", message: "SMTP timeout" },
|
|
392
669
|
});
|
|
393
|
-
const serverGeneratedId = "
|
|
670
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000ac";
|
|
394
671
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
395
672
|
|
|
396
673
|
const res = await app.inject({
|
|
397
674
|
method: "POST",
|
|
398
|
-
url: "/v1/leads/
|
|
675
|
+
url: "/v1/leads/submit",
|
|
399
676
|
payload: {
|
|
400
|
-
kind: "
|
|
677
|
+
kind: "submit",
|
|
401
678
|
siteId: "site-abc",
|
|
402
679
|
source: "contact",
|
|
403
680
|
email: "erroremail@example.com",
|
|
@@ -413,3 +690,133 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
413
690
|
}
|
|
414
691
|
});
|
|
415
692
|
});
|
|
693
|
+
|
|
694
|
+
// ── #4985 — PII-safe logging still holds when email arrives via `payload` ──
|
|
695
|
+
|
|
696
|
+
describe("POST /v1/leads/submit — structured log stays PII-safe for envelope input", () => {
|
|
697
|
+
afterEach(async () => {
|
|
698
|
+
delete process.env.AILK_LEAD_NOTIFICATION_TO;
|
|
699
|
+
delete process.env.AILK_LEAD_NOTIFICATION_FROM;
|
|
700
|
+
delete process.env.DATABASE_URL;
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("the 'lead routed' log line carries no email or payload value when email is lifted from payload", async () => {
|
|
704
|
+
jest.clearAllMocks();
|
|
705
|
+
process.env.AILK_LEAD_NOTIFICATION_TO = "owner@example.com";
|
|
706
|
+
process.env.AILK_LEAD_NOTIFICATION_FROM = "noreply@example.com";
|
|
707
|
+
process.env.DATABASE_URL = "postgres://stub";
|
|
708
|
+
|
|
709
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000e5";
|
|
710
|
+
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
711
|
+
|
|
712
|
+
const logLines: string[] = [];
|
|
713
|
+
const logCapture = require("fastify")({
|
|
714
|
+
logger: {
|
|
715
|
+
level: "info",
|
|
716
|
+
stream: { write: (msg: string) => logLines.push(msg) },
|
|
717
|
+
},
|
|
718
|
+
}) as FastifyInstance;
|
|
719
|
+
await logCapture.register(leadSubmitRoute);
|
|
720
|
+
await logCapture.ready();
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
const res = await logCapture.inject({
|
|
724
|
+
method: "POST",
|
|
725
|
+
url: "/v1/leads/submit",
|
|
726
|
+
payload: {
|
|
727
|
+
siteId: "site-abc",
|
|
728
|
+
source: "step-flow",
|
|
729
|
+
payload: {
|
|
730
|
+
email: "secret-envelope@example.com",
|
|
731
|
+
message: "this message body must never be logged",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
expect(res.statusCode).toBe(201);
|
|
737
|
+
|
|
738
|
+
const routedLine = logLines.find((line) => line.includes("lead routed"));
|
|
739
|
+
expect(routedLine).toBeDefined();
|
|
740
|
+
expect(routedLine).not.toMatch(/secret-envelope@example\.com/);
|
|
741
|
+
expect(routedLine).not.toMatch(/this message body must never be logged/);
|
|
742
|
+
|
|
743
|
+
const logged = JSON.parse(routedLine!) as Record<string, unknown>;
|
|
744
|
+
expect(logged).not.toHaveProperty("email");
|
|
745
|
+
expect(logged).not.toHaveProperty("payload");
|
|
746
|
+
expect(logged).toMatchObject({
|
|
747
|
+
siteId: "site-abc",
|
|
748
|
+
kind: "submit",
|
|
749
|
+
persisted: "db",
|
|
750
|
+
});
|
|
751
|
+
} finally {
|
|
752
|
+
await logCapture.close();
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// ── D5 — registration binds the client's literal path (issue #3964) ───────────
|
|
758
|
+
//
|
|
759
|
+
// The drift class this issue fixed: every test above hand-supplied the same
|
|
760
|
+
// (wrong) URL the route was actually registered at, so the mismatch between
|
|
761
|
+
// the registered path and the path every consumer calls shipped silently.
|
|
762
|
+
// This suite registers the real exported plugin and asserts against the
|
|
763
|
+
// EXACT literal packages/api-client/src/leads.ts uses (cross-referenced
|
|
764
|
+
// below), so a future re-drift fails here instead of shipping.
|
|
765
|
+
|
|
766
|
+
describe("POST /v1/leads/submit — registration binds the client's literal path", () => {
|
|
767
|
+
let app: FastifyInstance;
|
|
768
|
+
|
|
769
|
+
beforeEach(async () => {
|
|
770
|
+
jest.clearAllMocks();
|
|
771
|
+
process.env.AILK_LEAD_NOTIFICATION_TO = "owner@example.com";
|
|
772
|
+
process.env.AILK_LEAD_NOTIFICATION_FROM = "noreply@example.com";
|
|
773
|
+
process.env.DATABASE_URL = "postgres://stub";
|
|
774
|
+
|
|
775
|
+
const fastify = require("fastify")();
|
|
776
|
+
await fastify.register(leadSubmitRoute);
|
|
777
|
+
app = fastify;
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
afterEach(async () => {
|
|
781
|
+
await app.close();
|
|
782
|
+
delete process.env.AILK_LEAD_NOTIFICATION_TO;
|
|
783
|
+
delete process.env.AILK_LEAD_NOTIFICATION_FROM;
|
|
784
|
+
delete process.env.DATABASE_URL;
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it("resolves POST /v1/leads/submit — the exact literal packages/api-client/src/leads.ts's leads.submit() posts to", async () => {
|
|
788
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000d1";
|
|
789
|
+
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
790
|
+
|
|
791
|
+
// Cross-reference: packages/api-client/src/leads.ts —
|
|
792
|
+
// submit: (payload) => post<LeadWriteResponse>("/v1/leads/submit", payload)
|
|
793
|
+
const res = await app.inject({
|
|
794
|
+
method: "POST",
|
|
795
|
+
url: "/v1/leads/submit",
|
|
796
|
+
payload: {
|
|
797
|
+
siteId: "site-abc",
|
|
798
|
+
source: "contact",
|
|
799
|
+
email: "d5@example.com",
|
|
800
|
+
payload: { message: "hello" },
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
expect(res.statusCode).toBe(201);
|
|
805
|
+
expect(res.json().id).toBe(serverGeneratedId);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("POST /v1/leads/lead — no longer registered (404); the pre-fix path", async () => {
|
|
809
|
+
const res = await app.inject({
|
|
810
|
+
method: "POST",
|
|
811
|
+
url: "/v1/leads/lead",
|
|
812
|
+
payload: {
|
|
813
|
+
siteId: "site-abc",
|
|
814
|
+
source: "contact",
|
|
815
|
+
email: "old-path@example.com",
|
|
816
|
+
payload: { message: "hello" },
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
expect(res.statusCode).toBe(404);
|
|
821
|
+
});
|
|
822
|
+
});
|
|
@@ -74,14 +74,14 @@ describe("POST /api/leads — track() telemetry", () => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
it("calls track('lead-captured') with source and locale on successful submission", async () => {
|
|
77
|
-
const serverGeneratedId = "
|
|
77
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b1";
|
|
78
78
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
79
79
|
|
|
80
80
|
const res = await app.inject({
|
|
81
81
|
method: "POST",
|
|
82
|
-
url: "/v1/leads/
|
|
82
|
+
url: "/v1/leads/submit",
|
|
83
83
|
payload: {
|
|
84
|
-
kind: "
|
|
84
|
+
kind: "submit",
|
|
85
85
|
siteId: "site-abc",
|
|
86
86
|
source: "newsletter",
|
|
87
87
|
email: "test@example.com",
|
|
@@ -108,9 +108,9 @@ describe("POST /api/leads — track() telemetry", () => {
|
|
|
108
108
|
|
|
109
109
|
const res = await app.inject({
|
|
110
110
|
method: "POST",
|
|
111
|
-
url: "/v1/leads/
|
|
111
|
+
url: "/v1/leads/submit",
|
|
112
112
|
payload: {
|
|
113
|
-
kind: "
|
|
113
|
+
kind: "submit",
|
|
114
114
|
siteId: "site-abc",
|
|
115
115
|
source: "contact",
|
|
116
116
|
email: "fail@example.com",
|
|
@@ -125,10 +125,10 @@ describe("POST /api/leads — track() telemetry", () => {
|
|
|
125
125
|
it("does NOT call track() when validation fails (missing required field → 400 or 503)", async () => {
|
|
126
126
|
const res = await app.inject({
|
|
127
127
|
method: "POST",
|
|
128
|
-
url: "/v1/leads/
|
|
128
|
+
url: "/v1/leads/submit",
|
|
129
129
|
payload: {
|
|
130
130
|
// missing email — required field for lead kind
|
|
131
|
-
kind: "
|
|
131
|
+
kind: "submit",
|
|
132
132
|
siteId: "site-abc",
|
|
133
133
|
source: "contact",
|
|
134
134
|
payload: {},
|
|
@@ -140,14 +140,14 @@ describe("POST /api/leads — track() telemetry", () => {
|
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it("track() props contain only source and locale — no PII fields", async () => {
|
|
143
|
-
const serverGeneratedId = "
|
|
143
|
+
const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b2";
|
|
144
144
|
(prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
|
|
145
145
|
|
|
146
146
|
const res = await app.inject({
|
|
147
147
|
method: "POST",
|
|
148
|
-
url: "/v1/leads/
|
|
148
|
+
url: "/v1/leads/submit",
|
|
149
149
|
payload: {
|
|
150
|
-
kind: "
|
|
150
|
+
kind: "submit",
|
|
151
151
|
siteId: "site-abc",
|
|
152
152
|
source: "landing-page",
|
|
153
153
|
email: "pii@example.com",
|