create-ailk 0.24.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 +42 -20
- package/component-catalog.json +3 -3
- package/component-catalog.md +3 -3
- package/dist/add-module.js +4 -2
- package/dist/add-route.d.ts +12 -1
- package/dist/add-route.js +31 -3
- package/dist/cli.js +12 -7
- package/dist/derive-architecture.d.ts +3 -4
- package/dist/derive-architecture.js +65 -29
- 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/module-architecture.d.ts +20 -3
- package/dist/module-architecture.js +117 -550
- package/dist/paid-sections.d.ts +25 -1
- package/dist/paid-sections.js +88 -3
- package/dist/surfaces.js +10 -2
- 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 +5 -5
- package/templates/.github/workflows/ci.yml +7 -4
- package/templates/CLAUDE.md +11 -1
- package/templates/CONVENTIONS.md +8 -10
- package/templates/README.md +14 -13
- package/templates/apps/api/.env.example +2 -2
- package/templates/apps/api/CLAUDE.md +102 -76
- 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 +3 -3
- 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 -1882
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/CLAUDE.md +67 -20
- 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 -13
- 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 +10 -3
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
- package/templates/apps/web/app/[locale]/contact/intake-analytics.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 +7 -3
- 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/auth-email.ts +1 -1
- 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 +5 -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 +553 -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 -308
- 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 -257
- 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 -321
- 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 -391
- 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 -2148
- 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 -332
- 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 -1426
- 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 -183
- package/templates/apps/api/src/routes/content/index.ts +0 -49
- package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
- 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 -100
- package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
- 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 -639
- 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 -491
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
- 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 -449
- 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 -809
- 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 -2028
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
- 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 -877
- 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 -1204
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
- 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 -336
- 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 -750
- 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 -348
- 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 -975
- 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 -400
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
- package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
- package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
- 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 -1211
- 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 -2085
- 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 -1232
- 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 -70
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
- 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 -180
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
- package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
- package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
- 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__/rate-limit-write.test.ts +0 -189
- package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
- package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
- package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
- 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 -222
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
- 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 -349
- 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 -564
- 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 -116
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
- 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/site-config.ts +0 -53
- 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 -66
- 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 -138
- package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
- 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 -81
- package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
- package/templates/apps/mcp/src/tools/search_content.ts +0 -63
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
- package/templates/apps/mcp/src/workspace-server.ts +0 -224
- package/templates/database/CHANGELOG.md +0 -981
- 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 -272
- package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
- package/templates/database/__tests__/tenancy.test.ts +0 -141
- package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
- 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/20261025120000_trial_magnet/migration.sql +0 -28
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
- 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,336 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file abandon-email.ts
|
|
3
|
-
* @description The abandon-email sweep service (#3962 D4).
|
|
4
|
-
*
|
|
5
|
-
* `runAbandonEmailSweep` is the one entry point: select partial `FlowSession`
|
|
6
|
-
* rows past their flow's configured delay, dispatch per the flow's send
|
|
7
|
-
* policy (dark → audit-only stamp; enabled → send + stamp), and record the
|
|
8
|
-
* outcome on the session row (D5's idempotency + audit guarantee). Invoked
|
|
9
|
-
* by the CLI entry (`src/bin/abandon-sweep.ts`) on an operator-supplied
|
|
10
|
-
* cadence — this service itself is stateless and has no timer of its own
|
|
11
|
-
* (D1: no queue/cron infrastructure).
|
|
12
|
-
*
|
|
13
|
-
* Mirrors the lead-routing/flow-engine house shape: type-result returns,
|
|
14
|
-
* mocked-dep tests, PII-safe logging via @working-theory/observability
|
|
15
|
-
* (siteId/flowSlug/sessionId/mode/outcome only — never email/state values).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { sendAbandonEmail } from "@working-theory/email";
|
|
19
|
-
import { logger } from "@working-theory/observability";
|
|
20
|
-
import {
|
|
21
|
-
flowConfigSchema,
|
|
22
|
-
type AbandonEmailConfig,
|
|
23
|
-
} from "@working-theory/validation";
|
|
24
|
-
|
|
25
|
-
import { prisma } from "../lib/prisma.js";
|
|
26
|
-
|
|
27
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export type AbandonEmailSweepSummary = {
|
|
30
|
-
scanned: number;
|
|
31
|
-
selected: number;
|
|
32
|
-
sent: number;
|
|
33
|
-
dark: number;
|
|
34
|
-
failed: number;
|
|
35
|
-
skipped: number;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type AbandonEmailSweepError = {
|
|
39
|
-
kind: "flows_not_configured" | "unexpected_error";
|
|
40
|
-
message: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export type AbandonEmailSweepResult =
|
|
44
|
-
| { ok: true; summary: AbandonEmailSweepSummary }
|
|
45
|
-
| { ok: false; error: AbandonEmailSweepError };
|
|
46
|
-
|
|
47
|
-
export type RunAbandonEmailSweepOptions = {
|
|
48
|
-
siteId?: string;
|
|
49
|
-
now?: Date;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type FlowDefinitionRow = {
|
|
53
|
-
id: string;
|
|
54
|
-
siteId: string;
|
|
55
|
-
slug: string;
|
|
56
|
-
config: unknown;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
type AbandonCandidateRow = {
|
|
60
|
-
id: string;
|
|
61
|
-
siteId: string;
|
|
62
|
-
flowSlug: string;
|
|
63
|
-
email: string | null;
|
|
64
|
-
locale: string | null;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// ─── DB-required guard (mirrors flow-engine.ts's requireDb) ────────────────────
|
|
68
|
-
|
|
69
|
-
const FLOWS_NOT_CONFIGURED_ERROR = {
|
|
70
|
-
kind: "flows_not_configured" as const,
|
|
71
|
-
message: "Flows are not configured.",
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function requireDb(): AbandonEmailSweepError | null {
|
|
75
|
-
if (!process.env.DATABASE_URL) return FLOWS_NOT_CONFIGURED_ERROR;
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ─── PII-safe outcome logging ───────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
type AbandonOutcome = "dark" | "skipped" | "sent" | "failed";
|
|
82
|
-
|
|
83
|
-
const OUTCOME_MESSAGE: Record<AbandonOutcome, string> = {
|
|
84
|
-
dark: "[abandon-email] session selected — dark mode, audit-only",
|
|
85
|
-
skipped: "[abandon-email] enabled candidate skipped — no from-address configured",
|
|
86
|
-
sent: "[abandon-email] session selected — sent",
|
|
87
|
-
failed: "[abandon-email] session selected — send failed",
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* The one shape every outcome logs (siteId/flowSlug/sessionId/mode/outcome —
|
|
92
|
-
* never email/state values, per this file's PII boundary).
|
|
93
|
-
*/
|
|
94
|
-
function logOutcome(
|
|
95
|
-
session: AbandonCandidateRow,
|
|
96
|
-
mode: AbandonEmailConfig["mode"],
|
|
97
|
-
outcome: AbandonOutcome,
|
|
98
|
-
): void {
|
|
99
|
-
logger.info(
|
|
100
|
-
{
|
|
101
|
-
siteId: session.siteId,
|
|
102
|
-
flowSlug: session.flowSlug,
|
|
103
|
-
sessionId: session.id,
|
|
104
|
-
mode,
|
|
105
|
-
outcome,
|
|
106
|
-
},
|
|
107
|
-
OUTCOME_MESSAGE[outcome],
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ─── Copy resolution (D6) ───────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* `copy[session.locale ?? liveConfig.defaultLocale] ?? copy[liveConfig.defaultLocale]`
|
|
115
|
-
* (D6). `defaultLocale` is a required key on every LocalizedString the
|
|
116
|
-
* `abandonEmailSchema` superRefine walk enforces, so the final fallback is
|
|
117
|
-
* always resolvable for a schema-valid config.
|
|
118
|
-
*/
|
|
119
|
-
function resolveCopy(
|
|
120
|
-
copy: Record<string, string>,
|
|
121
|
-
sessionLocale: string | null,
|
|
122
|
-
defaultLocale: string,
|
|
123
|
-
): string {
|
|
124
|
-
const locale = sessionLocale ?? defaultLocale;
|
|
125
|
-
return copy[locale] ?? copy[defaultLocale] ?? "";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Link = resumeUrl + '?fs=' + sessionId — no other session data (D7, no PII). */
|
|
129
|
-
function buildResumeLink(
|
|
130
|
-
resumeUrl: string | undefined,
|
|
131
|
-
sessionId: string,
|
|
132
|
-
): string | undefined {
|
|
133
|
-
if (!resumeUrl) return undefined;
|
|
134
|
-
return `${resumeUrl}?fs=${sessionId}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─── Candidate paging (#9249) ───────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* How many candidates one run reads per flow definition. A flow with more
|
|
141
|
-
* past-delay sessions than this processes one page and leaves the rest —
|
|
142
|
-
* still `abandonEmailStatus: null` — for the next run. Same page as
|
|
143
|
-
* `STANDARD_SEED_PAGE` (lib/tenant-db.ts), this app's other background page.
|
|
144
|
-
*/
|
|
145
|
-
export const ABANDON_CANDIDATE_PAGE_SIZE = 500;
|
|
146
|
-
|
|
147
|
-
// ─── The sweep ──────────────────────────────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* The unguarded sweep body — mutates `summary` in place as it goes, so a
|
|
151
|
-
* mid-loop throw still leaves the caller's catch block able to log how far
|
|
152
|
-
* it got (code-review finding on #3962's acceptance PR: an unhandled
|
|
153
|
-
* rejection mid-sweep should degrade to a structured error with the partial
|
|
154
|
-
* progress visible, not vanish).
|
|
155
|
-
*/
|
|
156
|
-
async function sweepAllDefinitions(
|
|
157
|
-
opts: RunAbandonEmailSweepOptions,
|
|
158
|
-
now: Date,
|
|
159
|
-
fromAddress: string | undefined,
|
|
160
|
-
summary: AbandonEmailSweepSummary,
|
|
161
|
-
): Promise<void> {
|
|
162
|
-
const definitions = (await prisma.flowDefinition.findMany({
|
|
163
|
-
...(opts.siteId !== undefined && { where: { siteId: opts.siteId } }),
|
|
164
|
-
select: { id: true, siteId: true, slug: true, config: true },
|
|
165
|
-
})) as FlowDefinitionRow[];
|
|
166
|
-
|
|
167
|
-
for (const definition of definitions) {
|
|
168
|
-
const parsedConfig = flowConfigSchema.safeParse(definition.config);
|
|
169
|
-
if (!parsedConfig.success) {
|
|
170
|
-
// Surface, do not swallow (code-review finding on #3962's acceptance
|
|
171
|
-
// PR) — every sibling path in this feature area (serveFlow /
|
|
172
|
-
// upsertFlowDefinition in flow-engine.ts) returns an explicit error on
|
|
173
|
-
// a config-parse failure; a stored config that has drifted from the
|
|
174
|
-
// schema must not silently stop that flow's abandon-email policy from
|
|
175
|
-
// ever running again with no operator-visible signal.
|
|
176
|
-
logger.warn(
|
|
177
|
-
{
|
|
178
|
-
siteId: definition.siteId,
|
|
179
|
-
flowSlug: definition.slug,
|
|
180
|
-
zodIssue: parsedConfig.error.issues[0]?.message,
|
|
181
|
-
},
|
|
182
|
-
"[abandon-email] skipping flow — stored config failed schema validation",
|
|
183
|
-
);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
const config = parsedConfig.data;
|
|
187
|
-
const policy: AbandonEmailConfig | undefined = config.abandonEmail;
|
|
188
|
-
// Block absent = abandon email OFF for this flow — never selected (D2).
|
|
189
|
-
if (!policy) continue;
|
|
190
|
-
|
|
191
|
-
const cutoff = new Date(now.getTime() - policy.delayMinutes * 60_000);
|
|
192
|
-
|
|
193
|
-
// The #3956 D2 partial-lead query (email != null AND completedAt == null)
|
|
194
|
-
// plus the not-yet-processed and delay-window terms (D4/D5).
|
|
195
|
-
const candidates = (await prisma.flowSession.findMany({
|
|
196
|
-
where: {
|
|
197
|
-
flowId: definition.id,
|
|
198
|
-
email: { not: null },
|
|
199
|
-
completedAt: null,
|
|
200
|
-
abandonEmailStatus: null,
|
|
201
|
-
updatedAt: { lte: cutoff },
|
|
202
|
-
},
|
|
203
|
-
select: {
|
|
204
|
-
id: true,
|
|
205
|
-
siteId: true,
|
|
206
|
-
flowSlug: true,
|
|
207
|
-
email: true,
|
|
208
|
-
locale: true,
|
|
209
|
-
},
|
|
210
|
-
// #9249 — oldest-updated first, so the page is deterministic (the same
|
|
211
|
-
// reason flow-aggregate.ts orders its capped read).
|
|
212
|
-
orderBy: { updatedAt: "asc" },
|
|
213
|
-
take: ABANDON_CANDIDATE_PAGE_SIZE,
|
|
214
|
-
})) as AbandonCandidateRow[];
|
|
215
|
-
|
|
216
|
-
summary.scanned += candidates.length;
|
|
217
|
-
|
|
218
|
-
const darkIds: string[] = [];
|
|
219
|
-
|
|
220
|
-
for (const session of candidates) {
|
|
221
|
-
summary.selected += 1;
|
|
222
|
-
|
|
223
|
-
if (policy.mode === "dark") {
|
|
224
|
-
darkIds.push(session.id);
|
|
225
|
-
logOutcome(session, policy.mode, "dark");
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// mode:'enabled' — an env gap must not consume the one send: leave the
|
|
230
|
-
// candidate UNSTAMPED so a later configured run still sends it (D4).
|
|
231
|
-
if (!fromAddress) {
|
|
232
|
-
summary.skipped += 1;
|
|
233
|
-
logOutcome(session, policy.mode, "skipped");
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const subject = resolveCopy(
|
|
238
|
-
policy.copy.subject,
|
|
239
|
-
session.locale,
|
|
240
|
-
config.defaultLocale,
|
|
241
|
-
);
|
|
242
|
-
const body = resolveCopy(
|
|
243
|
-
policy.copy.body,
|
|
244
|
-
session.locale,
|
|
245
|
-
config.defaultLocale,
|
|
246
|
-
);
|
|
247
|
-
const cta = policy.copy.cta
|
|
248
|
-
? resolveCopy(policy.copy.cta, session.locale, config.defaultLocale)
|
|
249
|
-
: undefined;
|
|
250
|
-
const resumeLink = buildResumeLink(policy.resumeUrl, session.id);
|
|
251
|
-
|
|
252
|
-
const sendResult = await sendAbandonEmail({
|
|
253
|
-
// `email: { not: null }` in the WHERE clause above guarantees this.
|
|
254
|
-
to: session.email as string,
|
|
255
|
-
fromAddress,
|
|
256
|
-
subject,
|
|
257
|
-
body,
|
|
258
|
-
...(cta !== undefined && { cta }),
|
|
259
|
-
...(resumeLink !== undefined && { resumeLink }),
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
if (sendResult.ok) {
|
|
263
|
-
await prisma.flowSession.update({
|
|
264
|
-
where: { id: session.id },
|
|
265
|
-
data: { abandonEmailStatus: "sent", abandonEmailAt: now },
|
|
266
|
-
});
|
|
267
|
-
summary.sent += 1;
|
|
268
|
-
logOutcome(session, policy.mode, "sent");
|
|
269
|
-
} else {
|
|
270
|
-
// Terminal to the sweep — no auto-retry (D5); clearing the stamp is
|
|
271
|
-
// the deliberate operator retry.
|
|
272
|
-
await prisma.flowSession.update({
|
|
273
|
-
where: { id: session.id },
|
|
274
|
-
data: { abandonEmailStatus: "failed", abandonEmailAt: now },
|
|
275
|
-
});
|
|
276
|
-
summary.failed += 1;
|
|
277
|
-
logOutcome(session, policy.mode, "failed");
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// #9249 — one stamp for the whole dark-mode page, not one per row. The
|
|
282
|
-
// `abandonEmailStatus: null` guard is the candidate read's own term, so a
|
|
283
|
-
// row an overlapping run stamped meanwhile keeps what that run wrote.
|
|
284
|
-
if (darkIds.length > 0) {
|
|
285
|
-
await prisma.flowSession.updateMany({
|
|
286
|
-
where: { id: { in: darkIds }, abandonEmailStatus: null },
|
|
287
|
-
data: { abandonEmailStatus: "dark", abandonEmailAt: now },
|
|
288
|
-
});
|
|
289
|
-
summary.dark += darkIds.length;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export async function runAbandonEmailSweep(
|
|
295
|
-
opts: RunAbandonEmailSweepOptions = {},
|
|
296
|
-
): Promise<AbandonEmailSweepResult> {
|
|
297
|
-
const dbError = requireDb();
|
|
298
|
-
if (dbError) return { ok: false, error: dbError };
|
|
299
|
-
|
|
300
|
-
const now = opts.now ?? new Date();
|
|
301
|
-
// #4740: `||`, not `??` — an accidentally-set empty-string override
|
|
302
|
-
// (AILK_ABANDON_EMAIL_FROM="") is an env GAP, not a sender, so it falls
|
|
303
|
-
// through to the owner-wide default; both empty resolves to `undefined` and
|
|
304
|
-
// the `!fromAddress` guard leaves the candidate unstamped. The same shape
|
|
305
|
-
// is applied identically in waitlist-followup.ts's sibling sweep.
|
|
306
|
-
const fromAddress =
|
|
307
|
-
process.env.AILK_ABANDON_EMAIL_FROM ||
|
|
308
|
-
process.env.AILK_LEAD_NOTIFICATION_FROM ||
|
|
309
|
-
undefined;
|
|
310
|
-
|
|
311
|
-
const summary: AbandonEmailSweepSummary = {
|
|
312
|
-
scanned: 0,
|
|
313
|
-
selected: 0,
|
|
314
|
-
sent: 0,
|
|
315
|
-
dark: 0,
|
|
316
|
-
failed: 0,
|
|
317
|
-
skipped: 0,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
await sweepAllDefinitions(opts, now, fromAddress, summary);
|
|
322
|
-
} catch (err) {
|
|
323
|
-
// An unexpected DB/network failure mid-loop must degrade to a
|
|
324
|
-
// structured error, not an unhandled rejection (code-review finding on
|
|
325
|
-
// #3962's acceptance PR) — the CLI entry's own try/catch expects this
|
|
326
|
-
// shape. Progress already stamped on session rows before the throw is
|
|
327
|
-
// NOT lost (D5's idempotency means a re-run picks up where this one
|
|
328
|
-
// stopped); the partial in-memory summary is logged so it is not
|
|
329
|
-
// silently discarded.
|
|
330
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
331
|
-
logger.error({ partialSummary: summary, err: message }, "[abandon-email] sweep failed mid-run");
|
|
332
|
-
return { ok: false, error: { kind: "unexpected_error", message } };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return { ok: true, summary };
|
|
336
|
-
}
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Instant AEO score service (#4666 D3/D4).
|
|
3
|
-
*
|
|
4
|
-
* Productizes the existing pure `@working-theory/aeo` scorer behind the
|
|
5
|
-
* SSRF-guarded external-URL crawler at `@working-theory/aeo/fetch`. The
|
|
6
|
-
* rule set, severity table, and `identityMode`/`locale` defaults are
|
|
7
|
-
* declared ONCE, here.
|
|
8
|
-
*/
|
|
9
|
-
import {
|
|
10
|
-
runAudit,
|
|
11
|
-
schemaCoverage,
|
|
12
|
-
jsonldValidity,
|
|
13
|
-
entityMarkup,
|
|
14
|
-
faqHowtoIncentive,
|
|
15
|
-
EXAMPLE_SCORE_CRAWL,
|
|
16
|
-
EXAMPLE_SCORE_META,
|
|
17
|
-
EXAMPLE_SCORE_ANNOTATIONS,
|
|
18
|
-
type Rule,
|
|
19
|
-
type AuditReport,
|
|
20
|
-
type PerRuleResult,
|
|
21
|
-
} from "@working-theory/aeo";
|
|
22
|
-
import {
|
|
23
|
-
assertUrlSafe,
|
|
24
|
-
crawlSite,
|
|
25
|
-
buildAuditPayload,
|
|
26
|
-
type CrawlResult,
|
|
27
|
-
type FetchImpl,
|
|
28
|
-
type Resolver,
|
|
29
|
-
type SkippedEntry,
|
|
30
|
-
} from "@working-theory/aeo/fetch";
|
|
31
|
-
import type {
|
|
32
|
-
AeoDimension,
|
|
33
|
-
AeoDimensionId,
|
|
34
|
-
AeoScoreResult,
|
|
35
|
-
} from "@working-theory/validation";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The readiness rules (#5949) are deliberately NOT in this set. They grade
|
|
39
|
-
* frontmatter and site-set facts (`AuditInput.readiness`); `buildAuditPayload`
|
|
40
|
-
* produces no frontmatter, so on a crawl input all three pass vacuously
|
|
41
|
-
* (packages/aeo FR5). Adding them would put three always-100 dimensions into a
|
|
42
|
-
* closed four-member public enum. `/v1/aeo/score` has no MCP twin
|
|
43
|
-
* (apps/api/CLAUDE.md § API ↔ MCP parity), so this is not a parity divergence.
|
|
44
|
-
*/
|
|
45
|
-
export const AEO_SCORE_EXCLUDED_READINESS_RULES = [
|
|
46
|
-
"policies-present",
|
|
47
|
-
"offer-on-priced-page",
|
|
48
|
-
"tax-code-when-stripe-tax",
|
|
49
|
-
] as const;
|
|
50
|
-
|
|
51
|
-
/** The service's fixed rule set — the four content rules only (§ 2.1 FR4). */
|
|
52
|
-
export const AEO_SCORE_RULES: Rule[] = [
|
|
53
|
-
schemaCoverage,
|
|
54
|
-
jsonldValidity,
|
|
55
|
-
entityMarkup,
|
|
56
|
-
faqHowtoIncentive,
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
/** Severity table, verbatim from prospect-audit-site's "Compose the score-led sales report". */
|
|
60
|
-
export const AEO_DIMENSION_SEVERITY: Record<
|
|
61
|
-
AeoDimensionId,
|
|
62
|
-
"high" | "medium" | "low"
|
|
63
|
-
> = {
|
|
64
|
-
"schema-coverage": "high",
|
|
65
|
-
"entity-markup": "high",
|
|
66
|
-
"jsonld-validity": "medium",
|
|
67
|
-
"faq-howto-incentive": "low",
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const AEO_DIMENSION_LABELS: Record<AeoDimensionId, string> = {
|
|
71
|
-
"schema-coverage": "Schema Coverage",
|
|
72
|
-
"jsonld-validity": "JSON-LD Validity",
|
|
73
|
-
"entity-markup": "Entity Markup",
|
|
74
|
-
"faq-howto-incentive": "FAQ / HowTo Incentive",
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/** D5 — the route's default + the configured-budget cap, both declared once. */
|
|
78
|
-
export const AEO_SCORE_DEFAULT_BUDGET_MS = 6000;
|
|
79
|
-
export const AEO_SCORE_MAX_BUDGET_MS = 15000;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Resolve `AEO_SCORE_BUDGET_MS` for the route. A missing/unparseable/
|
|
83
|
-
* non-positive value falls back to the default silently (nothing to warn
|
|
84
|
-
* about — the env var is simply unset in dev). A value above the 15 000 ms
|
|
85
|
-
* cap is logged and falls back to the default (D5) — raising it beyond the
|
|
86
|
-
* crawler's own 15 s per-fetch bound would collide with the handler's 25 s
|
|
87
|
-
* request guard once cold start is added. Re-checked on EVERY request, not
|
|
88
|
-
* once at process startup — a serverless deployment has no persistent
|
|
89
|
-
* startup phase to check this at.
|
|
90
|
-
*/
|
|
91
|
-
export function resolveScoreBudgetMs(
|
|
92
|
-
rawEnv: string | undefined,
|
|
93
|
-
log?: { warn: (msg: string) => void },
|
|
94
|
-
): number {
|
|
95
|
-
if (!rawEnv) return AEO_SCORE_DEFAULT_BUDGET_MS;
|
|
96
|
-
const parsed = Number(rawEnv);
|
|
97
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
98
|
-
return AEO_SCORE_DEFAULT_BUDGET_MS;
|
|
99
|
-
if (parsed > AEO_SCORE_MAX_BUDGET_MS) {
|
|
100
|
-
log?.warn(
|
|
101
|
-
`AEO_SCORE_BUDGET_MS=${rawEnv} exceeds the ${AEO_SCORE_MAX_BUDGET_MS}ms cap; falling back to the ${AEO_SCORE_DEFAULT_BUDGET_MS}ms default.`,
|
|
102
|
-
);
|
|
103
|
-
return AEO_SCORE_DEFAULT_BUDGET_MS;
|
|
104
|
-
}
|
|
105
|
-
return parsed;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const SEVERITY_RANK: Record<"high" | "medium" | "low", number> = {
|
|
109
|
-
high: 0,
|
|
110
|
-
medium: 1,
|
|
111
|
-
low: 2,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/** `lowestTwo` ordering (D4): score asc, then severity high>medium>low, then weight desc. */
|
|
115
|
-
function compareDimensions(a: AeoDimension, b: AeoDimension): number {
|
|
116
|
-
if (a.score !== b.score) return a.score - b.score;
|
|
117
|
-
const severityDiff = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
118
|
-
if (severityDiff !== 0) return severityDiff;
|
|
119
|
-
return b.weight - a.weight;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const FAQ_HOWTO_INCENTIVE_ID: AeoDimensionId = "faq-howto-incentive";
|
|
123
|
-
|
|
124
|
-
/** The fixed opportunity sentence for the incentive dimension (D4) — its per-page results carry no evidence. */
|
|
125
|
-
function faqHowtoOpportunityFinding(slug: string): string {
|
|
126
|
-
return `${slug}: no FAQPage or HowTo structured data (a visibility opportunity, not a defect)`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function buildDimension(
|
|
130
|
-
ruleId: AeoDimensionId,
|
|
131
|
-
report: AuditReport,
|
|
132
|
-
): AeoDimension {
|
|
133
|
-
const perRule = report.perRule.find((r) => r.id === ruleId) as PerRuleResult;
|
|
134
|
-
const perPage = perRule.perPage ?? [];
|
|
135
|
-
const fraction = perRule.score ?? (perRule.pass ? 1 : 0);
|
|
136
|
-
const score = Math.round(fraction * 100);
|
|
137
|
-
|
|
138
|
-
let finding: string | null;
|
|
139
|
-
let pagesPassing: number;
|
|
140
|
-
const pagesTotal = perPage.length > 0 ? perPage.length : 1;
|
|
141
|
-
|
|
142
|
-
if (ruleId === FAQ_HOWTO_INCENTIVE_ID) {
|
|
143
|
-
pagesPassing = perPage.filter((p) => p.bonus === true).length;
|
|
144
|
-
const firstMissing = perPage.find((p) => p.bonus !== true);
|
|
145
|
-
finding = firstMissing
|
|
146
|
-
? faqHowtoOpportunityFinding(firstMissing.slug)
|
|
147
|
-
: null;
|
|
148
|
-
} else {
|
|
149
|
-
pagesPassing =
|
|
150
|
-
perPage.length > 0
|
|
151
|
-
? perPage.filter((p) => p.pass).length
|
|
152
|
-
: perRule.pass
|
|
153
|
-
? 1
|
|
154
|
-
: 0;
|
|
155
|
-
const firstFailing = perPage.find((p) => !p.pass);
|
|
156
|
-
finding = firstFailing
|
|
157
|
-
? (firstFailing.evidence ?? perRule.evidence ?? null)
|
|
158
|
-
: null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
id: ruleId,
|
|
163
|
-
label: AEO_DIMENSION_LABELS[ruleId],
|
|
164
|
-
weight: perRule.weight,
|
|
165
|
-
score,
|
|
166
|
-
severity: AEO_DIMENSION_SEVERITY[ruleId],
|
|
167
|
-
finding,
|
|
168
|
-
pagesPassing,
|
|
169
|
-
pagesTotal,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* `assertUrlSafe`'s per-path rejection text (`packages/aeo/src/fetch/ssrf.ts`)
|
|
175
|
-
* can disclose which internal host/IP a crawl target resolved to — an
|
|
176
|
-
* anonymous-endpoint recon primitive (AILK #5110, mirroring the #4707 fix
|
|
177
|
-
* already applied to `scoreUrl`'s top-level `unavailable`/`ssrf_blocked`
|
|
178
|
-
* check below). `crawlSite` re-checks every path individually (DNS-rebinding
|
|
179
|
-
* defense) and writes the raw `assertUrlSafe` text straight into
|
|
180
|
-
* `SkippedEntry.reason` (`packages/aeo/src/fetch/crawl.ts:125`) — deliberately
|
|
181
|
-
* so, per that file's own test
|
|
182
|
-
* (`packages/aeo/__tests__/fetch/crawl.test.ts:47-49`), so the redaction
|
|
183
|
-
* happens HERE instead, at the boundary where a `ready` result's
|
|
184
|
-
* `meta.skipped` is assembled. Every `assertUrlSafe` rejection message has
|
|
185
|
-
* one of these six fixed prefixes; `crawlSite`'s other `SkippedEntry.reason`
|
|
186
|
-
* producers (`"budget_exceeded"`, a `fetchPage` HTTP/redirect/fetch-failure
|
|
187
|
-
* message) never do.
|
|
188
|
-
*/
|
|
189
|
-
const SSRF_GUARD_REASON_PATTERN =
|
|
190
|
-
/^(Not a valid URL:|Only http\(s\) URLs allowed,|Blocked host \(internal\/loopback\/metadata\):|DNS resolution failed for|No DNS addresses resolved for|Host .+ resolves to a blocked address:)/;
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* `meta.skipped[]` boundary serializer (#5110). A per-path SSRF-guard
|
|
194
|
-
* rejection is logged server-side (the specific host/IP detail, for operator
|
|
195
|
-
* diagnosis) and replaced on the wire with the same generic `"ssrf_blocked"`
|
|
196
|
-
* sentinel `scoreUrl`'s top-level check already returns (below) — never the
|
|
197
|
-
* raw text. Any other skip reason passes through unchanged.
|
|
198
|
-
*/
|
|
199
|
-
function toClientSkippedEntry(entry: SkippedEntry): SkippedEntry {
|
|
200
|
-
if (!SSRF_GUARD_REASON_PATTERN.test(entry.reason)) return entry;
|
|
201
|
-
console.warn(
|
|
202
|
-
"aeo-score: per-path SSRF-guard rejection (redacted on the wire)",
|
|
203
|
-
{
|
|
204
|
-
path: entry.path,
|
|
205
|
-
url: entry.url,
|
|
206
|
-
reason: entry.reason,
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
return { ...entry, reason: "ssrf_blocked" };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Pure: crawl payload → runAudit → dimensions (D3/D4). Never throws on an empty crawl — the caller checks first. */
|
|
213
|
-
export function scoreCrawl(
|
|
214
|
-
crawl: CrawlResult,
|
|
215
|
-
opts?: { now?: () => Date },
|
|
216
|
-
): AeoScoreResult {
|
|
217
|
-
if (crawl.pages.length === 0) {
|
|
218
|
-
return {
|
|
219
|
-
status: "unavailable",
|
|
220
|
-
reason: "no_pages_fetched",
|
|
221
|
-
message: "No pages were fetched.",
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const payload = buildAuditPayload(crawl.pages);
|
|
226
|
-
const report = runAudit(
|
|
227
|
-
{ ...payload, locale: "en", identityMode: "organization" },
|
|
228
|
-
AEO_SCORE_RULES,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const dimensions = AEO_SCORE_RULES.map((rule) =>
|
|
232
|
-
buildDimension(rule.id as AeoDimensionId, report),
|
|
233
|
-
);
|
|
234
|
-
const lowestTwo = [...dimensions].sort(compareDimensions).slice(0, 2);
|
|
235
|
-
const partial = crawl.skipped.some((s) => s.reason === "budget_exceeded");
|
|
236
|
-
const now = opts?.now ? opts.now() : new Date();
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
status: "ready",
|
|
240
|
-
score: Math.round(report.fractionalScore ?? report.score),
|
|
241
|
-
dimensions,
|
|
242
|
-
lowestTwo,
|
|
243
|
-
meta: {
|
|
244
|
-
auditedAt: now.toISOString(),
|
|
245
|
-
pagesAudited: crawl.pages.length,
|
|
246
|
-
partial,
|
|
247
|
-
skipped: crawl.skipped.map(toClientSkippedEntry),
|
|
248
|
-
schemaVersion: report.meta.schemaVersion,
|
|
249
|
-
registryVersion: report.meta.registryVersion,
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export interface ScoreUrlDeps {
|
|
255
|
-
fetchImpl?: FetchImpl;
|
|
256
|
-
resolver?: Resolver;
|
|
257
|
-
budgetMs?: number;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** `assertUrlSafe` → `crawlSite` → `scoreCrawl` (D3). */
|
|
261
|
-
export async function scoreUrl(
|
|
262
|
-
url: string,
|
|
263
|
-
deps: ScoreUrlDeps = {},
|
|
264
|
-
): Promise<AeoScoreResult> {
|
|
265
|
-
const safe = await assertUrlSafe(url, {
|
|
266
|
-
...(deps.resolver !== undefined && { resolver: deps.resolver }),
|
|
267
|
-
});
|
|
268
|
-
if (!safe.ok) {
|
|
269
|
-
return {
|
|
270
|
-
status: "unavailable",
|
|
271
|
-
reason: "ssrf_blocked",
|
|
272
|
-
message: safe.reason,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const budgetMs = deps.budgetMs ?? AEO_SCORE_DEFAULT_BUDGET_MS;
|
|
277
|
-
try {
|
|
278
|
-
const crawl = await crawlSite(url, {
|
|
279
|
-
...(deps.fetchImpl !== undefined && { fetchImpl: deps.fetchImpl }),
|
|
280
|
-
...(deps.resolver !== undefined && { resolver: deps.resolver }),
|
|
281
|
-
deadline: Date.now() + budgetMs,
|
|
282
|
-
});
|
|
283
|
-
if (crawl.pages.length === 0) {
|
|
284
|
-
return {
|
|
285
|
-
status: "unavailable",
|
|
286
|
-
reason: "no_pages_fetched",
|
|
287
|
-
message: "No pages were fetched within the crawl budget.",
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
return scoreCrawl(crawl);
|
|
291
|
-
} catch (e) {
|
|
292
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
293
|
-
return { status: "unavailable", reason: "crawl_failed", message };
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** The bundled pre-launch example — no runtime `fs` read (D6). */
|
|
298
|
-
export function loadExampleScore(): AeoScoreResult {
|
|
299
|
-
const result = scoreCrawl(EXAMPLE_SCORE_CRAWL);
|
|
300
|
-
if (result.status !== "ready") {
|
|
301
|
-
// The fixture is committed and known-good; this should never happen.
|
|
302
|
-
throw new Error("Example score fixture failed to score.");
|
|
303
|
-
}
|
|
304
|
-
const annotate = (d: AeoDimension): AeoDimension => ({
|
|
305
|
-
...d,
|
|
306
|
-
annotation: EXAMPLE_SCORE_ANNOTATIONS[d.id],
|
|
307
|
-
});
|
|
308
|
-
return {
|
|
309
|
-
...result,
|
|
310
|
-
dimensions: result.dimensions.map(annotate),
|
|
311
|
-
lowestTwo: result.lowestTwo.map(annotate),
|
|
312
|
-
meta: {
|
|
313
|
-
...result.meta,
|
|
314
|
-
example: true,
|
|
315
|
-
sourceUrl: EXAMPLE_SCORE_META.sourceUrl,
|
|
316
|
-
generatedAt: EXAMPLE_SCORE_META.generatedAt,
|
|
317
|
-
// Wire-visible disclosure (acceptance-review finding, #4666): whether
|
|
318
|
-
// the crawl data is a genuine fetch of `sourceUrl` or a hand-authored
|
|
319
|
-
// representative dataset. Sourced from EXAMPLE_SCORE_META itself, not
|
|
320
|
-
// hardcoded here, so regenerating the fixture from a real crawl flips
|
|
321
|
-
// this automatically.
|
|
322
|
-
illustrative: EXAMPLE_SCORE_META.illustrative,
|
|
323
|
-
},
|
|
324
|
-
};
|
|
325
|
-
}
|