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,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The comparison instrument for #5314 — diffs the routes the server ACTUALLY
|
|
3
|
-
* registers against each surface that claims to describe them.
|
|
4
|
-
*
|
|
5
|
-
* Two surfaces describe this API's endpoints: the root index `endpoints`
|
|
6
|
-
* object (`server.ts`) and the OpenAPI `paths` object (`spec.ts`). Before
|
|
7
|
-
* #5314 both were hand-maintained literals, and both had drifted — 11 and 7
|
|
8
|
-
* entries against 68 registered routes, with `/v1/project-listings/public`
|
|
9
|
-
* missing from both and `/v1/billing/usage` missing from the root index.
|
|
10
|
-
*
|
|
11
|
-
* This module only COMPARES. The derivation that keeps the surfaces honest
|
|
12
|
-
* lives in `spec.ts` beside the shapes it produces; keeping the comparison
|
|
13
|
-
* separate is what lets the gate assert against a surface it did not build.
|
|
14
|
-
*
|
|
15
|
-
* Deliberately free of any `fastify` or `spec.ts` import: it takes plain data,
|
|
16
|
-
* so the gate can feed it a SERVED document (fetched over `inject`) rather
|
|
17
|
-
* than the object the code under test just returned.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/** One route as Fastify registered it. `url` is Fastify-shaped (`:param`). */
|
|
21
|
-
export interface RegisteredRoute {
|
|
22
|
-
/** Uppercase HTTP method. HEAD is excluded — Fastify derives it from GET. */
|
|
23
|
-
method: string;
|
|
24
|
-
/** Fastify path, e.g. `/v1/content/:type/:slug`. */
|
|
25
|
-
url: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** One surface's disagreement with the registered route table. */
|
|
29
|
-
export interface SurfaceDiff {
|
|
30
|
-
/** Which surface this diff describes. */
|
|
31
|
-
surface: "openapi-paths" | "root-index-endpoints";
|
|
32
|
-
/** `METHOD /path` registered by the server but absent from the surface. */
|
|
33
|
-
missingFromSurface: string[];
|
|
34
|
-
/** `METHOD /path` named by the surface but not registered by the server. */
|
|
35
|
-
orphanedInSurface: string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Fastify path → OpenAPI path template: `/v1/content/:type` becomes
|
|
40
|
-
* `/v1/content/{type}`. OpenAPI 3.1 has no `:param` form, so a document that
|
|
41
|
-
* published the Fastify string would be describing a path no client can build.
|
|
42
|
-
*/
|
|
43
|
-
export function toOpenApiPath(fastifyPath: string): string {
|
|
44
|
-
return fastifyPath
|
|
45
|
-
.split("/")
|
|
46
|
-
.map((segment) =>
|
|
47
|
-
segment.startsWith(":") ? `{${segment.slice(1)}}` : segment,
|
|
48
|
-
)
|
|
49
|
-
.join("/");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** The comparison key both sides normalise to: `GET /v1/content/types`. */
|
|
53
|
-
export function routeKey(method: string, path: string): string {
|
|
54
|
-
return `${method.toUpperCase()} ${path}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Registered routes as OpenAPI-shaped comparison keys. */
|
|
58
|
-
function registeredOpenApiKeys(routes: RegisteredRoute[]): Set<string> {
|
|
59
|
-
return new Set(routes.map((r) => routeKey(r.method, toOpenApiPath(r.url))));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Registered routes as Fastify-shaped comparison keys. */
|
|
63
|
-
function registeredFastifyKeys(routes: RegisteredRoute[]): Set<string> {
|
|
64
|
-
return new Set(routes.map((r) => routeKey(r.method, r.url)));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The OpenAPI methods a path item declares. Non-method keys an OpenAPI path
|
|
69
|
-
* item may legally carry (`parameters`, `summary`, `description`, `servers`,
|
|
70
|
-
* `$ref`) are not operations and must not be compared as if they were.
|
|
71
|
-
*/
|
|
72
|
-
const OPENAPI_METHODS = new Set([
|
|
73
|
-
"get",
|
|
74
|
-
"put",
|
|
75
|
-
"post",
|
|
76
|
-
"delete",
|
|
77
|
-
"options",
|
|
78
|
-
"head",
|
|
79
|
-
"patch",
|
|
80
|
-
"trace",
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Comparison keys a served OpenAPI `paths` object claims.
|
|
85
|
-
*
|
|
86
|
-
* Takes the served document's `paths` as plain data so the gate can compare
|
|
87
|
-
* what an agent would actually fetch, not what the builder returned.
|
|
88
|
-
*/
|
|
89
|
-
export function openApiSurfaceKeys(
|
|
90
|
-
paths: Record<string, Record<string, unknown>>,
|
|
91
|
-
): Set<string> {
|
|
92
|
-
const keys = new Set<string>();
|
|
93
|
-
for (const [path, item] of Object.entries(paths ?? {})) {
|
|
94
|
-
for (const method of Object.keys(item ?? {})) {
|
|
95
|
-
if (!OPENAPI_METHODS.has(method.toLowerCase())) continue;
|
|
96
|
-
if (method.toLowerCase() === "head") continue;
|
|
97
|
-
keys.add(routeKey(method, path));
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return keys;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Comparison keys a root-index `endpoints` object claims.
|
|
105
|
-
*
|
|
106
|
-
* Each value is `"METHOD[/METHOD...] /path"` — the shape the hand-authored
|
|
107
|
-
* object already used (`"GET/PATCH /v1/user/profile"`), preserved so no
|
|
108
|
-
* published entry changes shape. A value that does not parse is reported as
|
|
109
|
-
* an orphan rather than skipped: an unparseable entry is precisely the kind
|
|
110
|
-
* of hand-edit this instrument exists to catch.
|
|
111
|
-
*/
|
|
112
|
-
export function rootIndexSurfaceKeys(
|
|
113
|
-
endpoints: Record<string, string>,
|
|
114
|
-
): Set<string> {
|
|
115
|
-
const keys = new Set<string>();
|
|
116
|
-
for (const value of Object.values(endpoints ?? {})) {
|
|
117
|
-
const match = /^([A-Za-z/]+)\s+(\S+)$/.exec(value ?? "");
|
|
118
|
-
if (!match?.[1] || !match[2]) {
|
|
119
|
-
keys.add(`UNPARSEABLE ${value}`);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
for (const method of match[1].split("/")) {
|
|
123
|
-
if (!method) continue;
|
|
124
|
-
if (method.toUpperCase() === "HEAD") continue;
|
|
125
|
-
keys.add(routeKey(method, match[2]));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return keys;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function diff(registered: Set<string>, surface: Set<string>) {
|
|
132
|
-
return {
|
|
133
|
-
missingFromSurface: [...registered].filter((k) => !surface.has(k)).sort(),
|
|
134
|
-
orphanedInSurface: [...surface].filter((k) => !registered.has(k)).sort(),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Diff the registered route table against both surfaces.
|
|
140
|
-
*
|
|
141
|
-
* `paths` is the served OpenAPI `paths` object (OpenAPI-templated); `endpoints`
|
|
142
|
-
* is the served root-index `endpoints` object (Fastify-shaped, matching the
|
|
143
|
-
* strings that surface has always published). Each is compared in its own
|
|
144
|
-
* vocabulary so neither surface is judged against a shape it never used.
|
|
145
|
-
*/
|
|
146
|
-
export function compareSurfaces(
|
|
147
|
-
registered: RegisteredRoute[],
|
|
148
|
-
paths: Record<string, Record<string, unknown>>,
|
|
149
|
-
endpoints: Record<string, string>,
|
|
150
|
-
): SurfaceDiff[] {
|
|
151
|
-
return [
|
|
152
|
-
{
|
|
153
|
-
surface: "openapi-paths",
|
|
154
|
-
...diff(registeredOpenApiKeys(registered), openApiSurfaceKeys(paths)),
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
surface: "root-index-endpoints",
|
|
158
|
-
...diff(
|
|
159
|
-
registeredFastifyKeys(registered),
|
|
160
|
-
rootIndexSurfaceKeys(endpoints),
|
|
161
|
-
),
|
|
162
|
-
},
|
|
163
|
-
];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Whether any surface disagrees with the registered route table. */
|
|
167
|
-
export function hasDrift(diffs: SurfaceDiff[]): boolean {
|
|
168
|
-
return diffs.some(
|
|
169
|
-
(d) => d.missingFromSurface.length > 0 || d.orphanedInSurface.length > 0,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Render the drift for a failing assertion, or `""` when there is none.
|
|
175
|
-
*
|
|
176
|
-
* A surface that agrees contributes nothing, so the empty string means "no
|
|
177
|
-
* drift" and can be asserted against directly. Counts come first so a reader
|
|
178
|
-
* sees the scale before the list, then EVERY entry — a truncated drift report
|
|
179
|
-
* is how a gate teaches people to ignore it.
|
|
180
|
-
*/
|
|
181
|
-
export function formatSurfaceDrift(diffs: SurfaceDiff[]): string {
|
|
182
|
-
const lines: string[] = [];
|
|
183
|
-
for (const d of diffs) {
|
|
184
|
-
if (d.missingFromSurface.length === 0 && d.orphanedInSurface.length === 0) {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
lines.push(
|
|
188
|
-
`${d.surface}: ${d.missingFromSurface.length} registered route(s) missing from the surface, ` +
|
|
189
|
-
`${d.orphanedInSurface.length} surface entr(ies) with no registered route`,
|
|
190
|
-
);
|
|
191
|
-
for (const k of d.missingFromSurface) lines.push(` missing: ${k}`);
|
|
192
|
-
for (const k of d.orphanedInSurface) lines.push(` orphaned: ${k}`);
|
|
193
|
-
}
|
|
194
|
-
return lines.join("\n");
|
|
195
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file index.test.ts
|
|
3
|
-
* Route-level inject tests for POST /v1/aeo/score and GET /v1/aeo/score/example
|
|
4
|
-
* (#4666 D5).
|
|
5
|
-
*
|
|
6
|
-
* Mirrors apps/api/src/routes/schedule/__tests__/index.test.ts's shape: a bare
|
|
7
|
-
* Fastify instance with only the routes under test registered; the fetch
|
|
8
|
-
* boundary is stubbed rather than the service, so these tests cover the real
|
|
9
|
-
* crawler + scorer alongside the route's envelope and status mapping. Test
|
|
10
|
-
* URLs use literal public/blocked IP addresses so the SSRF gate's DNS step is
|
|
11
|
-
* never exercised (no real network egress needed).
|
|
12
|
-
*/
|
|
13
|
-
import type { FastifyInstance } from "fastify";
|
|
14
|
-
|
|
15
|
-
import { scoreAeoRoute, exampleAeoRoute } from "../index";
|
|
16
|
-
|
|
17
|
-
const HOME_HTML =
|
|
18
|
-
'<html><script type="application/ld+json">{"@type":"HomePage","name":"Home"}</script></html>';
|
|
19
|
-
|
|
20
|
-
/** Typed as the platform signature so it assigns to `globalThis.fetch` with no cast. */
|
|
21
|
-
type FetchStub = jest.Mock<Promise<Response>, [RequestInfo | URL, RequestInit?]>;
|
|
22
|
-
|
|
23
|
-
function buildApp(): FastifyInstance {
|
|
24
|
-
const fastify = require("fastify")();
|
|
25
|
-
fastify.register(scoreAeoRoute);
|
|
26
|
-
fastify.register(exampleAeoRoute);
|
|
27
|
-
return fastify as FastifyInstance;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("POST /v1/aeo/score", () => {
|
|
31
|
-
let app: FastifyInstance;
|
|
32
|
-
const realFetch = globalThis.fetch;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
app = buildApp();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
afterEach(async () => {
|
|
39
|
-
await app.close();
|
|
40
|
-
globalThis.fetch = realFetch;
|
|
41
|
-
delete process.env["AEO_SCORE_BUDGET_MS"];
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("400s on a malformed body", async () => {
|
|
45
|
-
const res = await app.inject({
|
|
46
|
-
method: "POST",
|
|
47
|
-
url: "/v1/aeo/score",
|
|
48
|
-
payload: { url: "not-a-url" },
|
|
49
|
-
});
|
|
50
|
-
expect(res.statusCode).toBe(400);
|
|
51
|
-
const body = JSON.parse(res.body) as { ok: boolean; error: { code: string } };
|
|
52
|
-
expect(body.ok).toBe(false);
|
|
53
|
-
expect(body.error.code).toBe("invalid_input");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("200s with data.status ready for a reachable public-IP host", async () => {
|
|
57
|
-
const stub: FetchStub = jest.fn(async () => new Response(HOME_HTML));
|
|
58
|
-
globalThis.fetch = stub;
|
|
59
|
-
|
|
60
|
-
const res = await app.inject({
|
|
61
|
-
method: "POST",
|
|
62
|
-
url: "/v1/aeo/score",
|
|
63
|
-
payload: { url: "http://93.184.216.34/" },
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
expect(res.statusCode).toBe(200);
|
|
67
|
-
const body = JSON.parse(res.body) as { ok: boolean; data: { status: string; score: number } };
|
|
68
|
-
expect(body.ok).toBe(true);
|
|
69
|
-
expect(body.data.status).toBe("ready");
|
|
70
|
-
expect(typeof body.data.score).toBe("number");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("200s with data.status unavailable/ssrf_blocked for a loopback URL (never 4xx/5xx)", async () => {
|
|
74
|
-
const stub: FetchStub = jest.fn();
|
|
75
|
-
globalThis.fetch = stub;
|
|
76
|
-
|
|
77
|
-
const res = await app.inject({
|
|
78
|
-
method: "POST",
|
|
79
|
-
url: "/v1/aeo/score",
|
|
80
|
-
payload: { url: "http://127.0.0.1/admin" },
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
expect(res.statusCode).toBe(200);
|
|
84
|
-
const body = JSON.parse(res.body) as { ok: boolean; data: { status: string; reason: string } };
|
|
85
|
-
expect(body.ok).toBe(true);
|
|
86
|
-
expect(body.data.status).toBe("unavailable");
|
|
87
|
-
expect(body.data.reason).toBe("ssrf_blocked");
|
|
88
|
-
expect(stub).not.toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// AILK #4707 — the specific SSRF rejection detail (which internal
|
|
92
|
-
// hostname/IP triggered the block, or that DNS resolved to one) is an
|
|
93
|
-
// anonymous-endpoint recon primitive and must never reach the caller; the
|
|
94
|
-
// response carries only the generic reason enum + a fixed message, with
|
|
95
|
-
// the specific detail logged server-side only.
|
|
96
|
-
it("returns a generic message for an SSRF-rejected URL — the specific rejection reason never reaches the response body", async () => {
|
|
97
|
-
const stub: FetchStub = jest.fn();
|
|
98
|
-
globalThis.fetch = stub;
|
|
99
|
-
|
|
100
|
-
const res = await app.inject({
|
|
101
|
-
method: "POST",
|
|
102
|
-
url: "/v1/aeo/score",
|
|
103
|
-
payload: { url: "http://127.0.0.1/admin" },
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const body = JSON.parse(res.body) as {
|
|
107
|
-
ok: boolean;
|
|
108
|
-
data: { status: string; reason: string; message: string };
|
|
109
|
-
};
|
|
110
|
-
expect(body.data.reason).toBe("ssrf_blocked");
|
|
111
|
-
// Same fixed text flow-engine.ts's aeo_score step already scrubs its
|
|
112
|
-
// own SSRF-blocked result to (#4663 M1) — one client-facing message
|
|
113
|
-
// across both aeo_score entry points.
|
|
114
|
-
expect(body.data.message).toBe("This URL cannot be scored.");
|
|
115
|
-
expect(body.data.message).not.toMatch(/127\.0\.0\.1/);
|
|
116
|
-
expect(body.data.message).not.toMatch(/blocked|internal|loopback|metadata/i);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("logs the specific SSRF rejection reason server-side (never returned to the caller)", async () => {
|
|
120
|
-
const logLines: string[] = [];
|
|
121
|
-
const logCapture = require("fastify")({
|
|
122
|
-
logger: {
|
|
123
|
-
level: "info",
|
|
124
|
-
stream: { write: (msg: string) => logLines.push(msg) },
|
|
125
|
-
},
|
|
126
|
-
}) as FastifyInstance;
|
|
127
|
-
await logCapture.register(scoreAeoRoute);
|
|
128
|
-
await logCapture.ready();
|
|
129
|
-
|
|
130
|
-
const stub: FetchStub = jest.fn();
|
|
131
|
-
globalThis.fetch = stub;
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
await logCapture.inject({
|
|
135
|
-
method: "POST",
|
|
136
|
-
url: "/v1/aeo/score",
|
|
137
|
-
payload: { url: "http://127.0.0.1/admin" },
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const logText = logLines.join("\n");
|
|
141
|
-
expect(logText).toMatch(/127\.0\.0\.1/);
|
|
142
|
-
expect(logText).toMatch(/blocked host/i);
|
|
143
|
-
} finally {
|
|
144
|
-
await logCapture.close();
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe("GET /v1/aeo/score/example", () => {
|
|
150
|
-
let app: FastifyInstance;
|
|
151
|
-
|
|
152
|
-
beforeEach(() => {
|
|
153
|
-
app = buildApp();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
afterEach(async () => {
|
|
157
|
-
await app.close();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("200s with the bundled example, meta.example true, and the illustrative disclosure wire-visible (acceptance-review finding)", async () => {
|
|
161
|
-
const res = await app.inject({ method: "GET", url: "/v1/aeo/score/example" });
|
|
162
|
-
expect(res.statusCode).toBe(200);
|
|
163
|
-
const body = JSON.parse(res.body) as {
|
|
164
|
-
ok: boolean;
|
|
165
|
-
data: { status: string; meta: { example: boolean; sourceUrl: string; illustrative: boolean } };
|
|
166
|
-
};
|
|
167
|
-
expect(body.ok).toBe(true);
|
|
168
|
-
expect(body.data.status).toBe("ready");
|
|
169
|
-
expect(body.data.meta.example).toBe(true);
|
|
170
|
-
expect(body.data.meta.sourceUrl).toBe("https://ailaunchkit.ai");
|
|
171
|
-
// Today's fixture is hand-authored, not a genuine crawl — the caller
|
|
172
|
-
// must not be told this is a live measurement without qualification.
|
|
173
|
-
expect(body.data.meta.illustrative).toBe(true);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AEO score route group (#4666 D5).
|
|
3
|
-
*
|
|
4
|
-
* Routes:
|
|
5
|
-
* POST /v1/aeo/score — score a URL against the four content rules
|
|
6
|
-
* GET /v1/aeo/score/example — the bundled pre-launch annotated example
|
|
7
|
-
*
|
|
8
|
-
* Anonymous, like the sibling `/v1/flows/*` and `/v1/schedule/*` routes — a
|
|
9
|
-
* waitlist respondent has no account. Both routes return `200 { ok: true,
|
|
10
|
-
* data }` for BOTH `ready` and `unavailable` results — an `unavailable`
|
|
11
|
-
* result is a successful call reporting a degraded outcome, not an HTTP
|
|
12
|
-
* error; the caller branches on `data.status`, mirroring how the browser
|
|
13
|
-
* will handle the `computing` state this route never itself emits. Only a
|
|
14
|
-
* malformed request body is a `400 invalid_input`.
|
|
15
|
-
*
|
|
16
|
-
* No MCP twin (a recorded, deliberate exception — brief D7 / apps/api/CLAUDE.md):
|
|
17
|
-
* exposing an arbitrary-URL crawler to anonymous agents is a security-posture
|
|
18
|
-
* decision the AEO Layer 1 MCP tool (`apps/mcp/src/tools/audit.ts`) already
|
|
19
|
-
* declines by staying same-origin-locked.
|
|
20
|
-
*
|
|
21
|
-
* Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
|
|
22
|
-
* `{ ok: false, error: { code, message } }`, never 200-with-ok:false.
|
|
23
|
-
*
|
|
24
|
-
* SSRF-reason leak (security review, AILK #4707): `assertUrlSafe`'s reject
|
|
25
|
-
* `reason` text (e.g. "Host x resolves to a blocked address: 10.1.2.3")
|
|
26
|
-
* is a recon primitive for an anonymous caller mapping internal
|
|
27
|
-
* infrastructure via DNS-rebinding probes. It is logged server-side only
|
|
28
|
-
* (`request.log.info`); the response's `data.message` is replaced with the
|
|
29
|
-
* fixed `SSRF_BLOCKED_CLIENT_MESSAGE` for `reason: "ssrf_blocked"`. The
|
|
30
|
-
* `reason` enum itself is unchanged — it is already generic, and is the
|
|
31
|
-
* discriminant the api-client contract keys on. `flow-engine.ts`'s
|
|
32
|
-
* `aeo_score` step landed the identical scrub for its own caller in #4663
|
|
33
|
-
* (M1) and named this route's gap explicitly as out of that fix's scope —
|
|
34
|
-
* this closes it, reusing that fix's exact client-facing message text for
|
|
35
|
-
* consistency across both `aeo_score` entry points.
|
|
36
|
-
*/
|
|
37
|
-
import { aeoScoreInputSchema } from "@working-theory/validation";
|
|
38
|
-
import type { FastifyPluginAsync } from "fastify";
|
|
39
|
-
|
|
40
|
-
import {
|
|
41
|
-
resolveScoreBudgetMs,
|
|
42
|
-
loadExampleScore,
|
|
43
|
-
scoreUrl,
|
|
44
|
-
} from "../../services/aeo-score.js";
|
|
45
|
-
|
|
46
|
-
const SSRF_BLOCKED_CLIENT_MESSAGE = "This URL cannot be scored.";
|
|
47
|
-
|
|
48
|
-
export const scoreAeoRoute: FastifyPluginAsync = async (fastify) => {
|
|
49
|
-
fastify.post("/v1/aeo/score", async (request, reply) => {
|
|
50
|
-
const parsedBody = aeoScoreInputSchema.safeParse(request.body);
|
|
51
|
-
if (!parsedBody.success) {
|
|
52
|
-
return reply.status(400).send({
|
|
53
|
-
ok: false,
|
|
54
|
-
error: {
|
|
55
|
-
code: "invalid_input",
|
|
56
|
-
message: parsedBody.error.issues[0]?.message ?? "Invalid request body.",
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const budgetMs = resolveScoreBudgetMs(process.env["AEO_SCORE_BUDGET_MS"], {
|
|
62
|
-
warn: (msg) => request.log.warn(msg),
|
|
63
|
-
});
|
|
64
|
-
const data = await scoreUrl(parsedBody.data.url, { budgetMs });
|
|
65
|
-
|
|
66
|
-
const host = safeHost(parsedBody.data.url);
|
|
67
|
-
if (data.status === "ready") {
|
|
68
|
-
request.log.info(
|
|
69
|
-
{ host, pagesAudited: data.meta.pagesAudited, skipped: data.meta.skipped.length, status: data.status },
|
|
70
|
-
"aeo score computed",
|
|
71
|
-
);
|
|
72
|
-
return reply.send({ ok: true, data });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (data.status === "unavailable") {
|
|
76
|
-
// The specific rejection detail (e.g. which internal hostname/IP an
|
|
77
|
-
// SSRF probe resolved to) is logged here, server-side only — never
|
|
78
|
-
// sent to the caller.
|
|
79
|
-
request.log.info(
|
|
80
|
-
{ host, status: data.status, reason: data.reason, message: data.message },
|
|
81
|
-
"aeo score unavailable",
|
|
82
|
-
);
|
|
83
|
-
const clientData: typeof data =
|
|
84
|
-
data.reason === "ssrf_blocked"
|
|
85
|
-
? { ...data, message: SSRF_BLOCKED_CLIENT_MESSAGE }
|
|
86
|
-
: data;
|
|
87
|
-
return reply.send({ ok: true, data: clientData });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return reply.send({ ok: true, data });
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export const exampleAeoRoute: FastifyPluginAsync = async (fastify) => {
|
|
95
|
-
fastify.get("/v1/aeo/score/example", async (_request, reply) => {
|
|
96
|
-
return reply.send({ ok: true, data: loadExampleScore() });
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/** Never log the full URL — host only (§ 5.2 Logging: no PII, no query strings). */
|
|
101
|
-
function safeHost(rawUrl: string): string {
|
|
102
|
-
try {
|
|
103
|
-
return new URL(rawUrl).host;
|
|
104
|
-
} catch {
|
|
105
|
-
return "invalid";
|
|
106
|
-
}
|
|
107
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file auth-tokens.functional.test.ts
|
|
3
|
-
*
|
|
4
|
-
* End-to-end coverage for the three /api/auth/tokens routes once the dual-mode
|
|
5
|
-
* preHandler (#587) is wired in front. Confirms that:
|
|
6
|
-
*
|
|
7
|
-
* - Cookie-mode happy path: cookie present + valid Session row + same-origin
|
|
8
|
-
* POST → handler runs, prisma.apiToken.create is called.
|
|
9
|
-
* - Bearer-mode happy path: Authorization: Bearer header → ApiToken lookup
|
|
10
|
-
* succeeds → handler runs.
|
|
11
|
-
* - Unauthenticated → 401 from the preHandler (handler not reached).
|
|
12
|
-
*
|
|
13
|
-
* Together with `tokens.test.ts` (route-body coverage with userId stub) this
|
|
14
|
-
* locks the spec §5 AC #4 bullet ("All eight existing routes ship functional
|
|
15
|
-
* via cookie AND via bearer; no public paths renamed").
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { createHash } from "node:crypto";
|
|
19
|
-
|
|
20
|
-
import Fastify from "fastify";
|
|
21
|
-
import type { FastifyInstance } from "fastify";
|
|
22
|
-
|
|
23
|
-
// eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
|
|
24
|
-
jest.mock("../../../lib/prisma.js", () => ({
|
|
25
|
-
prisma: {
|
|
26
|
-
session: { findUnique: jest.fn() },
|
|
27
|
-
apiToken: {
|
|
28
|
-
findUnique: jest.fn(),
|
|
29
|
-
findMany: jest.fn(),
|
|
30
|
-
create: jest.fn(),
|
|
31
|
-
update: jest.fn().mockResolvedValue(undefined),
|
|
32
|
-
updateMany: jest.fn(),
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
import { prisma } from "../../../lib/prisma.js";
|
|
38
|
-
import { authPreHandlerPlugin } from "../../../middleware/auth.js";
|
|
39
|
-
import { tokenRoutes } from "../tokens.js";
|
|
40
|
-
|
|
41
|
-
const FRONTEND = "https://app.example.com";
|
|
42
|
-
|
|
43
|
-
async function buildApp(): Promise<FastifyInstance> {
|
|
44
|
-
const app = Fastify();
|
|
45
|
-
app.register(authPreHandlerPlugin, { trustedOrigins: [FRONTEND] });
|
|
46
|
-
app.register(tokenRoutes);
|
|
47
|
-
await app.ready();
|
|
48
|
-
return app;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("tokens routes — functional via dual-mode preHandler", () => {
|
|
52
|
-
let app: FastifyInstance;
|
|
53
|
-
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
jest.clearAllMocks();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
afterEach(async () => {
|
|
59
|
-
await app.close();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("cookie mode", () => {
|
|
63
|
-
it("POST /api/auth/tokens succeeds when authenticated via cookie + same-origin", async () => {
|
|
64
|
-
(prisma.session.findUnique as jest.Mock).mockResolvedValue({
|
|
65
|
-
userId: "user_cookie",
|
|
66
|
-
expiresAt: new Date(Date.now() + 60_000),
|
|
67
|
-
});
|
|
68
|
-
(prisma.apiToken.create as jest.Mock).mockResolvedValue({
|
|
69
|
-
id: "tok_1",
|
|
70
|
-
name: "desktop",
|
|
71
|
-
tokenPrefix: "tok_abcdef",
|
|
72
|
-
clientType: "api",
|
|
73
|
-
scopes: [],
|
|
74
|
-
expiresAt: null,
|
|
75
|
-
createdAt: new Date(),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
app = await buildApp();
|
|
79
|
-
const res = await app.inject({
|
|
80
|
-
method: "POST",
|
|
81
|
-
url: "/v1/auth/tokens",
|
|
82
|
-
headers: {
|
|
83
|
-
cookie: "better-auth.session_token=sess_abc",
|
|
84
|
-
origin: FRONTEND,
|
|
85
|
-
},
|
|
86
|
-
payload: { name: "desktop" },
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(res.statusCode).toBe(201);
|
|
90
|
-
expect(prisma.apiToken.create).toHaveBeenCalled();
|
|
91
|
-
expect(
|
|
92
|
-
(prisma.apiToken.create as jest.Mock).mock.calls[0][0].data.userId,
|
|
93
|
-
).toBe("user_cookie");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("GET /api/auth/tokens succeeds when authenticated via cookie (no Origin needed)", async () => {
|
|
97
|
-
(prisma.session.findUnique as jest.Mock).mockResolvedValue({
|
|
98
|
-
userId: "user_cookie",
|
|
99
|
-
expiresAt: new Date(Date.now() + 60_000),
|
|
100
|
-
});
|
|
101
|
-
(prisma.apiToken.findMany as jest.Mock).mockResolvedValue([]);
|
|
102
|
-
|
|
103
|
-
app = await buildApp();
|
|
104
|
-
const res = await app.inject({
|
|
105
|
-
method: "GET",
|
|
106
|
-
url: "/v1/auth/tokens",
|
|
107
|
-
headers: { cookie: "better-auth.session_token=sess_abc" },
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
expect(res.statusCode).toBe(200);
|
|
111
|
-
expect(prisma.apiToken.findMany).toHaveBeenCalledWith(
|
|
112
|
-
expect.objectContaining({
|
|
113
|
-
where: { userId: "user_cookie", revokedAt: null },
|
|
114
|
-
}),
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("bearer mode", () => {
|
|
120
|
-
it("GET /api/auth/tokens succeeds when authenticated via Authorization: Bearer", async () => {
|
|
121
|
-
const bearer = "tok_live_zzz";
|
|
122
|
-
const hash = createHash("sha256").update(bearer).digest("hex");
|
|
123
|
-
(prisma.apiToken.findUnique as jest.Mock).mockResolvedValue({
|
|
124
|
-
id: "tok_id",
|
|
125
|
-
userId: "user_bear",
|
|
126
|
-
expiresAt: null,
|
|
127
|
-
revokedAt: null,
|
|
128
|
-
});
|
|
129
|
-
(prisma.apiToken.findMany as jest.Mock).mockResolvedValue([]);
|
|
130
|
-
|
|
131
|
-
app = await buildApp();
|
|
132
|
-
const res = await app.inject({
|
|
133
|
-
method: "GET",
|
|
134
|
-
url: "/v1/auth/tokens",
|
|
135
|
-
headers: { authorization: `Bearer ${bearer}` },
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(res.statusCode).toBe(200);
|
|
139
|
-
expect(prisma.apiToken.findUnique).toHaveBeenCalledWith({
|
|
140
|
-
where: { token: hash },
|
|
141
|
-
select: { id: true, userId: true, expiresAt: true, revokedAt: true },
|
|
142
|
-
});
|
|
143
|
-
expect(prisma.apiToken.findMany).toHaveBeenCalledWith(
|
|
144
|
-
expect.objectContaining({
|
|
145
|
-
where: { userId: "user_bear", revokedAt: null },
|
|
146
|
-
}),
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("DELETE /api/auth/tokens/:id succeeds via bearer; revokes the row", async () => {
|
|
151
|
-
const bearer = "tok_live_zzz";
|
|
152
|
-
// Well-formed apitok_ id (ADR-0031): the :id param is validated with
|
|
153
|
-
// zApiTokenId before any query runs.
|
|
154
|
-
const targetId = "apitok_00000000-0000-7000-8000-000000000009";
|
|
155
|
-
(prisma.apiToken.findUnique as jest.Mock).mockResolvedValue({
|
|
156
|
-
id: "tok_id",
|
|
157
|
-
userId: "user_bear",
|
|
158
|
-
expiresAt: null,
|
|
159
|
-
revokedAt: null,
|
|
160
|
-
});
|
|
161
|
-
(prisma.apiToken.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
|
|
162
|
-
|
|
163
|
-
app = await buildApp();
|
|
164
|
-
const res = await app.inject({
|
|
165
|
-
method: "DELETE",
|
|
166
|
-
url: `/v1/auth/tokens/${targetId}`,
|
|
167
|
-
headers: { authorization: `Bearer ${bearer}` },
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
expect(res.statusCode).toBe(200);
|
|
171
|
-
expect(prisma.apiToken.updateMany).toHaveBeenCalledWith({
|
|
172
|
-
where: { id: targetId, userId: "user_bear", revokedAt: null },
|
|
173
|
-
data: { revokedAt: expect.any(Date) },
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
describe("unauthenticated", () => {
|
|
179
|
-
it.each([
|
|
180
|
-
["POST", "/v1/auth/tokens"],
|
|
181
|
-
["GET", "/v1/auth/tokens"],
|
|
182
|
-
["DELETE", "/v1/auth/tokens/tok_x"],
|
|
183
|
-
])("%s %s → 401 (handler never runs)", async (method, url) => {
|
|
184
|
-
app = await buildApp();
|
|
185
|
-
const res = await app.inject({
|
|
186
|
-
method: method as "POST" | "GET" | "DELETE",
|
|
187
|
-
url,
|
|
188
|
-
payload: method === "POST" ? { name: "x" } : undefined,
|
|
189
|
-
});
|
|
190
|
-
expect(res.statusCode).toBe(401);
|
|
191
|
-
expect(prisma.apiToken.create).not.toHaveBeenCalled();
|
|
192
|
-
expect(prisma.apiToken.findMany).not.toHaveBeenCalled();
|
|
193
|
-
expect(prisma.apiToken.updateMany).not.toHaveBeenCalled();
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|