create-ailk 0.23.0 → 0.25.1
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 +68 -33
- package/component-catalog.json +3 -3
- package/component-catalog.md +3 -3
- package/dist/add-module.js +10 -3
- package/dist/add-route.d.ts +12 -1
- package/dist/add-route.js +38 -4
- package/dist/cli.js +12 -7
- package/dist/derive-architecture.d.ts +3 -4
- package/dist/derive-architecture.js +75 -22
- package/dist/lib/design-checks.d.ts +9 -4
- package/dist/lib/design-checks.js +13 -7
- package/dist/lib/extract-module-bundle.js +29 -2
- package/dist/lib/fetch-template-snapshot.d.ts +1 -1
- package/dist/lib/fetch-template-snapshot.js +7 -3
- package/dist/lib/import-graph.d.ts +1 -1
- package/dist/lib/import-graph.js +4 -4
- package/dist/lib/module-license-gate.d.ts +12 -1
- package/dist/lib/module-license-gate.js +34 -12
- package/dist/lib/scaffold-render-sweep.d.ts +33 -0
- package/dist/lib/scaffold-render-sweep.js +45 -0
- package/dist/module-architecture.d.ts +20 -3
- package/dist/module-architecture.js +122 -538
- package/dist/paid-sections.d.ts +25 -1
- package/dist/paid-sections.js +88 -3
- package/dist/surfaces.js +10 -2
- package/dist/sync-routes.d.ts +30 -4
- package/dist/sync-routes.js +271 -18
- package/dist/template-record.d.ts +14 -4
- package/dist/template-record.js +23 -5
- package/dist/topology.d.ts +16 -3
- package/dist/topology.js +31 -19
- package/dist/update.d.ts +10 -1
- package/dist/update.js +540 -16
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/template-guard.sh +137 -0
- package/templates/.claude/rules/extending.md +106 -0
- package/templates/.claude/rules/loop-contract.md +2 -2
- package/templates/.claude/settings.json +11 -0
- package/templates/.env.example +9 -5
- package/templates/.github/workflows/ci.yml +7 -4
- package/templates/CLAUDE.md +15 -3
- package/templates/CONVENTIONS.md +8 -10
- package/templates/README.md +14 -13
- package/templates/apps/api/.env.example +9 -2
- package/templates/apps/api/CLAUDE.md +122 -82
- package/templates/apps/api/jest.config.cjs +15 -5
- package/templates/apps/api/package.json +16 -20
- package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
- package/templates/apps/api/src/index.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -4
- package/templates/apps/api/src/modules/commerce.ts +10 -0
- package/templates/apps/api/src/modules/content-write.ts +10 -0
- package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
- package/templates/apps/api/src/modules/scheduling.ts +10 -0
- package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
- package/templates/apps/api/src/modules/workspace.ts +10 -0
- package/templates/apps/api/src/server.ts +117 -1857
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/CLAUDE.md +77 -24
- package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
- package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
- package/templates/apps/mcp/jest.config.cjs +6 -1
- package/templates/apps/mcp/package.json +5 -12
- package/templates/apps/mcp/src/app.ts +8 -7
- package/templates/apps/mcp/src/index.ts +6 -34
- package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
- package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
- package/templates/apps/mcp/src/site.ts +12 -0
- package/templates/apps/web/.env.example +1 -1
- package/templates/apps/web/CLAUDE.md +21 -5
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +28 -7
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
- package/templates/apps/web/app/api/revalidate/route.ts +3 -3
- package/templates/apps/web/auth.ts +2 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
- package/templates/apps/web/lib/auth-email.ts +1 -1
- package/templates/apps/web/lib/magnet-routes.ts +3 -1
- package/templates/apps/web/lib/newsletter.actions.ts +9 -6
- package/templates/apps/web/lib/relay.ts +2 -2
- package/templates/apps/web/lib/release-scope.ts +47 -0
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
- package/templates/customizations.yaml +37 -0
- package/templates/database/prisma.config.ts +10 -14
- package/templates/database/seed-test.ts +2 -2
- package/templates/database/seed.ts +32 -5
- package/templates/package.json +12 -6
- package/templates/pnpm-workspace.yaml +0 -1
- package/templates/project.yaml +16 -8
- package/templates/tools/jest/reporters.cjs +65 -0
- package/templates/tools/lint-architecture.ts +757 -7
- package/templates/tsconfig.json +1 -2
- package/versions.json +48 -35
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
- package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
- package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
- package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
- package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
- package/templates/apps/api/src/bin/spokes.ts +0 -198
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
- package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
- package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
- package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
- package/templates/apps/api/src/config/index.ts +0 -596
- package/templates/apps/api/src/config/modules.ts +0 -83
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
- package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -292
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
- package/templates/apps/api/src/lib/agent-store.ts +0 -613
- package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
- package/templates/apps/api/src/lib/consent-store.ts +0 -363
- package/templates/apps/api/src/lib/content-adapter.ts +0 -112
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
- package/templates/apps/api/src/lib/error-serializer.ts +0 -164
- package/templates/apps/api/src/lib/errors.ts +0 -95
- package/templates/apps/api/src/lib/file-fallback.ts +0 -87
- package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
- package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
- package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
- package/templates/apps/api/src/lib/license-key-store.ts +0 -45
- package/templates/apps/api/src/lib/locales.ts +0 -201
- package/templates/apps/api/src/lib/outbound-http.ts +0 -127
- package/templates/apps/api/src/lib/prisma.ts +0 -21
- package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
- package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
- package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
- package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
- package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
- package/templates/apps/api/src/lib/site.ts +0 -254
- package/templates/apps/api/src/lib/slice-load.ts +0 -90
- package/templates/apps/api/src/lib/spoke-store.ts +0 -180
- package/templates/apps/api/src/lib/stripe.ts +0 -318
- package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
- package/templates/apps/api/src/middleware/auth.ts +0 -864
- package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
- package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -264
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
- package/templates/apps/api/src/openapi/index.ts +0 -18
- package/templates/apps/api/src/openapi/spec.ts +0 -1815
- package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
- package/templates/apps/api/src/routes/aeo/index.ts +0 -107
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
- package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
- package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
- package/templates/apps/api/src/routes/auth/user.ts +0 -305
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -325
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1300
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
- package/templates/apps/api/src/routes/checkout/index.ts +0 -34
- package/templates/apps/api/src/routes/checkout/session.ts +0 -171
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
- package/templates/apps/api/src/routes/consent/account.ts +0 -268
- package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
- package/templates/apps/api/src/routes/consent/index.ts +0 -26
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
- package/templates/apps/api/src/routes/content/get-page.ts +0 -268
- package/templates/apps/api/src/routes/content/index.ts +0 -49
- package/templates/apps/api/src/routes/content/list-pages.ts +0 -95
- package/templates/apps/api/src/routes/content/list-types.ts +0 -19
- package/templates/apps/api/src/routes/content/offerings.ts +0 -184
- package/templates/apps/api/src/routes/content/pricing.ts +0 -176
- package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
- package/templates/apps/api/src/routes/content/search.ts +0 -129
- package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
- package/templates/apps/api/src/routes/leads/index.ts +0 -573
- package/templates/apps/api/src/routes/leads/list.ts +0 -104
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -480
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -465
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
- package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
- package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
- package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
- package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
- package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
- package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
- package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
- package/templates/apps/api/src/routes/requests/index.ts +0 -54
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
- package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
- package/templates/apps/api/src/routes/webhooks/README.md +0 -398
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -448
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -808
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2026
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -528
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
- package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
- package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -867
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
- package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
- package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1176
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -444
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -154
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
- package/templates/apps/api/src/services/abandon-email.ts +0 -313
- package/templates/apps/api/src/services/aeo-score.ts +0 -325
- package/templates/apps/api/src/services/answer-distributions.ts +0 -138
- package/templates/apps/api/src/services/audit-log.ts +0 -961
- package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
- package/templates/apps/api/src/services/consent.ts +0 -834
- package/templates/apps/api/src/services/default-tenant.ts +0 -416
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -755
- package/templates/apps/api/src/services/dunning-policy.ts +0 -294
- package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
- package/templates/apps/api/src/services/entitlements.ts +0 -338
- package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
- package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
- package/templates/apps/api/src/services/lead-promotion.ts +0 -160
- package/templates/apps/api/src/services/lead-routing.ts +0 -1372
- package/templates/apps/api/src/services/leads-inbox.ts +0 -312
- package/templates/apps/api/src/services/listing-config.ts +0 -178
- package/templates/apps/api/src/services/magnet-completion.ts +0 -242
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -295
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -868
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -396
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -681
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
- package/templates/apps/api/src/services/offerings/catalog.ts +0 -308
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -385
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
- package/templates/apps/api/src/services/offerings/connection.ts +0 -1003
- package/templates/apps/api/src/services/offerings/index.ts +0 -614
- package/templates/apps/api/src/services/offerings/source.ts +0 -140
- package/templates/apps/api/src/services/offerings/store.ts +0 -150
- package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
- package/templates/apps/api/src/services/person.ts +0 -1188
- package/templates/apps/api/src/services/plan-offerings.ts +0 -125
- package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
- package/templates/apps/api/src/services/project-listings.ts +0 -2073
- package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
- package/templates/apps/api/src/services/request-confirm.ts +0 -698
- package/templates/apps/api/src/services/revalidation.ts +0 -235
- package/templates/apps/api/src/services/slack-notify.ts +0 -291
- package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
- package/templates/apps/api/src/services/usage-credits.ts +0 -340
- package/templates/apps/api/src/services/verified-email.ts +0 -71
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
- package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
- package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
- package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
- package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
- package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1151
- package/templates/apps/mcp/__tests__/index.test.ts +0 -195
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -65
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -187
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -165
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
- package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -228
- package/templates/apps/mcp/__tests__/locale.test.ts +0 -135
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
- package/templates/apps/mcp/__tests__/read_content.test.ts +0 -474
- package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -225
- package/templates/apps/mcp/__tests__/search_content.test.ts +0 -256
- package/templates/apps/mcp/__tests__/server.test.ts +0 -388
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -307
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -223
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
- package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
- package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
- package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
- package/templates/apps/mcp/src/abuse/index.ts +0 -342
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
- package/templates/apps/mcp/src/boot.ts +0 -167
- package/templates/apps/mcp/src/config/modules.ts +0 -43
- package/templates/apps/mcp/src/http-transport.ts +0 -526
- package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
- package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -142
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -139
- package/templates/apps/mcp/src/lib/errors.ts +0 -61
- package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
- package/templates/apps/mcp/src/lib/locales.ts +0 -171
- package/templates/apps/mcp/src/lib/prisma.ts +0 -39
- package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
- package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
- package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
- package/templates/apps/mcp/src/request-context.ts +0 -52
- package/templates/apps/mcp/src/server.ts +0 -343
- package/templates/apps/mcp/src/serverless-app.ts +0 -88
- package/templates/apps/mcp/src/tools/audit.ts +0 -127
- package/templates/apps/mcp/src/tools/capture_lead.ts +0 -64
- package/templates/apps/mcp/src/tools/create_project.ts +0 -134
- package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
- package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
- package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
- package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
- package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
- package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
- package/templates/apps/mcp/src/tools/index.ts +0 -137
- package/templates/apps/mcp/src/tools/list_pages.ts +0 -74
- package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
- package/templates/apps/mcp/src/tools/proposals.ts +0 -27
- package/templates/apps/mcp/src/tools/purchase.ts +0 -205
- package/templates/apps/mcp/src/tools/read_content.ts +0 -75
- package/templates/apps/mcp/src/tools/request_callback.ts +0 -59
- package/templates/apps/mcp/src/tools/search_content.ts +0 -67
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -57
- package/templates/apps/mcp/src/workspace-server.ts +0 -224
- package/templates/database/CHANGELOG.md +0 -961
- package/templates/database/LICENSE +0 -202
- package/templates/database/NOTICE +0 -2
- package/templates/database/README.md +0 -243
- package/templates/database/__tests__/agents.db.test.ts +0 -320
- package/templates/database/__tests__/appointment.db.test.ts +0 -106
- package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
- package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
- package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
- package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
- package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
- package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
- package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
- package/templates/database/__tests__/flows-presets.test.ts +0 -173
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
- package/templates/database/__tests__/member-area.db.test.ts +0 -257
- package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
- package/templates/database/__tests__/member-contract.db.test.ts +0 -413
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
- package/templates/database/__tests__/offerings-migration.test.ts +0 -197
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
- package/templates/database/__tests__/offerings.db.test.ts +0 -303
- package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
- package/templates/database/__tests__/person-contact.db.test.ts +0 -563
- package/templates/database/__tests__/person-notes.db.test.ts +0 -249
- package/templates/database/__tests__/person.db.test.ts +0 -586
- package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
- package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
- package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
- package/templates/database/__tests__/seed.test.ts +0 -240
- package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
- package/templates/database/__tests__/tenancy.test.ts +0 -141
- package/templates/database/__tests__/user-cascade.test.ts +0 -140
- package/templates/database/_gitignore +0 -8
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/auth/schema.prisma +0 -641
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/config/schema.prisma +0 -441
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/content/schema.prisma +0 -339
- package/templates/database/eslint.config.mjs +0 -69
- package/templates/database/flows/listing-diagnostic.ts +0 -182
- package/templates/database/flows/presets.ts +0 -315
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +0 -3
- package/templates/database/inbox/__tests__/schema.test.ts +0 -44
- package/templates/database/inbox/schema.prisma +0 -1447
- package/templates/database/jest.config.cjs +0 -36
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
- package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
- package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
- package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
- package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
- package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
- package/templates/database/migrations/migration_lock.toml +0 -3
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/ops/schema.prisma +0 -43
- package/templates/database/package.json +0 -89
- package/templates/database/schema.prisma +0 -71
- package/templates/database/scripts/db-generate-locked.sh +0 -80
- package/templates/database/scripts/person-backfill.sql +0 -132
- package/templates/database/tenancy.ts +0 -183
- package/templates/database/tsconfig.build.json +0 -16
- package/templates/database/tsconfig.flows.json +0 -15
- package/templates/database/tsconfig.json +0 -16
- package/templates/database/tsconfig.tenancy.json +0 -14
|
@@ -1,573 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Leads route group.
|
|
3
|
-
*
|
|
4
|
-
* Routes:
|
|
5
|
-
* POST /v1/leads/submit — lead submission (contact form / inquiry)
|
|
6
|
-
* POST /v1/leads/subscribe — newsletter subscription
|
|
7
|
-
* POST /v1/leads/callback — callback request (phone-back)
|
|
8
|
-
* GET /v1/leads/capture — whether this deployment accepts a contact
|
|
9
|
-
* message / a newsletter signup (#6730 D4)
|
|
10
|
-
*
|
|
11
|
-
* API↔MCP PARITY — `GET /v1/leads/capture` has NO MCP twin, a recorded
|
|
12
|
-
* exception on the same footing as the anonymous form-lifecycle routes. It
|
|
13
|
-
* exists so a web page can choose between the form and the contact email
|
|
14
|
-
* before rendering; an agent never needs that preflight, because the three
|
|
15
|
-
* write tools (`capture_lead`, `subscribe_newsletter`, `request_callback`)
|
|
16
|
-
* already return a typed `api_error` carrying the 503 when capture is not
|
|
17
|
-
* accepted — the same answer, at the moment it matters.
|
|
18
|
-
*
|
|
19
|
-
* TENANT RESOLUTION (#6203, spec §10.4 operator ruling — reverses this
|
|
20
|
-
* file's former byte-unchanged posture). `siteId` used to be taken VERBATIM
|
|
21
|
-
* from the request body (`body["siteId"] as string`) — an anonymous, no-
|
|
22
|
-
* ownership-check value (`packages/validation/src/schemas/leads.ts`'s
|
|
23
|
-
* `safeSiteIdString` permits a `wksp_<uuidv7>`-shaped literal). Every
|
|
24
|
-
* capture kind now resolves its tenant SERVER-SIDE, via
|
|
25
|
-
* `resolveSiteWorkspaceId()` (`lib/site.ts`) — the SAME alias -> workspace
|
|
26
|
-
* resolution `currentSiteId()`/`AILK_SITE_ID` already drive throughout this
|
|
27
|
-
* app, unconditional on the content adapter — and a client-posted `siteId`
|
|
28
|
-
* is IGNORED entirely: it can no longer place a row anywhere. An unresolved
|
|
29
|
-
* result (no DATABASE_URL, or the alias names no workspace this database
|
|
30
|
-
* holds) is a 503 before any write is attempted — never a guess. See
|
|
31
|
-
* `lead-routing.ts`'s own header for the persistence-side half.
|
|
32
|
-
*
|
|
33
|
-
* A REGISTERED SPOKE (#7532 D4) resolves FIRST: a hub serving several spokes
|
|
34
|
-
* reads the spoke's key from `x-ailk-spoke-key` through
|
|
35
|
-
* `resolveCallerSite()` (`services/public-site-resolver.ts`) and writes the
|
|
36
|
-
* lead into that spoke's own workspace. A refused key — malformed, unknown or
|
|
37
|
-
* revoked, or no key at all on a hub (a deployment whose spoke registry holds
|
|
38
|
-
* a row, or one whose `AILK_HUB_REQUIRE_SPOKE_KEY` is `true`; #7633) — is the
|
|
39
|
-
* same 503 `tenant_unresolved` before any write. A header-less request on a
|
|
40
|
-
* deployment with no registered spoke, or under the emergency opt-out
|
|
41
|
-
* (`AILK_HUB_REQUIRE_SPOKE_KEY=false`), takes the alias resolution above,
|
|
42
|
-
* unchanged. So does a keyless request whose hub-status read fails before the
|
|
43
|
-
* process has any answer: lead capture is never lost (#7633, conductor
|
|
44
|
-
* amendment to NFR3). The body's `siteId` stays ignored on every branch.
|
|
45
|
-
*
|
|
46
|
-
* IDEMPOTENCY KEY (#7869, roadmap D5). `POST /v1/leads/submit` accepts an
|
|
47
|
-
* optional `Idempotency-Key` header: 16 to 255 visible ASCII characters, and
|
|
48
|
-
* it must be UNGUESSABLE (a UUID, a Stripe Checkout Session id) — a key is
|
|
49
|
-
* first-write-wins inside its workspace, so a short or predictable one could
|
|
50
|
-
* be claimed by someone else first (SEC-7869-1). It is checked after the
|
|
51
|
-
* tenant gate and before `routeLead`; a malformed key is `400 invalid_input`
|
|
52
|
-
* with no read or write. A repeat of a stored key in the same workspace with
|
|
53
|
-
* the same `email`, `source` and `payload` answers the first lead's 201 and
|
|
54
|
-
* writes nothing; with anything different it is `422
|
|
55
|
-
* idempotency_key_reused`. The key dedupes only where a `Lead` row exists —
|
|
56
|
-
* `inbox` delivery — so a first write that fell back to email or a local
|
|
57
|
-
* file leaves nothing to find, and its retry writes again. `subscribe` and
|
|
58
|
-
* `callback` never read the header. The key is never logged or echoed.
|
|
59
|
-
*
|
|
60
|
-
* CONTACT TERMS ACCEPTANCE (#8730). A `submit` whose `source` is a
|
|
61
|
-
* consent-bearing source (`isConsentLeadSource` in @working-theory/validation:
|
|
62
|
-
* the contact sources `contact-form`, `conversational-intake`, `flow:contact`,
|
|
63
|
-
* and since #9130 the Free apply page's `flow:job-application`) must carry a
|
|
64
|
-
* top-level `consent: true` — the boolean, nothing else — or it is
|
|
65
|
-
* `400 consent_required` before the tenant read or any write. An accepted one
|
|
66
|
-
* gets `payload.consent = { statement, acceptedAt }` written HERE, replacing
|
|
67
|
-
* any `payload.consent` the client sent: `statement` is the platform's
|
|
68
|
-
* contact terms statement (`CONTACT_FORM_TERMS_STATEMENT`) and `acceptedAt`
|
|
69
|
-
* this request's receive time. The `consent` flag itself is never stored. A
|
|
70
|
-
* lead with any other source, `subscribe` and `callback` are unchanged. A
|
|
71
|
-
* direct caller can still send `consent: true`; the gate makes acceptance an
|
|
72
|
-
* explicit, recorded assertion, not proof that a person read the terms.
|
|
73
|
-
*
|
|
74
|
-
* API↔MCP PARITY for `consent` (#8730): no guard entry, for the reason the
|
|
75
|
-
* Idempotency-Key paragraph below gives — this route parses no validation
|
|
76
|
-
* export, so it binds as `api-args-unbound`. `capture_lead` stamps `mcp`, a
|
|
77
|
-
* source this check never reads, so the tool is unchanged.
|
|
78
|
-
*
|
|
79
|
-
* A MAGNET'S COMPLETION (#8757 D6). After a `flow:<slug>` submit is written
|
|
80
|
-
* to the database, `services/magnet-completion.ts` reads that LIVE magnet's
|
|
81
|
-
* completion setting. On `invite` the 201 gains `next: { completion:
|
|
82
|
-
* "invite" }` and — on a first write, never a replay — the submitter is sent
|
|
83
|
-
* one confirmation email whose link starts sign-in with their address
|
|
84
|
-
* prefilled (capped per recipient and per magnet, counted in the database);
|
|
85
|
-
* on `checkout`, re-checked against the `commerce` claim and the published
|
|
86
|
-
* offerings, it gains `next: { completion: "checkout", checkoutOffering }`.
|
|
87
|
-
* `lead`, a draft or missing magnet, or a caller with nowhere to sign in
|
|
88
|
-
* leaves the 201 as it was. A failed read or send never changes the 201,
|
|
89
|
-
* and the completion read has a time budget (`MAGNET_COMPLETION_BUDGET_MS`).
|
|
90
|
-
* `next` is reachable only through a `flow:` source: `capture_lead` posts
|
|
91
|
-
* `source: "mcp"`, so the tool's arguments get the same answer as before and
|
|
92
|
-
* no parity guard row changes (NFR5).
|
|
93
|
-
*
|
|
94
|
-
* A CHECKOUT'S ACCOUNT LINK (#8757 D14). A checkout fulfilment's submit
|
|
95
|
-
* (`source: "web"`, `payload.page: "checkout"`) carries the caller's account
|
|
96
|
-
* origin into `routeLead` as `accountOrigin`, resolved exactly as the invite
|
|
97
|
-
* link's (`accountTargetFor`): the spoke's enabled sign-in client's callback
|
|
98
|
-
* origin, or `FRONTEND_URL`'s on a keyless build with an account area. With
|
|
99
|
-
* none, or a failed read, it is left out and the buyer's purchase email goes
|
|
100
|
-
* without the link. No other submit makes this read, and the 201 is unchanged.
|
|
101
|
-
*
|
|
102
|
-
* API↔MCP PARITY for the header (#7869 D9, roadmap §12 Q5): NO guard entry.
|
|
103
|
-
* The guard binds a route's arguments to the validation export its handler
|
|
104
|
-
* `.safeParse`s on `request.params|query|body`; the header check below is a
|
|
105
|
-
* regex test, not such a parse, so this route still binds as
|
|
106
|
-
* `api-args-unbound` and the `capture_lead` row is unchanged. The 201 body's
|
|
107
|
-
* field set is unchanged too, and `capture_lead` sends no header. If the
|
|
108
|
-
* guard ever models headers, this becomes an API-side `arg-only-in`.
|
|
109
|
-
*/
|
|
110
|
-
|
|
111
|
-
import { track } from "@working-theory/analytics/server";
|
|
112
|
-
import {
|
|
113
|
-
CONTACT_FORM_TERMS_STATEMENT,
|
|
114
|
-
isConsentLeadSource,
|
|
115
|
-
type LeadSubmitNext,
|
|
116
|
-
} from "@working-theory/validation";
|
|
117
|
-
import type { FastifyPluginAsync } from "fastify";
|
|
118
|
-
|
|
119
|
-
import { acceptsKind, resolveLeadDelivery } from "../../lib/lead-delivery.js";
|
|
120
|
-
import { currentSiteId, resolveSiteWorkspaceId } from "../../lib/site.js";
|
|
121
|
-
import {
|
|
122
|
-
CONSENT_VISITOR_HEADER,
|
|
123
|
-
consentVisitorFromHeader,
|
|
124
|
-
} from "../../services/consent.js";
|
|
125
|
-
import {
|
|
126
|
-
type LeadKind,
|
|
127
|
-
type RouteLeadInput,
|
|
128
|
-
routeLead,
|
|
129
|
-
type SourceMeta,
|
|
130
|
-
} from "../../services/lead-routing.js";
|
|
131
|
-
import {
|
|
132
|
-
accountTargetFor,
|
|
133
|
-
magnetCompletionWithinBudget,
|
|
134
|
-
sendInviteConfirmation,
|
|
135
|
-
} from "../../services/magnet-completion.js";
|
|
136
|
-
import { resolveCallerSite } from "../../services/public-site-resolver.js";
|
|
137
|
-
import { sendRequestConfirmLink } from "../../services/request-confirm.js";
|
|
138
|
-
|
|
139
|
-
/** #7869 D1 — 16 to 255 visible ASCII characters; no spaces, no list. */
|
|
140
|
-
const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{16,255}$/;
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Read a named string field from the body, falling back to the same-named
|
|
144
|
-
* field inside `body.payload` when the top-level value is absent — the
|
|
145
|
-
* step-flow envelope (`{ siteId, source, payload: <record> }`) carries its
|
|
146
|
-
* identity fields inside `payload` rather than at the top level. A top-level
|
|
147
|
-
* value always wins.
|
|
148
|
-
*/
|
|
149
|
-
function liftString(
|
|
150
|
-
body: Record<string, unknown>,
|
|
151
|
-
payload: Record<string, unknown>,
|
|
152
|
-
key: string,
|
|
153
|
-
): string {
|
|
154
|
-
const topLevel = body[key];
|
|
155
|
-
if (typeof topLevel === "string") return topLevel;
|
|
156
|
-
const fromPayload = payload[key];
|
|
157
|
-
return (typeof fromPayload === "string" ? fromPayload : topLevel) as string;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** #8730, #9130 — the refusal a consent-bearing lead without terms acceptance gets. */
|
|
161
|
-
const CONSENT_REQUIRED_BODY = {
|
|
162
|
-
error: "consent_required",
|
|
163
|
-
message: "Accept the terms to send this form.",
|
|
164
|
-
} as const;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* #8730 D3, #9130 — the accepted terms, recorded by the server on a
|
|
168
|
-
* consent-bearing lead's payload, replacing any `consent` the client wrote
|
|
169
|
-
* there. Only a plain object payload is stamped: anything else is left for
|
|
170
|
-
* `routeLead`'s schema to refuse, never spread into an object that would then
|
|
171
|
-
* pass it.
|
|
172
|
-
*/
|
|
173
|
-
function withConsentRecord(payload: unknown, acceptedAt: string): unknown {
|
|
174
|
-
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
175
|
-
return payload;
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
...(payload as Record<string, unknown>),
|
|
179
|
-
consent: { statement: CONTACT_FORM_TERMS_STATEMENT, acceptedAt },
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Build a typed RouteLeadInput from the raw request body for a given kind.
|
|
185
|
-
* Explicit destructure per kind prevents unknown fields (e.g. attacker-supplied
|
|
186
|
-
* `id`) from reaching the service — the service's .strict() Zod schemas provide
|
|
187
|
-
* a second layer, but we also keep the handler clean at the boundary. Lifting
|
|
188
|
-
* `email`/`name`/`phone` out of `payload` (see liftString above) reads by
|
|
189
|
-
* explicit field name only — it does not spread `payload`'s other keys, so
|
|
190
|
-
* the same discipline holds. `payload` itself is passed through unchanged.
|
|
191
|
-
*
|
|
192
|
-
* `siteId` is the RESOLVED `workspaceId` (#6203) — never `body["siteId"]`.
|
|
193
|
-
* A body-supplied `siteId` is read nowhere in this function; a client that
|
|
194
|
-
* posts one has it silently ignored, never honoured.
|
|
195
|
-
*/
|
|
196
|
-
function buildInput(
|
|
197
|
-
kind: LeadKind,
|
|
198
|
-
body: Record<string, unknown>,
|
|
199
|
-
workspaceId: string,
|
|
200
|
-
receivedAt: string,
|
|
201
|
-
): RouteLeadInput {
|
|
202
|
-
const payload = (body["payload"] ?? {}) as Record<string, unknown>;
|
|
203
|
-
|
|
204
|
-
if (kind === "submit") {
|
|
205
|
-
return {
|
|
206
|
-
kind,
|
|
207
|
-
siteId: workspaceId,
|
|
208
|
-
source: body["source"] as string,
|
|
209
|
-
email: liftString(body, payload, "email"),
|
|
210
|
-
payload: isConsentLeadSource(body["source"])
|
|
211
|
-
? (withConsentRecord(payload, receivedAt) as Record<string, unknown>)
|
|
212
|
-
: payload,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
if (kind === "subscribe") {
|
|
216
|
-
return {
|
|
217
|
-
kind,
|
|
218
|
-
siteId: workspaceId,
|
|
219
|
-
email: liftString(body, payload, "email"),
|
|
220
|
-
source: body["source"] as string,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
return {
|
|
224
|
-
kind,
|
|
225
|
-
siteId: workspaceId,
|
|
226
|
-
name: liftString(body, payload, "name"),
|
|
227
|
-
phone: liftString(body, payload, "phone"),
|
|
228
|
-
...(body["preferredTime"] !== undefined && {
|
|
229
|
-
preferredTime: body["preferredTime"] as string,
|
|
230
|
-
}),
|
|
231
|
-
payload,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** A spoke checkout fulfilment's submit (#8757 D14): `source: "web"`, `payload.page: "checkout"`. */
|
|
236
|
-
function isCheckoutSubmit(body: Record<string, unknown>): boolean {
|
|
237
|
-
const payload = body["payload"];
|
|
238
|
-
return (
|
|
239
|
-
body["source"] === "web" &&
|
|
240
|
-
typeof payload === "object" &&
|
|
241
|
-
payload !== null &&
|
|
242
|
-
(payload as Record<string, unknown>)["page"] === "checkout"
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
247
|
-
return async (fastify) => {
|
|
248
|
-
fastify.post(`/v1/leads/${kind}`, async (request, reply) => {
|
|
249
|
-
const body = request.body as Record<string, unknown>;
|
|
250
|
-
|
|
251
|
-
// #8730 D1, #9130 — a contact lead or a job application carries the
|
|
252
|
-
// visitor's terms acceptance, or it is refused before the tenant read
|
|
253
|
-
// and any write.
|
|
254
|
-
if (
|
|
255
|
-
kind === "submit" &&
|
|
256
|
-
isConsentLeadSource(body?.["source"]) &&
|
|
257
|
-
body?.["consent"] !== true
|
|
258
|
-
) {
|
|
259
|
-
return reply.status(400).send(CONSENT_REQUIRED_BODY);
|
|
260
|
-
}
|
|
261
|
-
// request.ip became attacker-influenceable when trustProxy: true went
|
|
262
|
-
// instance-wide (server.ts, #4730/#4778) — leads/index.ts was never
|
|
263
|
-
// itself touched by that PR. Traced every consumer of this value
|
|
264
|
-
// (#5112, DEF-004): lead-routing.ts's structured log field and its
|
|
265
|
-
// opt-in (LEAD_CAPTURE_INCLUDE_IP) persisted metadata.ip both RECORD
|
|
266
|
-
// the value; neither branches routing, persistence, or dedup on it.
|
|
267
|
-
// The best-effort/record-only posture is judged sufficient — the same
|
|
268
|
-
// trade-off server.ts's own trustProxy comment already accepts. The
|
|
269
|
-
// private @working-theory/lead-routing package (not present in this
|
|
270
|
-
// repo, not inspectable) is the one consumer this trace could not
|
|
271
|
-
// complete; see docs/specs/5112-leads-ip-trace-spec.md §8.
|
|
272
|
-
const sourceMeta: SourceMeta = {
|
|
273
|
-
ip: request.ip,
|
|
274
|
-
userAgent: request.headers["user-agent"],
|
|
275
|
-
receivedAt: new Date().toISOString(),
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
// #7532 D4 — a registered spoke resolves first, to its own workspace; a
|
|
279
|
-
// refused key never falls through to the alias below. A keyless lead
|
|
280
|
-
// whose hub-status read fails on a cold start proceeds as on a non-hub,
|
|
281
|
-
// so routeLead's outage fallback captures it (#7633 amendment).
|
|
282
|
-
const caller = await resolveCallerSite(request, {
|
|
283
|
-
keylessOnRegistryFailure: "anonymous",
|
|
284
|
-
});
|
|
285
|
-
if (caller.kind === "refused") {
|
|
286
|
-
return reply.status(503).send({
|
|
287
|
-
error: "tenant_unresolved",
|
|
288
|
-
message: "This site is not configured to accept submissions.",
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// #6203 — resolve the tenant SERVER-SIDE before any write is
|
|
293
|
-
// attempted. Two branches, deliberately different:
|
|
294
|
-
//
|
|
295
|
-
// - No DATABASE_URL: the zero-config OSS file-fallback deployment.
|
|
296
|
-
// There is no `workspaces` table to resolve against (and
|
|
297
|
-
// `assertSiteAliasResolves()` already skips its own boot check for
|
|
298
|
-
// exactly this reason), so the "resolved" value here is the site
|
|
299
|
-
// ALIAS itself (`currentSiteId()`) — the same value the file
|
|
300
|
-
// fallback would have carried before this unit. Never a rejection:
|
|
301
|
-
// that would break lead capture for every zero-config scaffold.
|
|
302
|
-
// - DATABASE_URL set: resolve via `resolveSiteWorkspaceId()`, the
|
|
303
|
-
// SAME alias -> workspace resolution `currentSiteId()` already
|
|
304
|
-
// drives everywhere else in this app. `null` here is a genuine
|
|
305
|
-
// misconfiguration `build()` already refuses to boot past
|
|
306
|
-
// (`assertSiteAliasResolves()`) — reaching it at request time is
|
|
307
|
-
// the residual case that startup check cannot catch (a workspace
|
|
308
|
-
// deleted after boot, a transient read), and it fails closed the
|
|
309
|
-
// same way: reject, never guess (spec AC-4).
|
|
310
|
-
let workspaceId: string;
|
|
311
|
-
if (caller.kind === "spoke") {
|
|
312
|
-
workspaceId = caller.workspaceId;
|
|
313
|
-
sourceMeta.viaSpoke = true;
|
|
314
|
-
} else if (!process.env.DATABASE_URL) {
|
|
315
|
-
workspaceId = currentSiteId();
|
|
316
|
-
} else {
|
|
317
|
-
const resolved = await resolveSiteWorkspaceId();
|
|
318
|
-
if (resolved === null) {
|
|
319
|
-
request.log.error(
|
|
320
|
-
{ kind },
|
|
321
|
-
"lead capture: DATABASE_URL is set but the site alias resolves to no workspace",
|
|
322
|
-
);
|
|
323
|
-
return reply.status(503).send({
|
|
324
|
-
error: "tenant_unresolved",
|
|
325
|
-
message: "This site is not configured to accept submissions.",
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
workspaceId = resolved;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// #7869 D1 — after the tenant gate, before any read or write. Node
|
|
332
|
-
// lower-cases header names, and joins a duplicated header with ", ",
|
|
333
|
-
// which the pattern refuses.
|
|
334
|
-
let idempotencyKey: string | undefined;
|
|
335
|
-
if (kind === "submit") {
|
|
336
|
-
const rawKey = request.headers["idempotency-key"];
|
|
337
|
-
if (rawKey !== undefined) {
|
|
338
|
-
if (
|
|
339
|
-
typeof rawKey !== "string" ||
|
|
340
|
-
!IDEMPOTENCY_KEY_PATTERN.test(rawKey)
|
|
341
|
-
) {
|
|
342
|
-
return reply.status(400).send({
|
|
343
|
-
error: "invalid_input",
|
|
344
|
-
message:
|
|
345
|
-
"Idempotency-Key must be 16 to 255 visible ASCII characters.",
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
idempotencyKey = rawKey;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// #8185 D3 — a spoke's same-origin relay marks every forwarded call.
|
|
353
|
-
// The mark can only remove the spoke-checkout marker, never grant it.
|
|
354
|
-
sourceMeta.relayed = request.headers["x-ailk-relayed"] !== undefined;
|
|
355
|
-
|
|
356
|
-
// #8438 D11 — the cookie banner's visitor id, forwarded by the site's
|
|
357
|
-
// own server from its first-party cookie. Only a v4 UUID counts; the
|
|
358
|
-
// header is never echoed or logged. It links the visitor to the
|
|
359
|
-
// lead's Person after the write, and changes nothing about the answer.
|
|
360
|
-
const consentVisitorId = consentVisitorFromHeader(
|
|
361
|
-
request.headers[CONSENT_VISITOR_HEADER],
|
|
362
|
-
);
|
|
363
|
-
if (consentVisitorId !== undefined) {
|
|
364
|
-
sourceMeta.consentVisitorId = consentVisitorId;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// #8757 D14 — where a buyer's account lives, for the purchase email.
|
|
368
|
-
let accountOrigin: string | undefined;
|
|
369
|
-
if (kind === "submit" && isCheckoutSubmit(body)) {
|
|
370
|
-
const target = await accountTargetFor(caller).catch(() => null);
|
|
371
|
-
accountOrigin = target?.origin;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const result = await routeLead(
|
|
375
|
-
buildInput(kind, body, workspaceId, sourceMeta.receivedAt),
|
|
376
|
-
sourceMeta,
|
|
377
|
-
{
|
|
378
|
-
...(idempotencyKey !== undefined && { idempotencyKey }),
|
|
379
|
-
...(accountOrigin !== undefined && { accountOrigin }),
|
|
380
|
-
},
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
if (!result.ok) {
|
|
384
|
-
if (result.error.kind === "idempotency_conflict") {
|
|
385
|
-
// #7869 D6 — the operator's signal that a keyed lead was refused.
|
|
386
|
-
// Never the key, the email or the payload.
|
|
387
|
-
request.log.warn(
|
|
388
|
-
{ kind, siteId: workspaceId, outcome: "idempotency_conflict" },
|
|
389
|
-
"lead idempotency_conflict",
|
|
390
|
-
);
|
|
391
|
-
return reply.status(422).send({
|
|
392
|
-
error: "idempotency_key_reused",
|
|
393
|
-
message: "This Idempotency-Key was used for a different lead.",
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
if (result.error.kind === "invalid_input") {
|
|
397
|
-
return reply
|
|
398
|
-
.status(400)
|
|
399
|
-
.send({ error: "invalid_input", message: result.error.message });
|
|
400
|
-
}
|
|
401
|
-
if (result.error.kind === "persist_failed") {
|
|
402
|
-
// Do not surface internal error detail — log it server-side only
|
|
403
|
-
request.log.error(
|
|
404
|
-
{ kind, siteId: workspaceId, error: result.error.message },
|
|
405
|
-
"lead persist_failed",
|
|
406
|
-
);
|
|
407
|
-
return reply.status(503).send({ error: "persist_failed" });
|
|
408
|
-
}
|
|
409
|
-
if (result.error.kind === "delivery_failed") {
|
|
410
|
-
// Email was the delivery and the provider did not accept it —
|
|
411
|
-
// never reported as sent (#6730 D5). Detail stays server-side.
|
|
412
|
-
// `{ kind, siteId }` only (spec D5): provider error text can echo
|
|
413
|
-
// an address.
|
|
414
|
-
request.log.error({ kind, siteId: workspaceId }, "lead delivery_failed");
|
|
415
|
-
return reply.status(503).send({
|
|
416
|
-
error: "delivery_failed",
|
|
417
|
-
message: "The message could not be delivered.",
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
// config_missing — this deployment has nowhere to put this kind of
|
|
421
|
-
// lead (#6730 D1: production with neither a database nor the email
|
|
422
|
-
// vars, or a newsletter signup with no database).
|
|
423
|
-
return reply.status(503).send({
|
|
424
|
-
error: "lead_capture_not_configured",
|
|
425
|
-
message: "Lead capture is not configured.",
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// PII-safe structured log: only routing metadata, no email/phone/name/payload
|
|
430
|
-
const emailKind =
|
|
431
|
-
"skipped" in result.email
|
|
432
|
-
? "skipped"
|
|
433
|
-
: result.email.ok
|
|
434
|
-
? "ok"
|
|
435
|
-
: "error";
|
|
436
|
-
request.log.info(
|
|
437
|
-
{
|
|
438
|
-
siteId: workspaceId,
|
|
439
|
-
kind,
|
|
440
|
-
ip: sourceMeta.ip,
|
|
441
|
-
persisted: result.persisted,
|
|
442
|
-
emailKind,
|
|
443
|
-
replayed: result.replayed === true,
|
|
444
|
-
},
|
|
445
|
-
"lead routed",
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
// Request-and-confirm (#5970): mint the tenant's one-time confirm
|
|
449
|
-
// credential and email them the link. Fire-and-forget and `void`-
|
|
450
|
-
// returning, so the 201 below stays independent of both Resend and that
|
|
451
|
-
// write — the request is already persisted by this point, and a send
|
|
452
|
-
// that fails must never turn a captured lead into an error.
|
|
453
|
-
//
|
|
454
|
-
// Callback only: `submit` and `subscribe` have no confirm step. The
|
|
455
|
-
// service itself no-ops (with a logged reason) when the feature is not
|
|
456
|
-
// configured, which is the default.
|
|
457
|
-
// `result.persisted === "db"` for the same reason `lead-routing.ts`
|
|
458
|
-
// consults it (#5970 security review round 3, M1): the credential is
|
|
459
|
-
// minted by an update on the `callbacks` row, so a FILE-FALLBACK id
|
|
460
|
-
// would match nothing and the operator would be left with no email at
|
|
461
|
-
// all. On that path the generic notification is sent instead, which is
|
|
462
|
-
// what keeps "exactly one" true in both directions.
|
|
463
|
-
// A spoke's callback never takes this path (#7532 D4, SEC-7532-2): the
|
|
464
|
-
// link is minted on the deployment's one Stripe account.
|
|
465
|
-
if (
|
|
466
|
-
kind === "callback" &&
|
|
467
|
-
result.persisted === "db" &&
|
|
468
|
-
caller.kind !== "spoke"
|
|
469
|
-
) {
|
|
470
|
-
const callbackInput = buildInput(
|
|
471
|
-
kind,
|
|
472
|
-
body,
|
|
473
|
-
workspaceId,
|
|
474
|
-
sourceMeta.receivedAt,
|
|
475
|
-
) as Extract<RouteLeadInput, { kind: "callback" }>;
|
|
476
|
-
sendRequestConfirmLink(result.id, {
|
|
477
|
-
name: callbackInput.name,
|
|
478
|
-
phone: callbackInput.phone,
|
|
479
|
-
...(callbackInput.preferredTime !== undefined && {
|
|
480
|
-
preferredTime: callbackInput.preferredTime,
|
|
481
|
-
}),
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// #8757 D6 — a written `flow:<slug>` submit answers its magnet's
|
|
486
|
-
// completion. A read that fails, or outlasts its budget, leaves the 201
|
|
487
|
-
// as it was: the lead is saved.
|
|
488
|
-
let next: LeadSubmitNext | undefined;
|
|
489
|
-
const source = body["source"];
|
|
490
|
-
if (
|
|
491
|
-
kind === "submit" &&
|
|
492
|
-
result.persisted === "db" &&
|
|
493
|
-
typeof source === "string" &&
|
|
494
|
-
source.startsWith("flow:")
|
|
495
|
-
) {
|
|
496
|
-
const slug = source.slice("flow:".length);
|
|
497
|
-
try {
|
|
498
|
-
const completion = await magnetCompletionWithinBudget(workspaceId, slug, () =>
|
|
499
|
-
accountTargetFor(caller),
|
|
500
|
-
);
|
|
501
|
-
if (completion !== null) {
|
|
502
|
-
next = completion.next;
|
|
503
|
-
if (completion.next.completion === "invite" && !result.replayed) {
|
|
504
|
-
const submitInput = buildInput(
|
|
505
|
-
kind,
|
|
506
|
-
body,
|
|
507
|
-
workspaceId,
|
|
508
|
-
sourceMeta.receivedAt,
|
|
509
|
-
) as Extract<RouteLeadInput, { kind: "submit" }>;
|
|
510
|
-
const locale = submitInput.payload["locale"];
|
|
511
|
-
void sendInviteConfirmation({
|
|
512
|
-
workspaceId,
|
|
513
|
-
slug,
|
|
514
|
-
email: submitInput.email,
|
|
515
|
-
locale: typeof locale === "string" ? locale : undefined,
|
|
516
|
-
completion,
|
|
517
|
-
now: new Date(),
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} catch (error) {
|
|
522
|
-
request.log.error(
|
|
523
|
-
{ event: "magnet-completion", error: error instanceof Error ? error.name : "unknown" },
|
|
524
|
-
"lead magnet completion read failed",
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Fire-and-forget: track() never throws; no need to hold the response.
|
|
530
|
-
// #7869 D6 — a replay records nothing new: the first write counted it.
|
|
531
|
-
if (!result.replayed) {
|
|
532
|
-
void track("lead-captured", {
|
|
533
|
-
source: body["source"] as string | undefined,
|
|
534
|
-
locale: (body["payload"] as Record<string, unknown> | undefined)?.[
|
|
535
|
-
"locale"
|
|
536
|
-
] as string | undefined,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return reply.status(201).send({
|
|
541
|
-
id: result.id,
|
|
542
|
-
persisted: result.persisted,
|
|
543
|
-
email: result.email,
|
|
544
|
-
...(next === undefined ? {} : { next }),
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
export const leadSubmitRoute = makeLeadsRoute("submit");
|
|
551
|
-
export const leadSubscribeRoute = makeLeadsRoute("subscribe");
|
|
552
|
-
export const leadCallbackRoute = makeLeadsRoute("callback");
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* `GET /v1/leads/capture` (#6730 D4) — two booleans and nothing else: never
|
|
556
|
-
* an address or which env var is missing. (The pair implies as much of the
|
|
557
|
-
* delivery mode as the rendered page already shows.) Registered at
|
|
558
|
-
* the top level in `server.ts`, outside `leadsScope`'s strict per-IP limit
|
|
559
|
-
* (a web server's single egress IP already spends that on form submissions),
|
|
560
|
-
* so only the server-wide unauthenticated ceiling applies.
|
|
561
|
-
*/
|
|
562
|
-
export const leadCaptureStatusRoute: FastifyPluginAsync = async (fastify) => {
|
|
563
|
-
fastify.get("/v1/leads/capture", async (_request, reply) => {
|
|
564
|
-
const delivery = resolveLeadDelivery();
|
|
565
|
-
return reply.header("Cache-Control", "public, max-age=60").send({
|
|
566
|
-
ok: true,
|
|
567
|
-
data: {
|
|
568
|
-
contact: acceptsKind(delivery, "submit"),
|
|
569
|
-
newsletter: acceptsKind(delivery, "subscribe"),
|
|
570
|
-
},
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
};
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The leads INBOX read (#5678).
|
|
3
|
-
*
|
|
4
|
-
* Routes:
|
|
5
|
-
* GET /v1/leads?cursor=&limit= — the caller's own sites' leads, newest first
|
|
6
|
-
*
|
|
7
|
-
* Registered INSIDE `server.ts`'s `authScope` (auth only — NOT the inner
|
|
8
|
-
* `tenantScope`, which the multi-tenant module owns), beside
|
|
9
|
-
* `/v1/waitlist-signups/me` and `/v1/project-listings/me`. Lead capture is a
|
|
10
|
-
* never-gate surface at every level, so reading a lead back must be too: this
|
|
11
|
-
* route is registered unconditionally, not behind an `isModuleEnabled()` gate.
|
|
12
|
-
* Registering everywhere is not the same as RETURNING a row everywhere —
|
|
13
|
-
* `services/leads-inbox.ts` § "What it takes for this to return anything"
|
|
14
|
-
* names the two deployment preconditions, both of which fail empty, never
|
|
15
|
-
* wide.
|
|
16
|
-
*
|
|
17
|
-
* Identity is `request.userId` ONLY. There is no site, org or workspace
|
|
18
|
-
* parameter — the query schema declares `cursor` and `limit` and nothing else
|
|
19
|
-
* — so the scope cannot be widened from the wire. `services/leads-inbox.ts`
|
|
20
|
-
* resolves it from the caller's own memberships through
|
|
21
|
-
* `services/owner-workspaces.ts`'s `resolveOwnerWorkspaces` and filters every
|
|
22
|
-
* read on `siteId ∈ workspaceIds`.
|
|
23
|
-
*
|
|
24
|
-
* Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
|
|
25
|
-
* `{ ok: false, error: { code, message } }`, real HTTP status codes:
|
|
26
|
-
* 400 validation_failed · 401 unauthorized · 500 internal_error.
|
|
27
|
-
*
|
|
28
|
-
* No response-contract re-parse (the `waitlist-experiments/respond.ts`
|
|
29
|
-
* `sendValidated` net): the five fields of a row are assembled one by one in
|
|
30
|
-
* `leads-inbox.ts` from five explicitly selected columns, with no
|
|
31
|
-
* pass-through of a stored value that could carry an unexpected shape — so
|
|
32
|
-
* there is no drift for that net to catch.
|
|
33
|
-
*
|
|
34
|
-
* API↔MCP parity: identity-bound (the session IS the scope; the OSS MCP
|
|
35
|
-
* surface is identity-blind) — no MCP twin (#3591 exception, the same shape as
|
|
36
|
-
* `/v1/workspaces/*`, `/v1/billing/*` and `/v1/waitlist-experiments/*`).
|
|
37
|
-
*/
|
|
38
|
-
import { zLeadId } from "@working-theory/ids";
|
|
39
|
-
import type { FastifyPluginAsync, FastifyReply } from "fastify";
|
|
40
|
-
import { z } from "zod";
|
|
41
|
-
|
|
42
|
-
import { listOwnedLeads } from "../../services/leads-inbox.js";
|
|
43
|
-
|
|
44
|
-
const DEFAULT_LIMIT = 50;
|
|
45
|
-
const MAX_LIMIT = 200;
|
|
46
|
-
|
|
47
|
-
const emptyToUndefined = (val: unknown) => (val === "" ? undefined : val);
|
|
48
|
-
|
|
49
|
-
/** Boundary validation, mirroring `waitlist-experiments/signups.ts`'s query schema. */
|
|
50
|
-
const listQuerySchema = z.object({
|
|
51
|
-
cursor: z.preprocess(emptyToUndefined, zLeadId.optional()),
|
|
52
|
-
limit: z.preprocess(
|
|
53
|
-
emptyToUndefined,
|
|
54
|
-
z.coerce.number().int().min(1).max(MAX_LIMIT).optional(),
|
|
55
|
-
),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
function unauthorized(reply: FastifyReply) {
|
|
59
|
-
return reply.status(401).send({
|
|
60
|
-
ok: false,
|
|
61
|
-
error: { code: "unauthorized", message: "Authentication required" },
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function invalidQuery(reply: FastifyReply, message: string) {
|
|
66
|
-
return reply.status(400).send({
|
|
67
|
-
ok: false,
|
|
68
|
-
error: { code: "validation_failed", message },
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const leadListRoute: FastifyPluginAsync = async (fastify) => {
|
|
73
|
-
fastify.get("/v1/leads", async (request, reply) => {
|
|
74
|
-
const userId = request.userId;
|
|
75
|
-
if (!userId) return unauthorized(reply);
|
|
76
|
-
|
|
77
|
-
const query = listQuerySchema.safeParse(request.query);
|
|
78
|
-
if (!query.success) return invalidQuery(reply, "Invalid query parameters");
|
|
79
|
-
|
|
80
|
-
const result = await listOwnedLeads({
|
|
81
|
-
userId,
|
|
82
|
-
cursor: query.data.cursor,
|
|
83
|
-
limit: query.data.limit ?? DEFAULT_LIMIT,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (!result.ok) {
|
|
87
|
-
if (result.error.kind === "invalid_cursor") {
|
|
88
|
-
return invalidQuery(reply, result.error.message);
|
|
89
|
-
}
|
|
90
|
-
// Do not surface internal error detail — log it server-side only, and
|
|
91
|
-
// with no lead field in the log line (the leads PII boundary).
|
|
92
|
-
request.log.error(
|
|
93
|
-
{ route: "leads/list", error: result.error.message },
|
|
94
|
-
"leads list failed",
|
|
95
|
-
);
|
|
96
|
-
return reply.status(500).send({
|
|
97
|
-
ok: false,
|
|
98
|
-
error: { code: "internal_error", message: "Failed to read leads" },
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return reply.status(200).send({ ok: true, data: result.value });
|
|
103
|
-
});
|
|
104
|
-
};
|