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
|
@@ -16,42 +16,65 @@ import {
|
|
|
16
16
|
routeLead,
|
|
17
17
|
} from "../../services/lead-routing.js";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Read a named string field from the body, falling back to the same-named
|
|
21
|
+
* field inside `body.payload` when the top-level value is absent — the
|
|
22
|
+
* step-flow envelope (`{ siteId, source, payload: <record> }`) carries its
|
|
23
|
+
* identity fields inside `payload` rather than at the top level. A top-level
|
|
24
|
+
* value always wins.
|
|
25
|
+
*/
|
|
26
|
+
function liftString(
|
|
27
|
+
body: Record<string, unknown>,
|
|
28
|
+
payload: Record<string, unknown>,
|
|
29
|
+
key: string,
|
|
30
|
+
): string {
|
|
31
|
+
const topLevel = body[key];
|
|
32
|
+
if (typeof topLevel === "string") return topLevel;
|
|
33
|
+
const fromPayload = payload[key];
|
|
34
|
+
return (typeof fromPayload === "string" ? fromPayload : topLevel) as string;
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
/**
|
|
20
38
|
* Build a typed RouteLeadInput from the raw request body for a given kind.
|
|
21
39
|
* Explicit destructure per kind prevents unknown fields (e.g. attacker-supplied
|
|
22
40
|
* `id`) from reaching the service — the service's .strict() Zod schemas provide
|
|
23
|
-
* a second layer, but we also keep the handler clean at the boundary.
|
|
41
|
+
* a second layer, but we also keep the handler clean at the boundary. Lifting
|
|
42
|
+
* `email`/`name`/`phone` out of `payload` (see liftString above) reads by
|
|
43
|
+
* explicit field name only — it does not spread `payload`'s other keys, so
|
|
44
|
+
* the same discipline holds. `payload` itself is passed through unchanged.
|
|
24
45
|
*/
|
|
25
46
|
function buildInput(
|
|
26
47
|
kind: LeadKind,
|
|
27
48
|
body: Record<string, unknown>,
|
|
28
49
|
): RouteLeadInput {
|
|
29
|
-
|
|
50
|
+
const payload = (body["payload"] ?? {}) as Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
if (kind === "submit") {
|
|
30
53
|
return {
|
|
31
54
|
kind,
|
|
32
55
|
siteId: body["siteId"] as string,
|
|
33
56
|
source: body["source"] as string,
|
|
34
|
-
email: body
|
|
35
|
-
payload
|
|
57
|
+
email: liftString(body, payload, "email"),
|
|
58
|
+
payload,
|
|
36
59
|
};
|
|
37
60
|
}
|
|
38
61
|
if (kind === "subscribe") {
|
|
39
62
|
return {
|
|
40
63
|
kind,
|
|
41
64
|
siteId: body["siteId"] as string,
|
|
42
|
-
email: body
|
|
65
|
+
email: liftString(body, payload, "email"),
|
|
43
66
|
source: body["source"] as string,
|
|
44
67
|
};
|
|
45
68
|
}
|
|
46
69
|
return {
|
|
47
70
|
kind,
|
|
48
71
|
siteId: body["siteId"] as string,
|
|
49
|
-
name: body
|
|
50
|
-
phone: body
|
|
72
|
+
name: liftString(body, payload, "name"),
|
|
73
|
+
phone: liftString(body, payload, "phone"),
|
|
51
74
|
...(body["preferredTime"] !== undefined && {
|
|
52
75
|
preferredTime: body["preferredTime"] as string,
|
|
53
76
|
}),
|
|
54
|
-
payload
|
|
77
|
+
payload,
|
|
55
78
|
};
|
|
56
79
|
}
|
|
57
80
|
|
|
@@ -59,6 +82,17 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
59
82
|
return async (fastify) => {
|
|
60
83
|
fastify.post(`/v1/leads/${kind}`, async (request, reply) => {
|
|
61
84
|
const body = request.body as Record<string, unknown>;
|
|
85
|
+
// request.ip became attacker-influenceable when trustProxy: true went
|
|
86
|
+
// instance-wide (server.ts, #4730/#4778) — leads/index.ts was never
|
|
87
|
+
// itself touched by that PR. Traced every consumer of this value
|
|
88
|
+
// (#5112, DEF-004): lead-routing.ts's structured log field and its
|
|
89
|
+
// opt-in (LEAD_CAPTURE_INCLUDE_IP) persisted metadata.ip both RECORD
|
|
90
|
+
// the value; neither branches routing, persistence, or dedup on it.
|
|
91
|
+
// The best-effort/record-only posture is judged sufficient — the same
|
|
92
|
+
// trade-off server.ts's own trustProxy comment already accepts. The
|
|
93
|
+
// private @working-theory/lead-routing package (not present in this
|
|
94
|
+
// repo, not inspectable) is the one consumer this trace could not
|
|
95
|
+
// complete; see docs/specs/5112-leads-ip-trace-spec.md §8.
|
|
62
96
|
const sourceMeta = {
|
|
63
97
|
ip: request.ip,
|
|
64
98
|
userAgent: request.headers["user-agent"],
|
|
@@ -125,6 +159,6 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
125
159
|
};
|
|
126
160
|
}
|
|
127
161
|
|
|
128
|
-
export const leadSubmitRoute = makeLeadsRoute("
|
|
162
|
+
export const leadSubmitRoute = makeLeadsRoute("submit");
|
|
129
163
|
export const leadSubscribeRoute = makeLeadsRoute("subscribe");
|
|
130
164
|
export const leadCallbackRoute = makeLeadsRoute("callback");
|
package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file configured-application.test.ts
|
|
3
|
+
*
|
|
4
|
+
* THE ACCEPTANCE GATE for the two halves of the application the SITE now
|
|
5
|
+
* decides (#5048 D5/D6), plus the control that deliberately does not ship
|
|
6
|
+
* (D7).
|
|
7
|
+
*
|
|
8
|
+
* #5036 validated a fixed payload against a fixed schema: ten words of
|
|
9
|
+
* headline, twenty-five of one-liner, and every one of eleven fields
|
|
10
|
+
* required. Both were one consuming site's editorial decisions compiled into
|
|
11
|
+
* the published package, and the second was worse than a preference — steps
|
|
12
|
+
* are individually optional, so a field whose step a site never renders
|
|
13
|
+
* cannot be supplied, and requiring it refuses a valid application with a
|
|
14
|
+
* message naming a question the founder was never shown.
|
|
15
|
+
*
|
|
16
|
+
* Both now derive from the site's SERVER-side `SiteListingConfig`
|
|
17
|
+
* (`wordCaps`, `collectedFields`), and the schemas are compiled per request
|
|
18
|
+
* by `buildProjectListingDraftPatchSchema` / `buildProjectListingSubmitSchema`.
|
|
19
|
+
*
|
|
20
|
+
* The tests that would fail QUIETLY, and are the reason this file exists:
|
|
21
|
+
* - a configured cap that is not actually consulted looks identical to one
|
|
22
|
+
* that is, until a site sets three and the tenth word still passes — so
|
|
23
|
+
* each cap assertion pins the SITE's number and shows the value AT the
|
|
24
|
+
* cap is accepted, and a default-config pair proves the previous
|
|
25
|
+
* behaviour is unchanged for a site that says nothing;
|
|
26
|
+
* - a site that does not collect `address` submitting successfully is the
|
|
27
|
+
* core regression guard for D5: the fixed schema #5036 shipped would
|
|
28
|
+
* refuse it, with `address` named as missing;
|
|
29
|
+
* - no honeypot and no start-to-submit timing check exists anywhere on this
|
|
30
|
+
* surface — #316 D6b removed both and #5039 deliberately keeps that
|
|
31
|
+
* channel open, so their ABSENCE is worth pinning before someone
|
|
32
|
+
* helpfully re-adds one.
|
|
33
|
+
*
|
|
34
|
+
* Same harness as `drafts.test.ts`: real routes, real services, mocked
|
|
35
|
+
* `lib/prisma.js` over a small in-memory row store, `routeLead` running for
|
|
36
|
+
* real against a per-test temp path.
|
|
37
|
+
*/
|
|
38
|
+
import { readFileSync } from "node:fs";
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock
|
|
42
|
+
jest.mock("../../../lib/prisma.js");
|
|
43
|
+
|
|
44
|
+
jest.mock("@working-theory/email", () => ({
|
|
45
|
+
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
jest.mock("@working-theory/license", () => ({
|
|
49
|
+
checkLicense: jest.fn().mockResolvedValue({
|
|
50
|
+
tier: "free",
|
|
51
|
+
plan: null,
|
|
52
|
+
features: [],
|
|
53
|
+
expiresAt: null,
|
|
54
|
+
revoked: false,
|
|
55
|
+
}),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
import Fastify from "fastify";
|
|
59
|
+
import type { FastifyInstance } from "fastify";
|
|
60
|
+
import {
|
|
61
|
+
projectListingApplicationFieldsSchema,
|
|
62
|
+
projectListingStartSchema,
|
|
63
|
+
SITE_LISTING_CONFIG_DEFAULTS,
|
|
64
|
+
} from "@working-theory/validation";
|
|
65
|
+
|
|
66
|
+
import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
|
|
67
|
+
import { prisma } from "../../../lib/prisma.js";
|
|
68
|
+
import { patchProjectListingDraftRoute } from "../patch-draft.js";
|
|
69
|
+
import { startProjectListingRoute } from "../start.js";
|
|
70
|
+
import { submitProjectListingRoute } from "../submit.js";
|
|
71
|
+
|
|
72
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const SITE_ID = "site-1";
|
|
75
|
+
const SITE_KEY = "pk-site-1";
|
|
76
|
+
|
|
77
|
+
const opt = (value: string, label: string) => ({ value, label });
|
|
78
|
+
|
|
79
|
+
const VALID_QUESTIONS = {
|
|
80
|
+
q2: {
|
|
81
|
+
prompt: "Why are you here?",
|
|
82
|
+
options: [opt("scale", "Scaling my team"), opt("just-curious", "Just curious")],
|
|
83
|
+
},
|
|
84
|
+
q3: { prompt: "Stage?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
|
|
85
|
+
q4: { prompt: "Team size?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
|
|
86
|
+
q5: {
|
|
87
|
+
prompt: "Urgency?",
|
|
88
|
+
options: [opt("urgent", "Now"), opt("planning", "Soon"), opt("exploring", "Someday")],
|
|
89
|
+
},
|
|
90
|
+
q6: {
|
|
91
|
+
prompt: "Pain?",
|
|
92
|
+
options: [
|
|
93
|
+
opt("low", "Low"),
|
|
94
|
+
opt("medium", "Medium"),
|
|
95
|
+
opt("high", "High"),
|
|
96
|
+
opt("critical", "Critical"),
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
q7: { prompt: "Anything else?" },
|
|
100
|
+
extras: [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const START_PAYLOAD = {
|
|
104
|
+
siteKey: SITE_KEY,
|
|
105
|
+
founderName: "Ada Founder",
|
|
106
|
+
founderEmail: "ada@example.com",
|
|
107
|
+
consent: true,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Everything the ten steps after step one collect. */
|
|
111
|
+
const COMPLETE_APPLICATION = {
|
|
112
|
+
name: "My Project",
|
|
113
|
+
headline: "Ten words or fewer for the tile title here",
|
|
114
|
+
oneLiner: "A short sentence describing what the project does for its people.",
|
|
115
|
+
problem: "The problem is real and this describes it.",
|
|
116
|
+
product: "wireframes",
|
|
117
|
+
commitment: "part-time",
|
|
118
|
+
teamComposition: "co-founders",
|
|
119
|
+
faqs: [
|
|
120
|
+
{ question: "Q one?", answer: "A one." },
|
|
121
|
+
{ question: "Q two?", answer: "A two." },
|
|
122
|
+
{ question: "Q three?", answer: "A three." },
|
|
123
|
+
],
|
|
124
|
+
address: "1 Founder Way, Springfield",
|
|
125
|
+
questions: VALID_QUESTIONS,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** The same application with no `address` — the D5 case. */
|
|
129
|
+
const { address: _address, ...APPLICATION_WITHOUT_ADDRESS } = COMPLETE_APPLICATION;
|
|
130
|
+
|
|
131
|
+
/** Every collected field except `address`, in the shape an operator writes. */
|
|
132
|
+
const COLLECTED_WITHOUT_ADDRESS = SITE_LISTING_CONFIG_DEFAULTS.collectedFields.filter(
|
|
133
|
+
(field) => field !== "address",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
/** `n` whitespace-separated words. */
|
|
137
|
+
const words = (n: number) => Array.from({ length: n }, (_, i) => `word${i + 1}`).join(" ");
|
|
138
|
+
|
|
139
|
+
// ─── The in-memory row store the Prisma mock is backed by ───────────────────
|
|
140
|
+
|
|
141
|
+
type Row = Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
let rows: Row[];
|
|
144
|
+
let nextId: number;
|
|
145
|
+
/**
|
|
146
|
+
* The `SiteListingConfig.data` blob this site is running on, per test. `null`
|
|
147
|
+
* means NO ROW — the platform defaults, which is the "site that says nothing"
|
|
148
|
+
* half of every pair below.
|
|
149
|
+
*/
|
|
150
|
+
let configData: Record<string, unknown> | null;
|
|
151
|
+
|
|
152
|
+
function matches(row: Row, where: Record<string, unknown>): boolean {
|
|
153
|
+
return Object.entries(where).every(([key, value]) => {
|
|
154
|
+
if (key === "OR") {
|
|
155
|
+
return (value as Record<string, unknown>[]).some((clause) => matches(row, clause));
|
|
156
|
+
}
|
|
157
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date)) {
|
|
158
|
+
const clause = value as Record<string, unknown>;
|
|
159
|
+
if ("in" in clause) return (clause["in"] as unknown[]).includes(row[key]);
|
|
160
|
+
if ("not" in clause) return row[key] !== clause["not"];
|
|
161
|
+
if ("gt" in clause) return (row[key] as Date) > (clause["gt"] as Date);
|
|
162
|
+
if ("startsWith" in clause) {
|
|
163
|
+
return (
|
|
164
|
+
typeof row[key] === "string" &&
|
|
165
|
+
(row[key] as string).startsWith(clause["startsWith"] as string)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return row[key] === value;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function installStore(): void {
|
|
174
|
+
rows = [];
|
|
175
|
+
nextId = 1;
|
|
176
|
+
configData = null;
|
|
177
|
+
|
|
178
|
+
(prisma.lead.create as jest.Mock).mockImplementation(() =>
|
|
179
|
+
Promise.resolve({ id: `lead_00000000-0000-7000-8000-00000000000${nextId}` }),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// One delegate, two lookups (#5048 D2/D6): `where.publicKey` resolves the
|
|
183
|
+
// site key, `where.siteId` reads the config the schemas are compiled from.
|
|
184
|
+
(prisma.siteListingConfig.findUnique as jest.Mock).mockImplementation(
|
|
185
|
+
({ where }: { where: { siteId?: string; publicKey?: string } }) => {
|
|
186
|
+
if (where.publicKey !== undefined) {
|
|
187
|
+
return Promise.resolve(where.publicKey === SITE_KEY ? { siteId: SITE_ID } : null);
|
|
188
|
+
}
|
|
189
|
+
return Promise.resolve(configData === null ? null : { data: configData });
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// #5275 — the grant write `start` makes after the row. Append-only; it only
|
|
194
|
+
// needs to resolve here (drafts.test.ts asserts on what it is called with).
|
|
195
|
+
(prisma.consentGrant.createMany as jest.Mock).mockImplementation(
|
|
196
|
+
({ data }: { data: Row[] }) => Promise.resolve({ count: data.length }),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// #5275 — `start` writes the row and its grant in one interactive
|
|
200
|
+
// transaction; the mock hands the same store-backed client through.
|
|
201
|
+
(prisma.$transaction as jest.Mock).mockImplementation(
|
|
202
|
+
async (fn: (tx: typeof prisma) => Promise<unknown>) => fn(prisma),
|
|
203
|
+
);
|
|
204
|
+
(prisma.projectListing.create as jest.Mock).mockImplementation(({ data }: { data: Row }) => {
|
|
205
|
+
const now = new Date();
|
|
206
|
+
const row: Row = {
|
|
207
|
+
id: `prjl_00000000-0000-7000-8000-00000000000${nextId++}`,
|
|
208
|
+
slug: null,
|
|
209
|
+
name: null,
|
|
210
|
+
oneLiner: null,
|
|
211
|
+
problem: null,
|
|
212
|
+
url: null,
|
|
213
|
+
product: null,
|
|
214
|
+
commitment: null,
|
|
215
|
+
teamComposition: null,
|
|
216
|
+
faqs: null,
|
|
217
|
+
anythingElse: null,
|
|
218
|
+
address: null,
|
|
219
|
+
questions: null,
|
|
220
|
+
founderCompany: null,
|
|
221
|
+
leadId: null,
|
|
222
|
+
headline: null,
|
|
223
|
+
logoMarkUrl: null,
|
|
224
|
+
logoWordmarkUrl: null,
|
|
225
|
+
featured: false,
|
|
226
|
+
decidedAt: null,
|
|
227
|
+
decidedBy: null,
|
|
228
|
+
issueUrl: null,
|
|
229
|
+
experimentKey: null,
|
|
230
|
+
windowEndsAt: null,
|
|
231
|
+
createdAt: now,
|
|
232
|
+
updatedAt: now,
|
|
233
|
+
...data,
|
|
234
|
+
};
|
|
235
|
+
rows.push(row);
|
|
236
|
+
return Promise.resolve(row);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
(prisma.projectListing.findFirst as jest.Mock).mockImplementation(
|
|
240
|
+
({ where }: { where: Record<string, unknown> }) =>
|
|
241
|
+
Promise.resolve(rows.find((row) => matches(row, where)) ?? null),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
(prisma.projectListing.findMany as jest.Mock).mockImplementation(
|
|
245
|
+
({ where }: { where: Record<string, unknown> }) =>
|
|
246
|
+
Promise.resolve(rows.filter((row) => matches(row, where))),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
(prisma.projectListing.update as jest.Mock).mockImplementation(
|
|
250
|
+
({ where, data }: { where: Record<string, unknown>; data: Row }) => {
|
|
251
|
+
const row = rows.find((r) => matches(r, where));
|
|
252
|
+
Object.assign(row!, data, { updatedAt: new Date() });
|
|
253
|
+
return Promise.resolve(row);
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
(prisma.projectListing.updateMany as jest.Mock).mockImplementation(
|
|
258
|
+
({ where, data }: { where: Record<string, unknown>; data: Row }) => {
|
|
259
|
+
const matched = rows.filter((r) => matches(r, where));
|
|
260
|
+
matched.forEach((r) => Object.assign(r, data, { updatedAt: new Date() }));
|
|
261
|
+
return Promise.resolve({ count: matched.length });
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
const bearer = (token: string) => ({ authorization: `Bearer ${token}` });
|
|
269
|
+
|
|
270
|
+
async function buildApp(): Promise<FastifyInstance> {
|
|
271
|
+
const app = Fastify();
|
|
272
|
+
await app.register(startProjectListingRoute);
|
|
273
|
+
await app.register(patchProjectListingDraftRoute);
|
|
274
|
+
await app.register(submitProjectListingRoute);
|
|
275
|
+
await app.ready();
|
|
276
|
+
return app;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function startDraft(
|
|
280
|
+
app: FastifyInstance,
|
|
281
|
+
): Promise<{ id: string; resumeToken: string }> {
|
|
282
|
+
const res = await app.inject({
|
|
283
|
+
method: "POST",
|
|
284
|
+
url: "/v1/project-listings",
|
|
285
|
+
payload: START_PAYLOAD,
|
|
286
|
+
});
|
|
287
|
+
expect(res.statusCode).toBe(201);
|
|
288
|
+
return (res.json() as { data: { id: string; resumeToken: string } }).data;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const patch = (
|
|
292
|
+
app: FastifyInstance,
|
|
293
|
+
draft: { id: string; resumeToken: string },
|
|
294
|
+
payload: Record<string, unknown>,
|
|
295
|
+
) =>
|
|
296
|
+
app.inject({
|
|
297
|
+
method: "PATCH",
|
|
298
|
+
url: `/v1/project-listings/drafts/${draft.id}`,
|
|
299
|
+
headers: bearer(draft.resumeToken),
|
|
300
|
+
payload,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const submit = (app: FastifyInstance, draft: { id: string; resumeToken: string }) =>
|
|
304
|
+
app.inject({
|
|
305
|
+
method: "POST",
|
|
306
|
+
url: `/v1/project-listings/drafts/${draft.id}/submit`,
|
|
307
|
+
headers: bearer(draft.resumeToken),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const messageOf = (res: { json: () => unknown }) =>
|
|
311
|
+
(res.json() as { error: { message: string } }).error.message;
|
|
312
|
+
|
|
313
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
describe("the site's own application config (#5048 D5/D6)", () => {
|
|
316
|
+
setupLeadFileEnv("project-listing-configured");
|
|
317
|
+
let app: FastifyInstance;
|
|
318
|
+
|
|
319
|
+
beforeEach(async () => {
|
|
320
|
+
jest.clearAllMocks();
|
|
321
|
+
installStore();
|
|
322
|
+
app = await buildApp();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
afterEach(async () => {
|
|
326
|
+
await app.close();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ── D6 — the word caps are the site's numbers ──
|
|
330
|
+
|
|
331
|
+
describe("configured word caps (D6)", () => {
|
|
332
|
+
/** Three words of headline, five of one-liner — nobody's platform default. */
|
|
333
|
+
const TIGHT_CAPS = { wordCaps: { headline: 3, oneLiner: 5 } };
|
|
334
|
+
|
|
335
|
+
it("patch: refuses a headline over THIS SITE's cap, naming the site's number", async () => {
|
|
336
|
+
configData = TIGHT_CAPS;
|
|
337
|
+
const draft = await startDraft(app);
|
|
338
|
+
|
|
339
|
+
const res = await patch(app, draft, { headline: words(4) });
|
|
340
|
+
|
|
341
|
+
expect(res.statusCode).toBe(400);
|
|
342
|
+
// Pinned to the CONFIGURED number, not to a constant: a message naming
|
|
343
|
+
// ten here would mean the cap came from the package, and the four-word
|
|
344
|
+
// refusal would be a coincidence of the platform default rather than
|
|
345
|
+
// this site's terms being enforced.
|
|
346
|
+
expect(messageOf(res)).toBe("headline must be 3 words or fewer");
|
|
347
|
+
expect(messageOf(res)).not.toContain("10");
|
|
348
|
+
expect(rows[0]!["headline"]).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("patch: ACCEPTS a headline at exactly the site's cap", async () => {
|
|
352
|
+
// The other half of the pair. Without it the test above passes just as
|
|
353
|
+
// well against a surface that refuses every headline it is given.
|
|
354
|
+
configData = TIGHT_CAPS;
|
|
355
|
+
const draft = await startDraft(app);
|
|
356
|
+
|
|
357
|
+
const res = await patch(app, draft, { headline: words(3) });
|
|
358
|
+
|
|
359
|
+
expect(res.statusCode).toBe(200);
|
|
360
|
+
expect(rows[0]!["headline"]).toBe(words(3));
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("patch: enforces the site's oneLiner cap too, at its own number", async () => {
|
|
364
|
+
configData = TIGHT_CAPS;
|
|
365
|
+
const draft = await startDraft(app);
|
|
366
|
+
|
|
367
|
+
const over = await patch(app, draft, { oneLiner: words(6) });
|
|
368
|
+
const at = await patch(app, draft, { oneLiner: words(5) });
|
|
369
|
+
|
|
370
|
+
expect(over.statusCode).toBe(400);
|
|
371
|
+
expect(messageOf(over)).toBe("oneLiner must be 5 words or fewer");
|
|
372
|
+
expect(at.statusCode).toBe(200);
|
|
373
|
+
expect(rows[0]!["oneLiner"]).toBe(words(5));
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("patch: a site with NO config row keeps the previous ten-word headline behaviour", async () => {
|
|
377
|
+
// The defaults reproduce #5036's constants exactly, so a site that says
|
|
378
|
+
// nothing sees no change — which is the whole claim D6 makes about
|
|
379
|
+
// turning a constant into configuration.
|
|
380
|
+
configData = null;
|
|
381
|
+
const draft = await startDraft(app);
|
|
382
|
+
|
|
383
|
+
const ten = await patch(app, draft, { headline: words(10) });
|
|
384
|
+
const eleven = await patch(app, draft, { headline: words(11) });
|
|
385
|
+
|
|
386
|
+
expect(ten.statusCode).toBe(200);
|
|
387
|
+
expect(eleven.statusCode).toBe(400);
|
|
388
|
+
expect(messageOf(eleven)).toBe("headline must be 10 words or fewer");
|
|
389
|
+
// The eleventh word never landed on the row.
|
|
390
|
+
expect(rows[0]!["headline"]).toBe(words(10));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("patch: a site with NO config row keeps the previous twenty-five-word oneLiner behaviour", async () => {
|
|
394
|
+
configData = null;
|
|
395
|
+
const draft = await startDraft(app);
|
|
396
|
+
|
|
397
|
+
const twentyFive = await patch(app, draft, { oneLiner: words(25) });
|
|
398
|
+
const twentySix = await patch(app, draft, { oneLiner: words(26) });
|
|
399
|
+
|
|
400
|
+
expect(twentyFive.statusCode).toBe(200);
|
|
401
|
+
expect(twentySix.statusCode).toBe(400);
|
|
402
|
+
expect(messageOf(twentySix)).toBe("oneLiner must be 25 words or fewer");
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ── D5 — completeness derives from the collected-field set ──
|
|
407
|
+
|
|
408
|
+
describe("derived completeness (D5)", () => {
|
|
409
|
+
it("submit: a site that does not collect `address` accepts a draft that has none", async () => {
|
|
410
|
+
// THE regression guard. #5036's fixed schema required `address` of every
|
|
411
|
+
// site; a site whose step set never asks for one would have its founders
|
|
412
|
+
// refused at submit, naming a question they were never shown.
|
|
413
|
+
configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
|
|
414
|
+
const draft = await startDraft(app);
|
|
415
|
+
await patch(app, draft, APPLICATION_WITHOUT_ADDRESS);
|
|
416
|
+
|
|
417
|
+
const res = await submit(app, draft);
|
|
418
|
+
|
|
419
|
+
expect(res.statusCode).toBe(200);
|
|
420
|
+
expect(res.json()).toEqual({ ok: true, data: { id: draft.id, slug: "my-project" } });
|
|
421
|
+
expect(rows[0]!["status"]).toBe("requested");
|
|
422
|
+
// Never collected, so never stored — not silently defaulted to something.
|
|
423
|
+
expect(rows[0]!["address"]).toBeNull();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("submit: the DEFAULT config still refuses that same draft, naming `address`", async () => {
|
|
427
|
+
// The other half: the field is only optional because THIS SITE said so.
|
|
428
|
+
configData = null;
|
|
429
|
+
const draft = await startDraft(app);
|
|
430
|
+
await patch(app, draft, APPLICATION_WITHOUT_ADDRESS);
|
|
431
|
+
|
|
432
|
+
const res = await submit(app, draft);
|
|
433
|
+
|
|
434
|
+
expect(res.statusCode).toBe(422);
|
|
435
|
+
const body = res.json() as { error: { code: string; missingFields: string[] } };
|
|
436
|
+
expect(body.error.code).toBe("incomplete_application");
|
|
437
|
+
expect(body.error.missingFields).toEqual(["address"]);
|
|
438
|
+
expect(rows[0]!["status"]).toBe("draft");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("patch: REFUSES a write to a field the site does not collect", async () => {
|
|
442
|
+
// The collected set is a WRITE gate, not only a completeness floor.
|
|
443
|
+
// `address` rides the public read by design (#5036 D6), so a site whose
|
|
444
|
+
// step set omits the address step must not have one written into a draft
|
|
445
|
+
// by an anonymous caller who knows the field name — and then published on
|
|
446
|
+
// its listing page. Narrowing `submit` alone left that open, because a
|
|
447
|
+
// value already on the row is simply carried.
|
|
448
|
+
configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
|
|
449
|
+
const draft = await startDraft(app);
|
|
450
|
+
|
|
451
|
+
const res = await patch(app, draft, { address: COMPLETE_APPLICATION.address });
|
|
452
|
+
|
|
453
|
+
expect(res.statusCode).toBe(400);
|
|
454
|
+
expect(rows[0]!["address"]).toBeNull();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("submit: a value ALREADY on the row for an uncollected field does not block submit", async () => {
|
|
458
|
+
// The other direction, and the reason `buildProjectListingSubmitSchema`
|
|
459
|
+
// DROPS an uncollected field rather than making it optional: a site that
|
|
460
|
+
// narrows its step set after a draft was opened must not strand that
|
|
461
|
+
// draft. The write gate above stops NEW writes; this stops an existing
|
|
462
|
+
// value from failing `.strict()`.
|
|
463
|
+
configData = null;
|
|
464
|
+
const draft = await startDraft(app);
|
|
465
|
+
await patch(app, draft, COMPLETE_APPLICATION); // address included
|
|
466
|
+
|
|
467
|
+
configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
|
|
468
|
+
const res = await submit(app, draft);
|
|
469
|
+
|
|
470
|
+
expect(res.statusCode).toBe(200);
|
|
471
|
+
expect(rows[0]!["status"]).toBe("requested");
|
|
472
|
+
expect(rows[0]!["address"]).toBe(COMPLETE_APPLICATION.address);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("submit: a collected-but-OPTIONAL field does not block submit when absent", async () => {
|
|
476
|
+
configData = null;
|
|
477
|
+
// These three are collected by default and still optional — the
|
|
478
|
+
// distinction the missing-field list turns on.
|
|
479
|
+
for (const field of ["url", "anythingElse", "founderCompany"]) {
|
|
480
|
+
expect(SITE_LISTING_CONFIG_DEFAULTS.collectedFields).toContain(field);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const draft = await startDraft(app);
|
|
484
|
+
await patch(app, draft, COMPLETE_APPLICATION); // carries none of the three
|
|
485
|
+
|
|
486
|
+
const res = await submit(app, draft);
|
|
487
|
+
|
|
488
|
+
expect(res.statusCode).toBe(200);
|
|
489
|
+
expect(rows[0]!["url"]).toBeNull();
|
|
490
|
+
expect(rows[0]!["anythingElse"]).toBeNull();
|
|
491
|
+
expect(rows[0]!["founderCompany"]).toBeNull();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ── D7 — the controls that deliberately do not ship ──
|
|
496
|
+
|
|
497
|
+
describe("no anti-automation control ships (D7)", () => {
|
|
498
|
+
const read = (relative: string) =>
|
|
499
|
+
readFileSync(join(__dirname, "..", "..", "..", relative), "utf8");
|
|
500
|
+
|
|
501
|
+
const SURFACE_FILES = [
|
|
502
|
+
"routes/project-listings/start.ts",
|
|
503
|
+
"routes/project-listings/patch-draft.ts",
|
|
504
|
+
"routes/project-listings/public.ts",
|
|
505
|
+
"routes/project-listings/submit.ts",
|
|
506
|
+
"services/project-listings.ts",
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
it("no honeypot field exists in the payload the surface accepts", async () => {
|
|
510
|
+
// Asserted on the SCHEMAS, not on the prose: a decoy field would have to
|
|
511
|
+
// be accepted somewhere to be a honeypot at all.
|
|
512
|
+
const fields = [
|
|
513
|
+
...Object.keys(projectListingStartSchema.shape),
|
|
514
|
+
...Object.keys(projectListingApplicationFieldsSchema.shape),
|
|
515
|
+
];
|
|
516
|
+
for (const field of fields) {
|
|
517
|
+
expect(field).not.toMatch(/honey|_?trap|decoy|confirmEmail|website2/i);
|
|
518
|
+
}
|
|
519
|
+
// And nothing on the surface names one.
|
|
520
|
+
for (const file of SURFACE_FILES) {
|
|
521
|
+
expect(read(file)).not.toMatch(/honeypot/i);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("no timing heuristic gates the start-to-submit interval", async () => {
|
|
526
|
+
// #5039 deliberately opens the agent channel a "too fast to be human"
|
|
527
|
+
// check would close, and an agent completes a ten-step form in
|
|
528
|
+
// milliseconds. A named heuristic anywhere on the surface is the
|
|
529
|
+
// regression; so is `submit` reaching for a clock to compare the draft's
|
|
530
|
+
// own `createdAt` against.
|
|
531
|
+
for (const file of SURFACE_FILES) {
|
|
532
|
+
expect(read(file)).not.toMatch(
|
|
533
|
+
/tooFast|too_fast|minElapsed|minSeconds|minDuration|dwellTime|timeOnPage|humanTiming/i,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const service = read("services/project-listings.ts");
|
|
538
|
+
const submitBody = service.slice(
|
|
539
|
+
service.indexOf("export async function submitProjectListingDraft"),
|
|
540
|
+
);
|
|
541
|
+
const submitOnly = submitBody.slice(0, submitBody.indexOf("\nfunction isUniqueViolation"));
|
|
542
|
+
expect(submitOnly).not.toContain("createdAt");
|
|
543
|
+
expect(submitOnly).not.toContain("Date.now(");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("an application completed in the same millisecond as its draft still submits", async () => {
|
|
547
|
+
// The behavioural half: nothing about how fast the eleven fields arrived
|
|
548
|
+
// is consulted. This whole test runs inside one tick.
|
|
549
|
+
configData = null;
|
|
550
|
+
const draft = await startDraft(app);
|
|
551
|
+
await patch(app, draft, COMPLETE_APPLICATION);
|
|
552
|
+
|
|
553
|
+
const res = await submit(app, draft);
|
|
554
|
+
|
|
555
|
+
expect(res.statusCode).toBe(200);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|