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,1907 +1,142 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AI Launch Kit API Server
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* in
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* AI Launch Kit API Server — the boot's composition (#9564 D5).
|
|
3
|
+
*
|
|
4
|
+
* The server is `@working-theory/api` (#9563 D8 PR 1): the base routes,
|
|
5
|
+
* services, middleware and stores, and `buildServer({ modules, topology,
|
|
6
|
+
* config })`, live in the package. This file is what a site owns: it loads
|
|
7
|
+
* the six module objects and hands them to the package, which registers
|
|
8
|
+
* every row of the registration table whose predicate holds over the
|
|
9
|
+
* registered set — the modules `AILK_ENABLED_MODULES` selects whose claim
|
|
10
|
+
* read answered `claimed` (#9565 D4/D5). The route table a build serves is
|
|
11
|
+
* the one it served before the move (`src/__tests__/registration-table.test.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Exports the same zero-argument `build()` for both:
|
|
14
|
+
* - Vercel serverless deployment (api/index.ts → src/vercel-handler.ts)
|
|
10
15
|
* - Local development (src/index.ts)
|
|
16
|
+
*
|
|
17
|
+
* ONE MODULE-LEVEL CONSTANT PER MODULE OBJECT — the #5304 form. The specifier
|
|
18
|
+
* is never written at the `import()` call site, so `tsc` does not resolve it
|
|
19
|
+
* and the public and keyless trees, which carry no module file, still
|
|
20
|
+
* compile; it IS a module-level constant dereferenced into `import()`, so
|
|
21
|
+
* `@vercel/nft` constant-propagates it and the deployed artifact carries the
|
|
22
|
+
* file. `scripts/enumerate-slice-specifiers.ts` and
|
|
23
|
+
* `scripts/check-slice-reachability.ts` re-measure both properties on every
|
|
24
|
+
* run. Every load site routes through `loadSlice()` so a bundled build says
|
|
25
|
+
* so at boot rather than failing mute (#5334; `apps/api/CLAUDE.md` § Deploy
|
|
26
|
+
* path). All six are loaded and passed on every boot (#9565 D5): the
|
|
27
|
+
* selection is `AILK_ENABLED_MODULES`, read by the package, and the claim is
|
|
28
|
+
* each package's own licence read — a passed module that is not selected
|
|
29
|
+
* registers nothing.
|
|
30
|
+
*
|
|
31
|
+
* Each `./modules/<name>.ts` file is a one-statement `// oss: false` re-export
|
|
32
|
+
* of `module: ApiModule` from `@working-theory/api-<name>` — the paid package
|
|
33
|
+
* that owns the routes, the plugin map and the claim read.
|
|
11
34
|
*/
|
|
12
|
-
import cors from "@fastify/cors";
|
|
13
|
-
import rateLimit from "@fastify/rate-limit";
|
|
14
|
-
import { logger } from "@working-theory/observability";
|
|
15
|
-
import Fastify from "fastify";
|
|
16
|
-
import type {
|
|
17
|
-
FastifyPluginAsync,
|
|
18
|
-
FastifyPluginCallback,
|
|
19
|
-
FastifyReply,
|
|
20
|
-
FastifyRequest,
|
|
21
|
-
} from "fastify";
|
|
22
|
-
|
|
23
|
-
import { config } from "./config/index.js";
|
|
24
|
-
import { isModuleEnabled, servedModules } from "./config/modules.js";
|
|
25
|
-
import { redactLogPath, registerErrorSerializer } from "./lib/error-serializer.js";
|
|
26
|
-
import { validateLeadCaptureDir } from "./lib/lead-capture-store.js";
|
|
27
|
-
import { registerLicenseKeyStore } from "./lib/license-key-store.js";
|
|
28
|
-
import { requireCommerceClaim } from "./lib/require-commerce-claim.js";
|
|
29
|
-
import { assertSiteAliasResolves } from "./lib/site.js";
|
|
30
|
-
import { loadSlice } from "./lib/slice-load.js";
|
|
31
|
-
import { authPreHandlerPlugin } from "./middleware/auth.js";
|
|
32
|
-
import { defaultTenantPreHandlerPlugin } from "./middleware/default-tenant.js";
|
|
33
|
-
import {
|
|
34
|
-
rateLimitMiddleware,
|
|
35
|
-
buildTrustedKeyDigests,
|
|
36
|
-
DEFAULT_SCHEDULE_PER_KEY_PER_MINUTE,
|
|
37
|
-
DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
38
|
-
DEFAULT_SIGNUPEVIDENCE_PER_KEY_PER_MINUTE,
|
|
39
|
-
resolveRateLimitIdentity,
|
|
40
|
-
SITE_KEY_IDENTITY_PREFIX,
|
|
41
|
-
siteKeyReader,
|
|
42
|
-
} from "./middleware/rate-limit.js";
|
|
43
|
-
import { openapiRoutes } from "./openapi/index.js";
|
|
44
|
-
import {
|
|
45
|
-
deriveRootEndpoints,
|
|
46
|
-
REGISTERED_ROUTES_DECORATOR,
|
|
47
|
-
API_VERSION,
|
|
48
|
-
apiDescription,
|
|
49
|
-
apiDisplayName,
|
|
50
|
-
resolveApiIdentity,
|
|
51
|
-
} from "./openapi/spec.js";
|
|
52
|
-
import type { RegisteredRoute } from "./openapi/surface-drift.js";
|
|
53
|
-
import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
|
|
54
35
|
import {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
consentSignUpRoute,
|
|
66
|
-
consentUnsubscribeRoute,
|
|
67
|
-
} from "./routes/consent/index.js";
|
|
68
|
-
import { getPageRoute } from "./routes/content/get-page.js";
|
|
69
|
-
import { listPagesRoute } from "./routes/content/list-pages.js";
|
|
70
|
-
import { listTypesRoute } from "./routes/content/list-types.js";
|
|
71
|
-
import { offeringsRoute } from "./routes/content/offerings.js";
|
|
72
|
-
import { pricingRoute } from "./routes/content/pricing.js";
|
|
73
|
-
import {
|
|
74
|
-
CONTENT_RATE_LIMIT_AUTH,
|
|
75
|
-
CONTENT_RATE_LIMIT_UNAUTH,
|
|
76
|
-
CONTENT_RATE_LIMIT_WINDOW,
|
|
77
|
-
} from "./routes/content/rate-limit.js";
|
|
78
|
-
import { searchContentRoute } from "./routes/content/search.js";
|
|
79
|
-
import {
|
|
80
|
-
shownOfferingsRoute,
|
|
81
|
-
type ShownOfferingsRouteOptions,
|
|
82
|
-
} from "./routes/content/shown-offerings.js";
|
|
83
|
-
import {
|
|
84
|
-
leadCallbackRoute,
|
|
85
|
-
leadCaptureStatusRoute,
|
|
86
|
-
leadSubmitRoute,
|
|
87
|
-
leadSubscribeRoute,
|
|
88
|
-
} from "./routes/leads/index.js";
|
|
89
|
-
import { leadListRoute } from "./routes/leads/list.js";
|
|
90
|
-
import { requestRoutes } from "./routes/requests/index.js";
|
|
91
|
-
import {
|
|
92
|
-
registerTestAuthRoute,
|
|
93
|
-
shouldMountTestAuth,
|
|
94
|
-
} from "./routes/test/auth/session.js";
|
|
95
|
-
import { consentLinkSecretConfigured } from "./services/consent.js";
|
|
96
|
-
import { stripeCatalogSourceFor } from "./services/offerings/source.js";
|
|
97
|
-
import { resolveCallerSite } from "./services/public-site-resolver.js";
|
|
36
|
+
buildServer,
|
|
37
|
+
config,
|
|
38
|
+
OPTIONAL_MODULES,
|
|
39
|
+
type ApiHub,
|
|
40
|
+
type ApiModule,
|
|
41
|
+
type OptionalModule,
|
|
42
|
+
} from "@working-theory/api";
|
|
43
|
+
import { loadSlice } from "@working-theory/api/lib/slice-load.js";
|
|
44
|
+
import { logger } from "@working-theory/observability";
|
|
45
|
+
import type { FastifyInstance } from "fastify";
|
|
98
46
|
|
|
99
47
|
/**
|
|
100
|
-
*
|
|
101
|
-
* the
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* Nothing that identifies a request is lost: a route parameter such as `:id`
|
|
111
|
-
* lives in the PATH and survives. Only the secret after `?` is dropped.
|
|
112
|
-
*
|
|
113
|
-
* Path segments that are themselves credentials or identifiers — a `cs_…`
|
|
114
|
-
* Checkout Session id (#6084 D14), a `lead_…` id (#6216 D11), a `person_…` id
|
|
115
|
-
* (#8438 SEC-8438-10) and a webhook connection token (#8932 D10) — are
|
|
116
|
-
* redacted by `redactLogPath` (`lib/error-serializer.ts`), the ONE path
|
|
117
|
-
* redactor this serializer and the global error handler share, so a request
|
|
118
|
-
* line and an error line can never disagree about what a path may carry.
|
|
119
|
-
* Fastify's automatic "incoming request" line is emitted by its internal hook
|
|
120
|
-
* BEFORE any route hook can intervene, so no route-level discipline can fix
|
|
121
|
-
* it; this is the only place that can.
|
|
48
|
+
* The hub package (#9571 D5), the same specifier-constant form as the module
|
|
49
|
+
* objects below: `tsc` never resolves it, so the public tree — which carries
|
|
50
|
+
* no `packages-paid/` — still compiles, and `@vercel/nft` traces it.
|
|
51
|
+
* INSTALLED means the package's manifest (its `./package.json` export)
|
|
52
|
+
* resolves from this composition; the manifest imports nothing, so a
|
|
53
|
+
* not-found there is exactly "the package is absent". Any error loading the
|
|
54
|
+
* package itself — a missing transitive dependency, an untraced file — is
|
|
55
|
+
* rethrown and stops the boot: a broken install never boots as a deployment
|
|
56
|
+
* with no hub (SEC-9571-5).
|
|
122
57
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
58
|
+
const HUB_PACKAGE = "@working-theory/api-hub";
|
|
59
|
+
const HUB_MANIFEST = "@working-theory/api-hub/package.json";
|
|
60
|
+
|
|
61
|
+
const MODULE_NOT_FOUND_CODES = new Set(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"]);
|
|
62
|
+
|
|
63
|
+
async function loadInstalledHub(): Promise<ApiHub | undefined> {
|
|
64
|
+
try {
|
|
65
|
+
await import(HUB_MANIFEST, { with: { type: "json" } });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const code = (error as { code?: unknown } | null | undefined)?.code;
|
|
68
|
+
if (typeof code === "string" && MODULE_NOT_FOUND_CODES.has(code)) return undefined;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const mod = (await import(HUB_PACKAGE)) as { hub?: ApiHub };
|
|
72
|
+
if (!mod.hub) {
|
|
73
|
+
throw new Error(`hub export missing from ${HUB_PACKAGE}`);
|
|
74
|
+
}
|
|
75
|
+
return mod.hub;
|
|
130
76
|
}
|
|
131
77
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
* one, byte for byte; only the log line changes.
|
|
139
|
-
*/
|
|
140
|
-
export function notFoundHandler(request: FastifyRequest, reply: FastifyReply) {
|
|
141
|
-
const { method, url = "" } = request.raw;
|
|
142
|
-
request.log.info(`Route ${method}:${redactLogPath(url)} not found`);
|
|
143
|
-
return reply.code(404).send({
|
|
144
|
-
message: `Route ${method}:${url} not found`,
|
|
145
|
-
error: "Not Found",
|
|
146
|
-
statusCode: 404,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
78
|
+
const MULTI_TENANT_MODULE = "./modules/multi-tenant.js";
|
|
79
|
+
const WORKSPACE_MODULE = "./modules/workspace.js";
|
|
80
|
+
const SCHEDULING_MODULE = "./modules/scheduling.js";
|
|
81
|
+
const SIGNAL_COLLECTORS_MODULE = "./modules/signal-collectors.js";
|
|
82
|
+
const COMMERCE_MODULE = "./modules/commerce.js";
|
|
83
|
+
const CONTENT_WRITE_MODULE = "./modules/content-write.js";
|
|
149
84
|
|
|
150
85
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* Every value here names a file the OSS strip removes. A shipping file may not
|
|
155
|
-
* name a stripped file in ANY import form (spec D1): `import()` defers
|
|
156
|
-
* EVALUATION, not RESOLUTION, so `tsc` resolves a string-literal specifier
|
|
157
|
-
* whether or not the `isModuleEnabled()` branch containing it can ever run. The
|
|
158
|
-
* ten literal `await import()` calls that used to sit at these call sites were
|
|
159
|
-
* therefore ten TS2307 errors on the public tree, and the static
|
|
160
|
-
* `./middleware/tenant.js` import was an eleventh — measured on a stripped
|
|
161
|
-
* tree, not argued.
|
|
162
|
-
*
|
|
163
|
-
* Keeping the specifier off the `import()` call site is what defeats that
|
|
164
|
-
* resolution, and the INDIRECTION — not the type of the value — is what does
|
|
165
|
-
* the work. WHICH FORM of indirection is not free, and #5304 is the correction:
|
|
166
|
-
* reading the specifier out of a TABLE, as this file did until then, hid it
|
|
167
|
-
* from the deploy-time file tracer as well as from `tsc`, so the files were
|
|
168
|
-
* compiled and never shipped. The form below — a module-level constant
|
|
169
|
-
* dereferenced into `import()` — is unresolvable to `tsc` and traceable to
|
|
170
|
-
* `@vercel/nft`, the only pair of properties that satisfies both the strip and
|
|
171
|
-
* the deployment. S1's `isModuleEnabled()` gate is unchanged and still decides
|
|
172
|
-
* whether the branch runs; `AILK_ENABLED_MODULES` unset still means every
|
|
173
|
-
* module on, so a default build registers exactly what it registered before.
|
|
174
|
-
*
|
|
175
|
-
* This mechanism is sound here because `apps/api` is a `tsc`-only surface with
|
|
176
|
-
* no bundler. It is NOT sound in `packages/ui`: webpack cannot statically
|
|
177
|
-
* analyse a non-literal `import()` either, and compiles it to an async context
|
|
178
|
-
* with no members, so the module silently fails to load on the tree that HAS
|
|
179
|
-
* it. See the note above the imports in `packages/ui/src/sections/SectionRenderer.tsx`.
|
|
180
|
-
*
|
|
181
|
-
* BUNDLING `apps/api` IS NOT A SUPPORTED DEPLOY PATH, and that is a property of
|
|
182
|
-
* the analysers rather than a posture (#5334). esbuild bundles a dynamic
|
|
183
|
-
* `import()` only when its argument is a string literal at the call site —
|
|
184
|
-
* exactly the predicate `tsc` resolves on, and exactly the one the strip
|
|
185
|
-
* forbids. So no expression shape can satisfy both, and in a bundled build
|
|
186
|
-
* these specifiers survive verbatim and resolve against the BUNDLE OUTPUT
|
|
187
|
-
* directory instead of their source module's: `"./list.js"` in
|
|
188
|
-
* `routes/project-listings/index.ts` becomes `dist/list.js` and is not there.
|
|
189
|
-
* esbuild does not warn. `build()` then throws during CONSTRUCTION, so every
|
|
190
|
-
* route 500s rather than one — measured in production, nine hours of total API
|
|
191
|
-
* outage on a consuming site. The supported path is what the scaffold ships:
|
|
192
|
-
* plain `tsc` output traced by `@vercel/node` (`apps/api/vercel.json`,
|
|
193
|
-
* `framework: null`). If you are syncing this file into a repo that bundles,
|
|
194
|
-
* that is the constraint you have just inherited; `apps/api/CLAUDE.md`
|
|
195
|
-
* § Deploy path says what to do instead. Every load site below routes through
|
|
196
|
-
* `loadSlice()` so a bundled build says so at boot rather than failing mute.
|
|
197
|
-
*
|
|
198
|
-
* `./routes/project-listings/index.js` carries two independent load paths on
|
|
199
|
-
* purpose: its five anonymous-surface exports are reached below through
|
|
200
|
-
* `PROJECT_LISTINGS_INDEX` / `SLICE_PLUGINS`, now that signal-collectors gates
|
|
201
|
-
* their registration (D4), while the tenant scope's own `import()` of the same
|
|
202
|
-
* specifier stays a literal — the file is a MIXED directory that always ships
|
|
203
|
-
* (multi-tenant's `TENANT_HANDLERS` table lives in the same file), so a literal
|
|
204
|
-
* there resolves fine on every tree regardless of module selection.
|
|
205
|
-
*/
|
|
206
|
-
/**
|
|
207
|
-
* One module-level constant per slice module. The call sites below read the
|
|
208
|
-
* loader out of `SLICE_PLUGINS`; the specifier itself is never written at an
|
|
209
|
-
* `import()` call site, which is what keeps `tsc` from resolving it (#5182 D1).
|
|
210
|
-
*
|
|
211
|
-
* The constant is not a style choice — it is the mechanism (#5304 D3).
|
|
212
|
-
* `@vercel/nft` constant-propagates a module-level binding into `import()` and
|
|
213
|
-
* emits the file; it cannot evaluate a table lookup or a for-of destructure,
|
|
214
|
-
* which is what the previous `Record<string, string>` shape forced it to do.
|
|
215
|
-
* So the old shape was invisible to the DEPLOY tracer as well as to `tsc`, the
|
|
216
|
-
* files went untraced and unshipped, and `build()` threw ERR_MODULE_NOT_FOUND
|
|
217
|
-
* on the first slice it reached — measured in production, nine hours of total
|
|
218
|
-
* API outage on a consuming site. Both analysers are re-measured on every run
|
|
219
|
-
* by `scripts/__tests__/enumerate-slice-specifiers.test.ts`, and
|
|
220
|
-
* `scripts/check-slice-reachability.ts` fails when any specifier here stops
|
|
221
|
-
* being traced.
|
|
222
|
-
*/
|
|
223
|
-
const WORKSPACES_INDEX = "./routes/workspaces/index.js";
|
|
224
|
-
const WORKSPACES_SESSIONS = "./routes/workspaces/sessions.js";
|
|
225
|
-
const WORKSPACES_CONFIG = "./routes/workspaces/config.js";
|
|
226
|
-
const WORKSPACES_GENERAL = "./routes/workspaces/general.js";
|
|
227
|
-
const WORKSPACES_MEMBERS = "./routes/workspaces/members.js";
|
|
228
|
-
// #6216 — a SIBLING specifier registered in the SAME workspace/multi-tenant
|
|
229
|
-
// scope as WORKSPACES_GENERAL/WORKSPACES_MEMBERS below, not the
|
|
230
|
-
// multi-tenant-only family WORKSPACES_MEETINGS/WORKSPACES_CONFIG sit in.
|
|
231
|
-
const WORKSPACES_MESSAGES = "./routes/workspaces/messages.js";
|
|
232
|
-
// #7535 — the Inbox submissions and subscribers reads, siblings of
|
|
233
|
-
// WORKSPACES_MESSAGES in the same workspace/multi-tenant scope.
|
|
234
|
-
const WORKSPACES_SUBMISSIONS = "./routes/workspaces/submissions.js";
|
|
235
|
-
// #9170 — the Inbox orders read, the SAME scope.
|
|
236
|
-
const WORKSPACES_ORDERS = "./routes/workspaces/orders.js";
|
|
237
|
-
const WORKSPACES_SUBSCRIBERS = "./routes/workspaces/subscribers.js";
|
|
238
|
-
// #852 D8 — the People reads (the workspace's clients), the SAME scope.
|
|
239
|
-
const WORKSPACES_PEOPLE = "./routes/workspaces/people.js";
|
|
240
|
-
// #7877 D9 — a client's pieces, plan and outbound messages, and the
|
|
241
|
-
// workspace's piece stages: the writes owners, admins and (#8514) members
|
|
242
|
-
// make, the SAME scope.
|
|
243
|
-
const WORKSPACES_PIECES = "./routes/workspaces/pieces.js";
|
|
244
|
-
const WORKSPACES_PLAN = "./routes/workspaces/plan.js";
|
|
245
|
-
const WORKSPACES_MESSAGES_OUT = "./routes/workspaces/messages-out.js";
|
|
246
|
-
// #8171 D8 — the workspace's Requests read (the inbound member rows), the
|
|
247
|
-
// SAME scope.
|
|
248
|
-
const WORKSPACES_REQUESTS = "./routes/workspaces/requests.js";
|
|
249
|
-
// #9254 DD5 — the workspace's Callbacks read (request-and-confirm rows), the
|
|
250
|
-
// SAME scope.
|
|
251
|
-
const WORKSPACES_CALLBACKS = "./routes/workspaces/callbacks.js";
|
|
252
|
-
// #9287 DD2 — the owner's Start, Get and Extend trial, the SAME scope.
|
|
253
|
-
const WORKSPACES_TRIAL = "./routes/workspaces/trial.js";
|
|
254
|
-
// #7877 D11 — the member reads: auth only, under the SAME module predicate.
|
|
255
|
-
const MEMBER_PIECES = "./routes/member/pieces.js";
|
|
256
|
-
const MEMBER_MESSAGES = "./routes/member/messages.js";
|
|
257
|
-
// #8169 D9 — the Phase 1 member reads. Five under the SAME predicate; the two
|
|
258
|
-
// that reach services/scheduling/ under scheduling && (workspace ||
|
|
259
|
-
// multi-tenant), because only the scheduling module delivers that directory.
|
|
260
|
-
const MEMBER_PURCHASES = "./routes/member/purchases.js";
|
|
261
|
-
const MEMBER_APPOINTMENTS = "./routes/member/appointments.js";
|
|
262
|
-
const MEMBER_ARTIFACTS = "./routes/member/artifacts.js";
|
|
263
|
-
const MEMBER_REQUEST_FORM = "./routes/member/request-form.js";
|
|
264
|
-
const MEMBER_BILLING = "./routes/member/billing.js";
|
|
265
|
-
const MEMBER_MEETING_TYPES = "./routes/member/meeting-types.js";
|
|
266
|
-
const MEMBER_AVAILABILITY = "./routes/member/availability.js";
|
|
267
|
-
// #8170 D11 — the member writes. Four under the tenancy predicate (the
|
|
268
|
-
// message POST rides MEMBER_MESSAGES); the booking, which reaches
|
|
269
|
-
// services/scheduling/, under scheduling && (workspace || multi-tenant).
|
|
270
|
-
const MEMBER_REQUESTS = "./routes/member/requests.js";
|
|
271
|
-
const MEMBER_ARTIFACT_REVIEW = "./routes/member/artifact-review.js";
|
|
272
|
-
const MEMBER_PROFILE = "./routes/member/profile.js";
|
|
273
|
-
const MEMBER_DELETION_REQUEST = "./routes/member/deletion-request.js";
|
|
274
|
-
const MEMBER_BOOKINGS = "./routes/member/bookings.js";
|
|
275
|
-
// #8438 D6 — the member's own consents, under the tenancy predicate beside the
|
|
276
|
-
// other member routes. D8 — the Admin owner's consents to Working Theory,
|
|
277
|
-
// under multi-tenant alone (the module that contributes the Admin app).
|
|
278
|
-
const MEMBER_CONSENTS = "./routes/member/consents.js";
|
|
279
|
-
// #8515 D13 — the client's onboarding read and write, under the tenancy
|
|
280
|
-
// predicate beside the request-quote pair.
|
|
281
|
-
const MEMBER_ONBOARDING = "./routes/member/onboarding.js";
|
|
282
|
-
const CONSENT_PLATFORM = "./routes/consent/platform.js";
|
|
283
|
-
// #3591 PR-B — the agent seat's management routes and an agent's own
|
|
284
|
-
// context, siblings of WORKSPACES_MEMBERS in the same workspace/multi-tenant
|
|
285
|
-
// scope.
|
|
286
|
-
const WORKSPACES_AGENTS = "./routes/workspaces/agents.js";
|
|
287
|
-
// #6183 S9b — a SEPARATE specifier from WORKSPACES_CONFIG: this route is
|
|
288
|
-
// registered under isModuleEnabled("scheduling") alone, never the
|
|
289
|
-
// workspace/multi-tenant scope config.js shares with general.js/members.js
|
|
290
|
-
// (see config.ts's own header for why).
|
|
291
|
-
const WORKSPACES_MEETINGS = "./routes/workspaces/meetings.js";
|
|
292
|
-
const WORKSPACES_CONTENT = "./routes/workspaces/content.js";
|
|
293
|
-
const ASSETS_UPLOAD = "./routes/assets/upload.js";
|
|
294
|
-
const ASSETS_READ = "./routes/assets/read.js";
|
|
295
|
-
const ASSETS_MANAGE = "./routes/assets/manage.js";
|
|
296
|
-
const BILLING_INDEX = "./routes/billing/index.js";
|
|
297
|
-
const WORKSPACES_OFFERINGS = "./routes/workspaces/offerings.js";
|
|
298
|
-
const WAITLIST_EXPERIMENTS_INDEX = "./routes/waitlist-experiments/index.js";
|
|
299
|
-
const SCHEDULE_INDEX = "./routes/schedule/index.js";
|
|
300
|
-
const FLOWS_INDEX = "./routes/flows/index.js";
|
|
301
|
-
const DELIVERABLES_INDEX = "./routes/deliverables/index.js";
|
|
302
|
-
const PROJECT_LISTINGS_INDEX = "./routes/project-listings/index.js";
|
|
303
|
-
const SLACK_INDEX = "./routes/slack/index.js";
|
|
304
|
-
const WAITLIST_SIGNUPS_INDEX = "./routes/waitlist-signups/index.js";
|
|
305
|
-
const CHECKOUT_INDEX = "./routes/checkout/index.js";
|
|
306
|
-
const WEBHOOKS_INDEX = "./routes/webhooks/index.js";
|
|
307
|
-
const FLOW_CHECKOUTS_INDEX = "./routes/flow-checkouts/index.js";
|
|
308
|
-
const SUBSCRIPTION_INDEX = "./routes/subscription/index.js";
|
|
309
|
-
const RECOMMENDER_INDEX = "./routes/flows/recommender.js";
|
|
310
|
-
const MAGNETS_INDEX = "./routes/magnets/index.js";
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Paid-module SLICE entry points, keyed by the export each branch needs.
|
|
314
|
-
*
|
|
315
|
-
* `specifier` is carried alongside `load` for the drift message below, and both
|
|
316
|
-
* read the SAME constant, so the two can never disagree. `load` is what runs:
|
|
317
|
-
* an arrow closing over that constant, which is the only shape both analysers
|
|
86
|
+
* `specifier` is carried alongside `load` for `loadSlice()`'s diagnostic, and
|
|
87
|
+
* both read the SAME constant, so the two can never disagree. `load` is what
|
|
88
|
+
* runs: an arrow closing over that constant, the only shape both analysers
|
|
318
89
|
* treat as intended — unresolvable to `tsc`, traceable to `nft`.
|
|
319
90
|
*/
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
load: () => import(WORKSPACES_INDEX),
|
|
328
|
-
},
|
|
329
|
-
sessionRoutes: {
|
|
330
|
-
specifier: WORKSPACES_SESSIONS,
|
|
331
|
-
load: () => import(WORKSPACES_SESSIONS),
|
|
332
|
-
},
|
|
333
|
-
workspaceConfigRoutes: {
|
|
334
|
-
specifier: WORKSPACES_CONFIG,
|
|
335
|
-
load: () => import(WORKSPACES_CONFIG),
|
|
336
|
-
},
|
|
337
|
-
workspaceGeneralRoutes: {
|
|
338
|
-
specifier: WORKSPACES_GENERAL,
|
|
339
|
-
load: () => import(WORKSPACES_GENERAL),
|
|
340
|
-
},
|
|
341
|
-
workspaceMembersRoutes: {
|
|
342
|
-
specifier: WORKSPACES_MEMBERS,
|
|
343
|
-
load: () => import(WORKSPACES_MEMBERS),
|
|
344
|
-
},
|
|
345
|
-
// #6216 — the Inbox message API, the SAME scope as the two above.
|
|
346
|
-
workspaceMessageRoutes: {
|
|
347
|
-
specifier: WORKSPACES_MESSAGES,
|
|
348
|
-
load: () => import(WORKSPACES_MESSAGES),
|
|
349
|
-
},
|
|
350
|
-
// #7535 — the Inbox submissions and subscribers reads, the SAME scope.
|
|
351
|
-
workspaceSubmissionRoutes: {
|
|
352
|
-
specifier: WORKSPACES_SUBMISSIONS,
|
|
353
|
-
load: () => import(WORKSPACES_SUBMISSIONS),
|
|
354
|
-
},
|
|
355
|
-
// #9170 — the Inbox orders read, the SAME scope.
|
|
356
|
-
workspaceOrderRoutes: {
|
|
357
|
-
specifier: WORKSPACES_ORDERS,
|
|
358
|
-
load: () => import(WORKSPACES_ORDERS),
|
|
359
|
-
},
|
|
360
|
-
workspaceSubscriberRoutes: {
|
|
361
|
-
specifier: WORKSPACES_SUBSCRIBERS,
|
|
362
|
-
load: () => import(WORKSPACES_SUBSCRIBERS),
|
|
363
|
-
},
|
|
364
|
-
// #852 D8 — the People list and Person record, the SAME scope.
|
|
365
|
-
workspacePeopleRoutes: {
|
|
366
|
-
specifier: WORKSPACES_PEOPLE,
|
|
367
|
-
load: () => import(WORKSPACES_PEOPLE),
|
|
368
|
-
},
|
|
369
|
-
// #7877 D9 — the member area's operator routes (owners, admins and, since
|
|
370
|
-
// #8514, members), the SAME scope.
|
|
371
|
-
workspacePieceRoutes: {
|
|
372
|
-
specifier: WORKSPACES_PIECES,
|
|
373
|
-
load: () => import(WORKSPACES_PIECES),
|
|
374
|
-
},
|
|
375
|
-
// #8171 D8 — the Requests read, the SAME scope.
|
|
376
|
-
workspaceRequestRoutes: {
|
|
377
|
-
specifier: WORKSPACES_REQUESTS,
|
|
378
|
-
load: () => import(WORKSPACES_REQUESTS),
|
|
379
|
-
},
|
|
380
|
-
// #9254 DD5 — the Callbacks read, the SAME scope.
|
|
381
|
-
workspaceCallbackRoutes: {
|
|
382
|
-
specifier: WORKSPACES_CALLBACKS,
|
|
383
|
-
load: () => import(WORKSPACES_CALLBACKS),
|
|
384
|
-
},
|
|
385
|
-
// #9287 DD2 — the trial routes, the SAME scope.
|
|
386
|
-
workspaceTrialRoutes: {
|
|
387
|
-
specifier: WORKSPACES_TRIAL,
|
|
388
|
-
load: () => import(WORKSPACES_TRIAL),
|
|
389
|
-
},
|
|
390
|
-
workspacePlanRoutes: {
|
|
391
|
-
specifier: WORKSPACES_PLAN,
|
|
392
|
-
load: () => import(WORKSPACES_PLAN),
|
|
393
|
-
},
|
|
394
|
-
workspaceOutboundMessageRoutes: {
|
|
395
|
-
specifier: WORKSPACES_MESSAGES_OUT,
|
|
396
|
-
load: () => import(WORKSPACES_MESSAGES_OUT),
|
|
397
|
-
},
|
|
398
|
-
// #7877 D11 — the member reads, in authScope under the same predicate.
|
|
399
|
-
memberPieceRoutes: {
|
|
400
|
-
specifier: MEMBER_PIECES,
|
|
401
|
-
load: () => import(MEMBER_PIECES),
|
|
402
|
-
},
|
|
403
|
-
memberMessageRoutes: {
|
|
404
|
-
specifier: MEMBER_MESSAGES,
|
|
405
|
-
load: () => import(MEMBER_MESSAGES),
|
|
406
|
-
},
|
|
407
|
-
// #8169 D9 — the Phase 1 member reads, in authScope beside the two above.
|
|
408
|
-
memberPurchaseRoutes: {
|
|
409
|
-
specifier: MEMBER_PURCHASES,
|
|
410
|
-
load: () => import(MEMBER_PURCHASES),
|
|
411
|
-
},
|
|
412
|
-
memberAppointmentRoutes: {
|
|
413
|
-
specifier: MEMBER_APPOINTMENTS,
|
|
414
|
-
load: () => import(MEMBER_APPOINTMENTS),
|
|
415
|
-
},
|
|
416
|
-
memberArtifactRoutes: {
|
|
417
|
-
specifier: MEMBER_ARTIFACTS,
|
|
418
|
-
load: () => import(MEMBER_ARTIFACTS),
|
|
419
|
-
},
|
|
420
|
-
memberRequestFormRoutes: {
|
|
421
|
-
specifier: MEMBER_REQUEST_FORM,
|
|
422
|
-
load: () => import(MEMBER_REQUEST_FORM),
|
|
423
|
-
},
|
|
424
|
-
memberBillingRoutes: {
|
|
425
|
-
specifier: MEMBER_BILLING,
|
|
426
|
-
load: () => import(MEMBER_BILLING),
|
|
427
|
-
},
|
|
428
|
-
// The two that reach services/scheduling/ (scheduling-owned, D9).
|
|
429
|
-
memberMeetingTypeRoutes: {
|
|
430
|
-
specifier: MEMBER_MEETING_TYPES,
|
|
431
|
-
load: () => import(MEMBER_MEETING_TYPES),
|
|
432
|
-
},
|
|
433
|
-
memberAvailabilityRoutes: {
|
|
434
|
-
specifier: MEMBER_AVAILABILITY,
|
|
435
|
-
load: () => import(MEMBER_AVAILABILITY),
|
|
436
|
-
},
|
|
437
|
-
// #8170 D11 — the member writes, in authScope beside the reads.
|
|
438
|
-
memberRequestRoutes: {
|
|
439
|
-
specifier: MEMBER_REQUESTS,
|
|
440
|
-
load: () => import(MEMBER_REQUESTS),
|
|
441
|
-
},
|
|
442
|
-
// #8515 D13 — the onboarding read and write.
|
|
443
|
-
memberOnboardingRoutes: {
|
|
444
|
-
specifier: MEMBER_ONBOARDING,
|
|
445
|
-
load: () => import(MEMBER_ONBOARDING),
|
|
446
|
-
},
|
|
447
|
-
memberArtifactReviewRoutes: {
|
|
448
|
-
specifier: MEMBER_ARTIFACT_REVIEW,
|
|
449
|
-
load: () => import(MEMBER_ARTIFACT_REVIEW),
|
|
450
|
-
},
|
|
451
|
-
memberProfileRoutes: {
|
|
452
|
-
specifier: MEMBER_PROFILE,
|
|
453
|
-
load: () => import(MEMBER_PROFILE),
|
|
454
|
-
},
|
|
455
|
-
memberDeletionRequestRoutes: {
|
|
456
|
-
specifier: MEMBER_DELETION_REQUEST,
|
|
457
|
-
load: () => import(MEMBER_DELETION_REQUEST),
|
|
458
|
-
},
|
|
459
|
-
// #8438 D6 — the member's own consents, the SAME scope as the writes above.
|
|
460
|
-
memberConsentRoutes: {
|
|
461
|
-
specifier: MEMBER_CONSENTS,
|
|
462
|
-
load: () => import(MEMBER_CONSENTS),
|
|
463
|
-
},
|
|
464
|
-
// #8438 D8 — the owner's consents to Working Theory (multi-tenant only).
|
|
465
|
-
consentPlatformRoutes: {
|
|
466
|
-
specifier: CONSENT_PLATFORM,
|
|
467
|
-
load: () => import(CONSENT_PLATFORM),
|
|
468
|
-
},
|
|
469
|
-
// The one that reaches services/scheduling/ (scheduling-owned, D11).
|
|
470
|
-
memberBookingRoutes: {
|
|
471
|
-
specifier: MEMBER_BOOKINGS,
|
|
472
|
-
load: () => import(MEMBER_BOOKINGS),
|
|
473
|
-
},
|
|
474
|
-
// #3591 PR-B — the agent seat, the SAME scope.
|
|
475
|
-
workspaceAgentRoutes: {
|
|
476
|
-
specifier: WORKSPACES_AGENTS,
|
|
477
|
-
load: () => import(WORKSPACES_AGENTS),
|
|
478
|
-
},
|
|
479
|
-
// #6183 S9b — registered in the scheduling scope, not the
|
|
480
|
-
// workspace/multi-tenant scope its siblings above share.
|
|
481
|
-
workspaceMeetingsRoutes: {
|
|
482
|
-
specifier: WORKSPACES_MEETINGS,
|
|
483
|
-
load: () => import(WORKSPACES_MEETINGS),
|
|
484
|
-
},
|
|
485
|
-
workspaceContentRoutes: {
|
|
486
|
-
specifier: WORKSPACES_CONTENT,
|
|
487
|
-
load: () => import(WORKSPACES_CONTENT),
|
|
488
|
-
},
|
|
489
|
-
workspaceAssetUploadRoute: {
|
|
490
|
-
specifier: ASSETS_UPLOAD,
|
|
491
|
-
load: () => import(ASSETS_UPLOAD),
|
|
492
|
-
},
|
|
493
|
-
publicAssetReadRoute: {
|
|
494
|
-
specifier: ASSETS_READ,
|
|
495
|
-
load: () => import(ASSETS_READ),
|
|
496
|
-
},
|
|
497
|
-
workspaceAssetManageRoutes: {
|
|
498
|
-
specifier: ASSETS_MANAGE,
|
|
499
|
-
load: () => import(ASSETS_MANAGE),
|
|
500
|
-
},
|
|
501
|
-
billingRoutes: {
|
|
502
|
-
specifier: BILLING_INDEX,
|
|
503
|
-
load: () => import(BILLING_INDEX),
|
|
504
|
-
},
|
|
505
|
-
// #8926 D1 — the workspace Offerings routes, multi-tenant only.
|
|
506
|
-
workspaceOfferingsRoutes: {
|
|
507
|
-
specifier: WORKSPACES_OFFERINGS,
|
|
508
|
-
load: () => import(WORKSPACES_OFFERINGS),
|
|
509
|
-
},
|
|
510
|
-
waitlistExperimentRoutes: {
|
|
511
|
-
specifier: WAITLIST_EXPERIMENTS_INDEX,
|
|
512
|
-
load: () => import(WAITLIST_EXPERIMENTS_INDEX),
|
|
513
|
-
},
|
|
514
|
-
listAvailabilityRoute: {
|
|
515
|
-
specifier: SCHEDULE_INDEX,
|
|
516
|
-
load: () => import(SCHEDULE_INDEX),
|
|
517
|
-
},
|
|
518
|
-
getEventMetaRoute: {
|
|
519
|
-
specifier: SCHEDULE_INDEX,
|
|
520
|
-
load: () => import(SCHEDULE_INDEX),
|
|
521
|
-
},
|
|
522
|
-
createBookingRoute: {
|
|
523
|
-
specifier: SCHEDULE_INDEX,
|
|
524
|
-
load: () => import(SCHEDULE_INDEX),
|
|
525
|
-
},
|
|
526
|
-
// #6183 S9b D2/D12 — the enabled-meeting-slugs public read; same
|
|
527
|
-
// specifier as the three anonymous schedule routes above (§ D6's
|
|
528
|
-
// precedent, no new SLICE_PLUGINS specifier constant needed).
|
|
529
|
-
meetingTypesRoute: {
|
|
530
|
-
specifier: SCHEDULE_INDEX,
|
|
531
|
-
load: () => import(SCHEDULE_INDEX),
|
|
532
|
-
},
|
|
533
|
-
// #8843 D1 — the meetings read; same specifier as its schedule siblings.
|
|
534
|
-
listMeetingsRoute: {
|
|
535
|
-
specifier: SCHEDULE_INDEX,
|
|
536
|
-
load: () => import(SCHEDULE_INDEX),
|
|
537
|
-
},
|
|
538
|
-
// #6085 — the tenant-guarded appointments read; same specifier as the
|
|
539
|
-
// three anonymous schedule routes above (§ D6, no new SLICE_PLUGINS
|
|
540
|
-
// specifier constant needed).
|
|
541
|
-
listAppointmentsRoute: {
|
|
542
|
-
specifier: SCHEDULE_INDEX,
|
|
543
|
-
load: () => import(SCHEDULE_INDEX),
|
|
544
|
-
},
|
|
545
|
-
getFlowRoute: {
|
|
546
|
-
specifier: FLOWS_INDEX,
|
|
547
|
-
load: () => import(FLOWS_INDEX),
|
|
548
|
-
},
|
|
549
|
-
submitFlowStepRoute: {
|
|
550
|
-
specifier: FLOWS_INDEX,
|
|
551
|
-
load: () => import(FLOWS_INDEX),
|
|
552
|
-
},
|
|
553
|
-
getFlowAggregateRoute: {
|
|
554
|
-
specifier: FLOWS_INDEX,
|
|
555
|
-
load: () => import(FLOWS_INDEX),
|
|
556
|
-
},
|
|
557
|
-
// #7876 D1 — a spoke seeds its declared flows; off until
|
|
558
|
-
// AILK_SPOKE_FLOW_SEED (the route answers 403 flow_seed_disabled).
|
|
559
|
-
putFlowDefinitionRoute: {
|
|
560
|
-
specifier: FLOWS_INDEX,
|
|
561
|
-
load: () => import(FLOWS_INDEX),
|
|
562
|
-
},
|
|
563
|
-
// #6157 — the tenant-scoped magnet Admin authoring surface. ONE plugin,
|
|
564
|
-
// ONE specifier (D2): all four routes register through this one entry.
|
|
565
|
-
magnetRoutes: {
|
|
566
|
-
specifier: MAGNETS_INDEX,
|
|
567
|
-
load: () => import(MAGNETS_INDEX),
|
|
568
|
-
},
|
|
569
|
-
deliverableDownloadRoute: {
|
|
570
|
-
specifier: DELIVERABLES_INDEX,
|
|
571
|
-
load: () => import(DELIVERABLES_INDEX),
|
|
572
|
-
},
|
|
573
|
-
// #8848 D5 — an ungated file's bytes, in its own `deliverableopen` scope.
|
|
574
|
-
deliverableOpenRoute: {
|
|
575
|
-
specifier: DELIVERABLES_INDEX,
|
|
576
|
-
load: () => import(DELIVERABLES_INDEX),
|
|
577
|
-
},
|
|
578
|
-
projectListingStartRoute: {
|
|
579
|
-
specifier: PROJECT_LISTINGS_INDEX,
|
|
580
|
-
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
581
|
-
},
|
|
582
|
-
projectListingDraftPatchRoute: {
|
|
583
|
-
specifier: PROJECT_LISTINGS_INDEX,
|
|
584
|
-
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
585
|
-
},
|
|
586
|
-
projectListingSubmitRoute: {
|
|
587
|
-
specifier: PROJECT_LISTINGS_INDEX,
|
|
588
|
-
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
589
|
-
},
|
|
590
|
-
// #5407 — the resume link's verification write. Anonymous surface,
|
|
591
|
-
// module-conditional (signal-collectors, D-48) like its four siblings.
|
|
592
|
-
projectListingVerifyEmailRoute: {
|
|
593
|
-
specifier: PROJECT_LISTINGS_INDEX,
|
|
594
|
-
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
595
|
-
},
|
|
596
|
-
projectListingPublicRoute: {
|
|
597
|
-
specifier: PROJECT_LISTINGS_INDEX,
|
|
598
|
-
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
599
|
-
},
|
|
600
|
-
// #7521 — the landing-page directory read; its own rate-limited scope below.
|
|
601
|
-
projectListingDirectoryRoute: {
|
|
602
|
-
specifier: PROJECT_LISTINGS_INDEX,
|
|
603
|
-
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
604
|
-
},
|
|
605
|
-
projectListingMeRoute: {
|
|
606
|
-
specifier: PROJECT_LISTINGS_INDEX,
|
|
607
|
-
load: () => import(PROJECT_LISTINGS_INDEX),
|
|
608
|
-
},
|
|
609
|
-
slackActionsRoute: {
|
|
610
|
-
specifier: SLACK_INDEX,
|
|
611
|
-
load: () => import(SLACK_INDEX),
|
|
612
|
-
},
|
|
613
|
-
waitlistSignupRoutes: {
|
|
614
|
-
specifier: WAITLIST_SIGNUPS_INDEX,
|
|
615
|
-
load: () => import(WAITLIST_SIGNUPS_INDEX),
|
|
616
|
-
},
|
|
617
|
-
// Commerce (D-47, #5364), re-cut by D-64 (#5796). `checkoutRoutes` and
|
|
618
|
-
// `webhooksRoutes` are the base-tree PRIMITIVES now: they register
|
|
619
|
-
// UNCONDITIONALLY and gate at call time on the `commerce` license claim
|
|
620
|
-
// (lib/require-commerce-claim.ts), so they are no longer slice-gated even
|
|
621
|
-
// though they still load through this table. `flowCheckoutRoutes` and the
|
|
622
|
-
// two recommender entries below stay module-conditional (isModuleEnabled
|
|
623
|
-
// "commerce") — they are commerce OVERLAY, not primitives.
|
|
624
|
-
checkoutRoutes: {
|
|
625
|
-
specifier: CHECKOUT_INDEX,
|
|
626
|
-
load: () => import(CHECKOUT_INDEX),
|
|
91
|
+
const MODULE_OBJECTS: Record<
|
|
92
|
+
OptionalModule,
|
|
93
|
+
{ specifier: string; load: () => Promise<unknown> }
|
|
94
|
+
> = {
|
|
95
|
+
"multi-tenant": {
|
|
96
|
+
specifier: MULTI_TENANT_MODULE,
|
|
97
|
+
load: () => import(MULTI_TENANT_MODULE),
|
|
627
98
|
},
|
|
628
|
-
|
|
629
|
-
specifier:
|
|
630
|
-
load: () => import(
|
|
99
|
+
workspace: {
|
|
100
|
+
specifier: WORKSPACE_MODULE,
|
|
101
|
+
load: () => import(WORKSPACE_MODULE),
|
|
631
102
|
},
|
|
632
|
-
|
|
633
|
-
specifier:
|
|
634
|
-
load: () => import(
|
|
103
|
+
scheduling: {
|
|
104
|
+
specifier: SCHEDULING_MODULE,
|
|
105
|
+
load: () => import(SCHEDULING_MODULE),
|
|
635
106
|
},
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
subscriptionRoutes: {
|
|
640
|
-
specifier: SUBSCRIPTION_INDEX,
|
|
641
|
-
load: () => import(SUBSCRIPTION_INDEX),
|
|
107
|
+
"signal-collectors": {
|
|
108
|
+
specifier: SIGNAL_COLLECTORS_MODULE,
|
|
109
|
+
load: () => import(SIGNAL_COLLECTORS_MODULE),
|
|
642
110
|
},
|
|
643
|
-
|
|
644
|
-
specifier:
|
|
645
|
-
load: () => import(
|
|
111
|
+
commerce: {
|
|
112
|
+
specifier: COMMERCE_MODULE,
|
|
113
|
+
load: () => import(COMMERCE_MODULE),
|
|
646
114
|
},
|
|
647
|
-
|
|
648
|
-
specifier:
|
|
649
|
-
load: () => import(
|
|
115
|
+
"content-write": {
|
|
116
|
+
specifier: CONTENT_WRITE_MODULE,
|
|
117
|
+
load: () => import(CONTENT_WRITE_MODULE),
|
|
650
118
|
},
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
type SlicePluginName = keyof typeof SLICE_PLUGINS;
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Load one slice route plugin. Every entry above is a `FastifyPluginAsync`
|
|
657
|
-
* export, which is the cast's whole content — `import(nonLiteral)` yields
|
|
658
|
-
* `any`, so this boundary is the load-bearing line and names the exact shape
|
|
659
|
-
* rather than spreading `any` into `register()`.
|
|
660
|
-
*
|
|
661
|
-
* Throws rather than skipping if the export is missing: the branch only runs
|
|
662
|
-
* when the module is declared enabled, so an absent export is a build/packaging
|
|
663
|
-
* fault to surface at boot, not a condition to degrade through.
|
|
664
|
-
*/
|
|
665
|
-
async function sliceRoutePlugin(
|
|
666
|
-
name: SlicePluginName,
|
|
667
|
-
): Promise<FastifyPluginAsync> {
|
|
668
|
-
const { specifier, load } = SLICE_PLUGINS[name];
|
|
669
|
-
const mod = (await loadSlice(specifier, load)) as Record<
|
|
670
|
-
string,
|
|
671
|
-
FastifyPluginAsync | undefined
|
|
672
|
-
>;
|
|
673
|
-
const plugin = mod[name];
|
|
674
|
-
if (!plugin) {
|
|
675
|
-
throw new Error(`slice export ${name} missing from ${specifier}`);
|
|
676
|
-
}
|
|
677
|
-
return plugin;
|
|
678
|
-
}
|
|
119
|
+
};
|
|
679
120
|
|
|
680
121
|
/**
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
* (apps/api/src/middleware/tenant.ts:83).
|
|
684
|
-
*/
|
|
685
|
-
async function tenantPreHandlerSlicePlugin(): Promise<FastifyPluginCallback> {
|
|
686
|
-
const specifier: string = "./middleware/tenant.js";
|
|
687
|
-
const mod = (await loadSlice(specifier, () => import(specifier))) as {
|
|
688
|
-
tenantPreHandlerPlugin?: FastifyPluginCallback;
|
|
689
|
-
};
|
|
690
|
-
const plugin = mod.tenantPreHandlerPlugin;
|
|
691
|
-
if (!plugin) {
|
|
692
|
-
throw new Error(
|
|
693
|
-
"slice export tenantPreHandlerPlugin missing from " + specifier,
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
return plugin;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* The tenant guard THIS build runs (#5677). Exactly one of the two, never
|
|
701
|
-
* both: the multi-tenant module's `resolveActiveTenant` guard when the module
|
|
702
|
-
* is enabled, and the base-build default-workspace guard
|
|
703
|
-
* (`middleware/default-tenant.ts`) otherwise.
|
|
704
|
-
*
|
|
705
|
-
* Precedence, not parallelism. On a build WITH the module this returns the
|
|
706
|
-
* identical plugin every tenant scope registered before, so multi-tenant
|
|
707
|
-
* resolution and behavior are unchanged. On a build without it, the base guard
|
|
708
|
-
* resolves the seeded default Workspace (`database/tenancy.ts`) under the same
|
|
709
|
-
* default-deny posture — which is what makes the already-built review
|
|
710
|
-
* dashboard reachable at Pro instead of leaving its routes unregistered.
|
|
711
|
-
*
|
|
712
|
-
* The base guard is a STATIC import, not a slice load: it always ships.
|
|
713
|
-
*/
|
|
714
|
-
async function tenantPreHandlerForBuild(): Promise<FastifyPluginCallback> {
|
|
715
|
-
return isModuleEnabled("multi-tenant")
|
|
716
|
-
? await tenantPreHandlerSlicePlugin()
|
|
717
|
-
: defaultTenantPreHandlerPlugin;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* The commerce boot-time key admission — the one other slice entry that is
|
|
722
|
-
* not a route plugin, mirroring `tenantPreHandlerSlicePlugin()` above (D3).
|
|
723
|
-
* `lib/stripe.ts` is commerce-owned (D5) and its `admitApiStripeKey` is the
|
|
724
|
-
* sole `apps/api` reach into the Stripe SDK wrapper package, so it can no
|
|
725
|
-
* longer be a static import (spec D1) once the module is stripped. The
|
|
726
|
-
* widened `const specifier: string` annotation is what defeats `tsc`
|
|
727
|
-
* resolution, exactly as it does above.
|
|
728
|
-
*
|
|
729
|
-
* #8450 D4: called with NO argument, so the admission covers the same
|
|
730
|
-
* `process.env` key `getStripeClient()` later reads. `STRIPE_MODE` unset is
|
|
731
|
-
* today's sandbox-only check; only a restricted live key beside
|
|
732
|
-
* `STRIPE_MODE=live` on a production build comes back `"live"`.
|
|
733
|
-
*/
|
|
734
|
-
async function admitCommerceStripeKey(): Promise<"test" | "live"> {
|
|
735
|
-
const specifier: string = "./lib/stripe.js";
|
|
736
|
-
const mod = (await loadSlice(specifier, () => import(specifier))) as {
|
|
737
|
-
admitApiStripeKey?: () => "test" | "live";
|
|
738
|
-
};
|
|
739
|
-
const admitApiStripeKeyFn = mod.admitApiStripeKey;
|
|
740
|
-
if (!admitApiStripeKeyFn) {
|
|
741
|
-
throw new Error("slice export admitApiStripeKey missing from " + specifier);
|
|
742
|
-
}
|
|
743
|
-
return admitApiStripeKeyFn();
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* #6889 (SEC-02 of the #6876 security review) — make the checkout route's
|
|
748
|
-
* posture observable. POST /v1/checkout/sessions admits a caller without
|
|
749
|
-
* X-Internal-Auth when OSS_API_INTERNAL_SECRET is unset. With FRONTEND_URL
|
|
750
|
-
* unset in production, the trusted-origin list (#7395) holds only
|
|
751
|
-
* AILK_TRUSTED_ORIGINS — with that unset too, CORS and cookie writes refuse
|
|
752
|
-
* every browser origin and the redirect lock (#6877) answers 500
|
|
753
|
-
* config-error to that caller. Both stay optional (OSS standalone compat), so
|
|
754
|
-
* this only ever WARNS — once per unset variable, and only in production.
|
|
755
|
-
* FRONTEND_URL is read from the raw environment because `config.FRONTEND_URL`
|
|
756
|
-
* carries a localhost default.
|
|
757
|
-
*/
|
|
758
|
-
function warnOnUnsetProductionProtections(): void {
|
|
759
|
-
// #8438 D9 — AILK_CONSENT_LINK_SECRET is optional, but a value shorter than
|
|
760
|
-
// 32 bytes is a misconfiguration worth an error at boot: it is treated as
|
|
761
|
-
// unset, so no unsubscribe link can be minted or honored. Never the value.
|
|
762
|
-
if (
|
|
763
|
-
process.env["AILK_CONSENT_LINK_SECRET"] !== undefined &&
|
|
764
|
-
process.env["AILK_CONSENT_LINK_SECRET"] !== "" &&
|
|
765
|
-
!consentLinkSecretConfigured()
|
|
766
|
-
) {
|
|
767
|
-
logger.error(
|
|
768
|
-
"[startup] AILK_CONSENT_LINK_SECRET is set but shorter than 32 bytes — treated as unset; no unsubscribe link can be minted or honored",
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
if (config.NODE_ENV !== "production") return;
|
|
772
|
-
if (!process.env["FRONTEND_URL"]) {
|
|
773
|
-
logger.warn(
|
|
774
|
-
"[startup] FRONTEND_URL not set in production — CORS, cookie-authenticated writes and checkout redirects trust only AILK_TRUSTED_ORIGINS; with neither set, all three refuse every browser origin and checkout answers 500 config-error to any caller admitted without X-Internal-Auth",
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
if (!process.env["OSS_API_INTERNAL_SECRET"]) {
|
|
778
|
-
logger.warn(
|
|
779
|
-
"[startup] OSS_API_INTERNAL_SECRET not set in production — the checkout route admits callers without X-Internal-Auth",
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/**
|
|
785
|
-
* Build Fastify server instance (for serverless or standalone)
|
|
122
|
+
* Build the Fastify server (for serverless or standalone): the six module
|
|
123
|
+
* objects, handed to `@working-theory/api`, which selects and claims.
|
|
786
124
|
* @returns Configured Fastify instance ready for requests
|
|
787
125
|
*/
|
|
788
|
-
export async function build() {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
// the same catch-log-exit(1) path index.ts already runs on this throws
|
|
798
|
-
// loud rather than serving empty lists. Carve-out: with no DATABASE_URL
|
|
799
|
-
// configured, it logs a warning and returns instead of throwing (a
|
|
800
|
-
// database-less dev boot).
|
|
801
|
-
await assertSiteAliasResolves();
|
|
802
|
-
|
|
803
|
-
// #6677 DD-6 — the renewed licence key's durable home: with a database,
|
|
804
|
-
// one deployment-wide SiteSetting row, so a redeploy keeps the key this
|
|
805
|
-
// site renewed. Never throws; without DATABASE_URL the file store stays.
|
|
806
|
-
await registerLicenseKeyStore();
|
|
807
|
-
|
|
808
|
-
// Runtime key admission (#8450 D4): refuse, before any webhook or checkout
|
|
809
|
-
// call can exercise it, every live Stripe key this process cannot vouch
|
|
810
|
-
// for — all of them unless STRIPE_MODE=live sits beside a restricted live
|
|
811
|
-
// key on a production build. UNCONDITIONAL since D-64 (#5796): the
|
|
812
|
-
// commerce primitives and lib/stripe.ts are base tree now, so the key's
|
|
813
|
-
// presence — not a module flag — is the whole question. A build with no
|
|
814
|
-
// STRIPE_SECRET_KEY has nothing to admit.
|
|
815
|
-
let stripeMode: "test" | "live" = "test";
|
|
816
|
-
if (config.STRIPE_SECRET_KEY) {
|
|
817
|
-
stripeMode = await admitCommerceStripeKey();
|
|
818
|
-
logger.info({ stripeMode }, "[startup] stripe key admitted");
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// trustProxy: true (#4730) — the Vercel serverless handler
|
|
822
|
-
// (apps/api/api/index.ts) bridges every request through Fastify's
|
|
823
|
-
// `.inject()` rather than a real socket, so `req.socket.remoteAddress`
|
|
824
|
-
// (the trustProxy:false default) is a meaningless simulated value, not
|
|
825
|
-
// the caller's address. With trustProxy enabled, req.ip instead resolves
|
|
826
|
-
// from `X-Forwarded-For` (the header Vercel's own edge sets on every
|
|
827
|
-
// inbound request) via Fastify's built-in proxy-addr parsing — reading
|
|
828
|
-
// the LEFTMOST address in a multi-hop chain, i.e. the address farthest
|
|
829
|
-
// from this process. This is what lets the per-scope rate limiter
|
|
830
|
-
// (middleware/rate-limit.ts) key on a real visitor's IP instead of a
|
|
831
|
-
// constant fallback or a shared intermediary's egress IP (see the flows/
|
|
832
|
-
// leads/aeo/schedule scopes below).
|
|
833
|
-
//
|
|
834
|
-
// Trade-off: X-Forwarded-For is caller-suppliable to anyone hitting this
|
|
835
|
-
// API directly (there is no fixed, allowlistable upstream proxy — this
|
|
836
|
-
// API's actual "proxies" are arbitrary consumer web functions), so a
|
|
837
|
-
// direct caller can still spoof a fresh identity per request. Per-IP
|
|
838
|
-
// rate limiting on these anonymous surfaces is a best-effort abuse
|
|
839
|
-
// deterrent, not a hard security boundary; this trades a small amount of
|
|
840
|
-
// spoof-resistance for correctness on the overwhelmingly common path
|
|
841
|
-
// (server-action-forwarded traffic), which was previously broken for
|
|
842
|
-
// every caller.
|
|
843
|
-
const server = Fastify({
|
|
844
|
-
logger: {
|
|
845
|
-
// The request serializer drops the QUERY STRING (#5970 security review,
|
|
846
|
-
// finding 1). Fastify's default `req` serializer logs `req.raw.url`
|
|
847
|
-
// verbatim, so any route whose credential rides in the query string
|
|
848
|
-
// writes that credential into the API's own log on every single request
|
|
849
|
-
// — including the automatic "incoming request" line, which is emitted by
|
|
850
|
-
// Fastify's internal hook before any route-level hook can intervene.
|
|
851
|
-
//
|
|
852
|
-
// `GET /v1/requests/:id/confirm?token=…` is exactly such a route: the
|
|
853
|
-
// token IS the authorization, it is live for seven days, and holding it
|
|
854
|
-
// is enough to read a requester's name and phone and to mint a Payment
|
|
855
|
-
// Link on the site's own Stripe account. `Referrer-Policy: no-referrer`
|
|
856
|
-
// on that route exists to keep the token out of a THIRD PARTY's logs;
|
|
857
|
-
// this keeps it out of ours, which is the larger hole of the two.
|
|
858
|
-
//
|
|
859
|
-
// The shape is otherwise Fastify's own default, minus the query: a log
|
|
860
|
-
// consumer reading `method` + `url` + `remoteAddress` sees what it
|
|
861
|
-
// always saw. Nothing that identifies a REQUEST is lost — `:id` is in
|
|
862
|
-
// the path and survives; only the secret after `?` is dropped.
|
|
863
|
-
serializers: { req: requestLogSerializer },
|
|
864
|
-
},
|
|
865
|
-
trustProxy: true,
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
// The hub's forwarding-key digests (#7394 DD-2), parsed once from
|
|
869
|
-
// AILK_VISITOR_FORWARD_KEYS and shared by the global ceiling and the leads
|
|
870
|
-
// scope below, so both limiters apply one identity rule. A keyed spoke's
|
|
871
|
-
// lead write is counted per visitor (`x-ailk-visitor-ip`); everything else
|
|
872
|
-
// keys on `req.ip`. Unset → `[]`, which believes no forwarded visitor. The
|
|
873
|
-
// parse warns by entry position only, never an entry's value.
|
|
874
|
-
const visitorForwardKeyDigests = buildTrustedKeyDigests(
|
|
875
|
-
process.env.AILK_VISITOR_FORWARD_KEYS,
|
|
876
|
-
(msg) => server.log.warn(msg),
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
// #8805 — the sign-up evidence read's site key, verified once per request
|
|
880
|
-
// and shared by the global ceiling and the `signupevidence` scope below. A
|
|
881
|
-
// registered, unrevoked spoke key (`resolveCallerSite`, the registry lookup
|
|
882
|
-
// every spoke-aware route uses) is counted per key; a malformed, unknown or
|
|
883
|
-
// revoked one, or none, keeps `req.ip`. It decides the bucket only — the
|
|
884
|
-
// route still serves `resolveSiteWorkspaceId()`.
|
|
885
|
-
const signUpEvidenceSiteKey = siteKeyReader(async (req) => {
|
|
886
|
-
const caller = await resolveCallerSite(req);
|
|
887
|
-
return caller.kind === "spoke" ? caller.spokeId : null;
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
// Collect the route table this server registers (#5314).
|
|
891
|
-
//
|
|
892
|
-
// Installed here — after `Fastify()` and before the first `register()` — so
|
|
893
|
-
// no route can precede it: a root-instance `onRoute` hook fires for every
|
|
894
|
-
// route added afterwards in ANY encapsulated child scope, which is what lets
|
|
895
|
-
// one hook see the auth, tenant, webhooks, leads, flows, aeo and schedule
|
|
896
|
-
// scopes without touching any of them.
|
|
897
|
-
//
|
|
898
|
-
// HEAD is dropped: Fastify synthesises it from every GET, so publishing it
|
|
899
|
-
// would describe routes no handler declared.
|
|
900
|
-
//
|
|
901
|
-
// The table lives on THIS instance, not in module state, so two servers
|
|
902
|
-
// built concurrently in one process cannot corrupt each other's — see the
|
|
903
|
-
// note on `readRegisteredRoutes` in openapi/spec.ts for the concrete race.
|
|
904
|
-
// The decorator holds the array by reference and the hook pushes into it, so
|
|
905
|
-
// every reader sees the current contents without re-publishing.
|
|
906
|
-
const registeredRoutes: RegisteredRoute[] = [];
|
|
907
|
-
server.decorate(REGISTERED_ROUTES_DECORATOR, registeredRoutes);
|
|
908
|
-
server.addHook("onRoute", (route) => {
|
|
909
|
-
const methods = Array.isArray(route.method) ? route.method : [route.method];
|
|
910
|
-
for (const method of methods) {
|
|
911
|
-
if (method === "HEAD") continue;
|
|
912
|
-
registeredRoutes.push({ method, url: route.url });
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
// Register error serializer — maps internal exceptions to generic strings
|
|
917
|
-
// so connection strings, hostnames, stack traces, and SDK error details
|
|
918
|
-
// never reach the client. Full detail still goes to pino at error level.
|
|
919
|
-
registerErrorSerializer(server);
|
|
920
|
-
server.setNotFoundHandler(notFoundHandler);
|
|
921
|
-
|
|
922
|
-
// Register CORS — public read API + the trusted browser origins (#7395).
|
|
923
|
-
// `origin` is ALWAYS a copy of `config.TRUSTED_ORIGINS`, the frozen list
|
|
924
|
-
// config/index.ts resolves once at startup from FRONTEND_URL and
|
|
925
|
-
// AILK_TRUSTED_ORIGINS (no `*`, no wildcard host). @fastify/cors echoes a
|
|
926
|
-
// request's Origin only on an exact match against that array, and writes no
|
|
927
|
-
// allow-origin header otherwise; an empty list means no cross-origin caller.
|
|
928
|
-
// Never pass `true`, a function, a RegExp or a delegator here, and never set
|
|
929
|
-
// a per-route `cors` option: with `credentials: true`, reflecting any origin
|
|
930
|
-
// would let any website read credentialed responses (security review M1).
|
|
931
|
-
// Same-origin and non-browser callers are unaffected.
|
|
932
|
-
//
|
|
933
|
-
// `methods` is EXPLICIT because @fastify/cors defaults it to `GET,HEAD,POST`
|
|
934
|
-
// (its own `defaultOptions`), which is narrower than the surface this server
|
|
935
|
-
// registers (#5397). With only `origin` + `credentials` passed, the preflight
|
|
936
|
-
// for `PATCH /v1/project-listings/drafts/:id` answered
|
|
937
|
-
// `access-control-allow-methods: GET, HEAD, POST`, so a browser refused every
|
|
938
|
-
// draft save on a split-origin deploy — a site could create a listing draft
|
|
939
|
-
// and never save one. The list below is the union of the methods the route
|
|
940
|
-
// table actually serves (GET/POST/PATCH/PUT/DELETE — `/v1/auth/tokens/:id`,
|
|
941
|
-
// `/v1/content/:type/:slug`, `/v1/user/data/:key`, the workspace-config PUTs),
|
|
942
|
-
// plus HEAD (Fastify synthesises it from every GET) and OPTIONS (the plugin's
|
|
943
|
-
// own wildcard preflight route). `cors.test.ts` re-derives that union from the
|
|
944
|
-
// live route table on every run, so a route added in a NEW method fails there
|
|
945
|
-
// rather than silently becoming unreachable from a browser.
|
|
946
|
-
//
|
|
947
|
-
// This widens WHICH METHODS the allowed origins may use, never WHO is allowed:
|
|
948
|
-
// `origin` and `credentials` are untouched.
|
|
949
|
-
//
|
|
950
|
-
// `allowedHeaders` is deliberately left unset: null means @fastify/cors
|
|
951
|
-
// reflects the request's `Access-Control-Request-Headers`, so `authorization`
|
|
952
|
-
// and `content-type` pass without this file having to maintain a list that
|
|
953
|
-
// silently drops a header a future route needs.
|
|
954
|
-
await server.register(cors, {
|
|
955
|
-
origin: [...config.TRUSTED_ORIGINS],
|
|
956
|
-
credentials: true,
|
|
957
|
-
methods: ["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
// Register rate limiting — applied globally with the unauth ceiling. The
|
|
961
|
-
// limiter is in-memory by default, which suits a single Vercel function /
|
|
962
|
-
// single-process dev server; multi-instance deployments will need a
|
|
963
|
-
// Redis store, swappable here without changing route code.
|
|
964
|
-
//
|
|
965
|
-
// `keyGenerator` (#7394 DD-7): a keyed spoke's lead write counts against its
|
|
966
|
-
// visitor's own ceiling here too, or a spoke whose visitors share one
|
|
967
|
-
// egress IP would still be capped at this limit in total. For every other
|
|
968
|
-
// request it returns `req.ip`, the plugin's own default key.
|
|
969
|
-
//
|
|
970
|
-
// #8805: a sign-up evidence read with a verified site key is keyed
|
|
971
|
-
// `site,<spokeId>` and let through here — its scope's per-key ceiling is
|
|
972
|
-
// its one limit, so an anonymous caller spoofing the web's egress address
|
|
973
|
-
// cannot spend it, and many web instances behind that address share no
|
|
974
|
-
// IP bucket.
|
|
975
|
-
await server.register(rateLimit, {
|
|
976
|
-
max: CONTENT_RATE_LIMIT_UNAUTH,
|
|
977
|
-
timeWindow: CONTENT_RATE_LIMIT_WINDOW,
|
|
978
|
-
keyGenerator: async (req) => {
|
|
979
|
-
const siteId = await signUpEvidenceSiteKey(req);
|
|
980
|
-
return siteId === null
|
|
981
|
-
? resolveRateLimitIdentity(req, visitorForwardKeyDigests).identity
|
|
982
|
-
: `${SITE_KEY_IDENTITY_PREFIX}${siteId}`;
|
|
983
|
-
},
|
|
984
|
-
allowList: (_req, key) =>
|
|
985
|
-
typeof key === "string" && key.startsWith(SITE_KEY_IDENTITY_PREFIX),
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
// Health check
|
|
989
|
-
server.get("/health", async () => ({
|
|
990
|
-
status: "healthy",
|
|
991
|
-
timestamp: new Date().toISOString(),
|
|
992
|
-
}));
|
|
993
|
-
|
|
994
|
-
// Root endpoint — introduces this deployment as the SITE it serves, not as
|
|
995
|
-
// the kit it was built with (#5307). `_site.mdx` is read once per process;
|
|
996
|
-
// a site without one degrades to a neutral name, never to the kit's.
|
|
997
|
-
//
|
|
998
|
-
// `modules` (#7501) is the optional modules whose routes this build
|
|
999
|
-
// registers, computed once here — before any route registers — so the index
|
|
1000
|
-
// reports what registered rather than re-reading the env per request. The
|
|
1001
|
-
// Admin reads it to hide the surfaces whose module is off.
|
|
1002
|
-
const modules = servedModules();
|
|
1003
|
-
server.get("/", async () => {
|
|
1004
|
-
const identity = resolveApiIdentity();
|
|
1005
|
-
return {
|
|
1006
|
-
name: apiDisplayName(identity),
|
|
1007
|
-
version: API_VERSION,
|
|
1008
|
-
description: apiDescription(identity),
|
|
1009
|
-
modules,
|
|
1010
|
-
// Derived from the routes this process actually registered (#5314),
|
|
1011
|
-
// not a hand-maintained literal. The literal listed 11 endpoints against
|
|
1012
|
-
// 68 registered routes, omitted `/v1/project-listings/public` and
|
|
1013
|
-
// `/v1/billing/usage`, advertised `DELETE /v1/auth/tokens` at a path
|
|
1014
|
-
// that 404s (the route is `/v1/auth/tokens/:id`), and named two billing
|
|
1015
|
-
// routes on builds where the multi-tenant module registers neither.
|
|
1016
|
-
// Same mechanism now covers commerce (D-47, #5364): checkout, the
|
|
1017
|
-
// Stripe webhook, the flow-checkouts read and the two recommender
|
|
1018
|
-
// routes are derived exactly like the billing routes above and
|
|
1019
|
-
// disappear from this list on a build where `commerce` is off, rather
|
|
1020
|
-
// than advertising a route that 404s.
|
|
1021
|
-
//
|
|
1022
|
-
// Read at REQUEST time, so `ready()` has fired and the set is complete
|
|
1023
|
-
// even though this handler is registered before most routes.
|
|
1024
|
-
endpoints: deriveRootEndpoints(registeredRoutes),
|
|
1025
|
-
};
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
// Register authenticated routes — the dual-mode preHandler (#587) runs in
|
|
1029
|
-
// this sub-scope so unauthenticated callers never reach token / user data
|
|
1030
|
-
// handlers. Cookie-first, Bearer-fallback; see middleware/auth.ts.
|
|
1031
|
-
await server.register(async (authScope) => {
|
|
1032
|
-
await authScope.register(authPreHandlerPlugin, {
|
|
1033
|
-
trustedOrigins: config.TRUSTED_ORIGINS,
|
|
1034
|
-
});
|
|
1035
|
-
await authScope.register(tokenRoutes);
|
|
1036
|
-
await authScope.register(userRoutes);
|
|
1037
|
-
|
|
1038
|
-
// The hub signs in its spokes (#7894 D5) — `codes` and `me`. Auth only,
|
|
1039
|
-
// NOT tenant-guarded: a person signing in to a spoke has no tenant here.
|
|
1040
|
-
// Unconditional, like the leads inbox read below: a spoke's sign-in is not
|
|
1041
|
-
// a module. `codes` answers a cookie session only; `me` the hub credential
|
|
1042
|
-
// only (both enforced in the route). The credential's own admission is the
|
|
1043
|
-
// preHandler's hub-credential branch (D6).
|
|
1044
|
-
await authScope.register(hubSignInSessionRoutes);
|
|
1045
|
-
|
|
1046
|
-
// #8765 D5 — the caller's account-level sign-up agreements. Auth only,
|
|
1047
|
-
// NOT tenant-guarded: the agreement is about the `User` row, read and
|
|
1048
|
-
// written in the deployment's own site workspace whatever site asks.
|
|
1049
|
-
// Unconditional — auth and consent are never-gate surfaces — and both
|
|
1050
|
-
// are in `HUB_SESSION_ROUTES`, so a spoke reaches them with its member's
|
|
1051
|
-
// credential (the account layout's interstitial, D7).
|
|
1052
|
-
await authScope.register(consentAccountRoutes);
|
|
1053
|
-
|
|
1054
|
-
// Tenant-context routes (multi-tenant module) — auth only. They
|
|
1055
|
-
// establish/inspect the active workspace, so the tenant guard does not
|
|
1056
|
-
// apply to them; each enforces membership in the service layer.
|
|
1057
|
-
// Module-conditional (S1, #5088): without the multi-tenant module there
|
|
1058
|
-
// is no workspace concept to switch or inspect.
|
|
1059
|
-
if (isModuleEnabled("multi-tenant")) {
|
|
1060
|
-
await authScope.register(
|
|
1061
|
-
await sliceRoutePlugin("workspaceContextRoutes"),
|
|
1062
|
-
);
|
|
1063
|
-
// #8438 D8 — the Admin owner's consents to Working Theory. Auth only,
|
|
1064
|
-
// NOT tenant-guarded (the owner role is checked inside the route), and
|
|
1065
|
-
// deliberately NOT in `HUB_SESSION_ROUTES`: a cookie session only.
|
|
1066
|
-
await authScope.register(await sliceRoutePlugin("consentPlatformRoutes"));
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// The leads INBOX read (#5678) — auth only, NOT tenant-guarded, for the
|
|
1070
|
-
// same reason as the three reads below: the tenant scope is registered
|
|
1071
|
-
// only when the multi-tenant module is present, and lead capture is a
|
|
1072
|
-
// never-gate surface at EVERY level, so reading a lead back cannot be
|
|
1073
|
-
// gated on a module either. Registered unconditionally — it is the one
|
|
1074
|
-
// route in this scope with no `isModuleEnabled()` gate. Identity is
|
|
1075
|
-
// `request.userId`; the scope is resolved from the caller's own
|
|
1076
|
-
// memberships by `services/leads-inbox.ts` through
|
|
1077
|
-
// `services/owner-workspaces.ts`'s `resolveOwnerWorkspaces`, never from a
|
|
1078
|
-
// request parameter.
|
|
1079
|
-
await authScope.register(leadListRoute);
|
|
1080
|
-
|
|
1081
|
-
// Respondent signup read (#4731) — auth only, NOT tenant-guarded: a
|
|
1082
|
-
// respondent has no workspace membership and the tenant guard is
|
|
1083
|
-
// default-deny. Identity is request.userId; the service re-checks
|
|
1084
|
-
// User.emailVerified before any Lead read. Module-conditional
|
|
1085
|
-
// (signal-collectors, D-48): the waitlist instrument's respondent half.
|
|
1086
|
-
if (isModuleEnabled("signal-collectors")) {
|
|
1087
|
-
await authScope.register(await sliceRoutePlugin("waitlistSignupRoutes"));
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Respondent checkouts read (#4984) — the same posture as the signup
|
|
1091
|
-
// read above: auth only, NOT tenant-guarded. Identity is request.userId;
|
|
1092
|
-
// the only join is the lead-promotion link (never an email match).
|
|
1093
|
-
// Module-conditional (commerce, D-47): the `/account` Purchases panel
|
|
1094
|
-
// already degrades to its empty state with no code change when this is
|
|
1095
|
-
// absent (`SKIPPED_STATUSES` covers 404 — spec §4.2).
|
|
1096
|
-
if (isModuleEnabled("commerce")) {
|
|
1097
|
-
await authScope.register(await sliceRoutePlugin("flowCheckoutRoutes"));
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// The update-your-card banner's read + portal handoff (#6038) — the
|
|
1101
|
-
// same posture as flow-checkouts immediately above: auth only, NOT
|
|
1102
|
-
// tenant-guarded (a subscriber has no workspace membership). Identity is
|
|
1103
|
-
// request.userId; GET is cookie-or-bearer, POST refuses bearer callers
|
|
1104
|
-
// (D3.1). Module-conditional (commerce, D8): the account page's
|
|
1105
|
-
// subscription-status read already degrades to "no banner" with no code
|
|
1106
|
-
// change when this is absent (SKIPPED_STATUSES covers 404 — spec §4.2).
|
|
1107
|
-
if (isModuleEnabled("commerce")) {
|
|
1108
|
-
await authScope.register(await sliceRoutePlugin("subscriptionRoutes"));
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// The founder's OWN listings read (#5295) — the same posture again: auth
|
|
1112
|
-
// only, NOT tenant-guarded, because a founder is not a workspace member
|
|
1113
|
-
// and the tenant guard is default-deny. Identity is request.userId; the
|
|
1114
|
-
// only join is the owner link (ProjectListing.ownerUserId, stamped by
|
|
1115
|
-
// services/listing-promotion.ts), never an email match. The tenant-scoped
|
|
1116
|
-
// listing routes below keep their owner|admin floor untouched — this route
|
|
1117
|
-
// is additive, not a relaxation of that floor. Registered here rather than
|
|
1118
|
-
// inside the multi-tenant scope so a founder can read their own rows on a
|
|
1119
|
-
// build without the module; the static `/me` path takes router precedence
|
|
1120
|
-
// over the tenant scope's parametric `/v1/project-listings/:id`, exactly as
|
|
1121
|
-
// the already-shipped static `/v1/project-listings/public` does.
|
|
1122
|
-
// Module-conditional (signal-collectors, D-48 / D4): the listing
|
|
1123
|
-
// instrument's founder-read half; not severable from the four anonymous
|
|
1124
|
-
// listing routes below (same static barrel, D4).
|
|
1125
|
-
if (isModuleEnabled("signal-collectors")) {
|
|
1126
|
-
await authScope.register(await sliceRoutePlugin("projectListingMeRoute"));
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// The member reads (#7877 D11) — auth only, NOT tenant-guarded: a client
|
|
1130
|
-
// holds no membership (and an agent, which reaches only tenant-guarded
|
|
1131
|
-
// routes, is refused 403 by the preHandler). Each handler resolves the
|
|
1132
|
-
// caller through `memberCaller` and the workspace from the calling
|
|
1133
|
-
// spoke's key, never a request value. Registered under the SAME module
|
|
1134
|
-
// predicate as every routes/workspaces/* route, at the exact URLs the
|
|
1135
|
-
// preHandler's `HUB_SESSION_ROUTES` admits a hub credential on.
|
|
1136
|
-
if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
|
|
1137
|
-
await authScope.register(await sliceRoutePlugin("memberPieceRoutes"));
|
|
1138
|
-
await authScope.register(await sliceRoutePlugin("memberMessageRoutes"));
|
|
1139
|
-
// #8169 D9 — the Phase 1 member reads, under the same predicate.
|
|
1140
|
-
await authScope.register(await sliceRoutePlugin("memberPurchaseRoutes"));
|
|
1141
|
-
await authScope.register(await sliceRoutePlugin("memberAppointmentRoutes"));
|
|
1142
|
-
await authScope.register(await sliceRoutePlugin("memberArtifactRoutes"));
|
|
1143
|
-
await authScope.register(await sliceRoutePlugin("memberRequestFormRoutes"));
|
|
1144
|
-
await authScope.register(await sliceRoutePlugin("memberBillingRoutes"));
|
|
1145
|
-
// #8170 D11 — the member writes, under the same predicate.
|
|
1146
|
-
await authScope.register(await sliceRoutePlugin("memberRequestRoutes"));
|
|
1147
|
-
// #8515 D13 — the onboarding pair, under the same predicate.
|
|
1148
|
-
await authScope.register(await sliceRoutePlugin("memberOnboardingRoutes"));
|
|
1149
|
-
await authScope.register(await sliceRoutePlugin("memberArtifactReviewRoutes"));
|
|
1150
|
-
await authScope.register(await sliceRoutePlugin("memberProfileRoutes"));
|
|
1151
|
-
await authScope.register(await sliceRoutePlugin("memberDeletionRequestRoutes"));
|
|
1152
|
-
// #8438 D6 — the member's own consents, under the same predicate.
|
|
1153
|
-
await authScope.register(await sliceRoutePlugin("memberConsentRoutes"));
|
|
1154
|
-
}
|
|
1155
|
-
// #8169 D9 — the two member reads that reach services/scheduling/, which
|
|
1156
|
-
// only the scheduling module delivers: a scaffold with a tenancy module
|
|
1157
|
-
// and no scheduling carries neither file, and one with scheduling and no
|
|
1158
|
-
// tenancy module registers neither.
|
|
1159
|
-
if (
|
|
1160
|
-
isModuleEnabled("scheduling") &&
|
|
1161
|
-
(isModuleEnabled("workspace") || isModuleEnabled("multi-tenant"))
|
|
1162
|
-
) {
|
|
1163
|
-
await authScope.register(await sliceRoutePlugin("memberMeetingTypeRoutes"));
|
|
1164
|
-
await authScope.register(await sliceRoutePlugin("memberAvailabilityRoutes"));
|
|
1165
|
-
// #8170 D11 — the member booking, under the same predicate.
|
|
1166
|
-
await authScope.register(await sliceRoutePlugin("memberBookingRoutes"));
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// Tenant-guarded domain routes (multi-tenant module) — the tenant
|
|
1170
|
-
// preHandler (default-deny guard, see middleware/tenant.ts) runs after
|
|
1171
|
-
// auth for every route in this inner scope. No domain handler executes
|
|
1172
|
-
// without request.tenant, and without the multi-tenant module there is
|
|
1173
|
-
// no tenant concept at all — the whole scope is module-conditional
|
|
1174
|
-
// (S1, #5088), so an OSS build that excludes multi-tenant registers
|
|
1175
|
-
// none of it.
|
|
1176
|
-
if (isModuleEnabled("multi-tenant")) {
|
|
1177
|
-
await authScope.register(async (tenantScope) => {
|
|
1178
|
-
await tenantScope.register(await tenantPreHandlerSlicePlugin());
|
|
1179
|
-
await tenantScope.register(
|
|
1180
|
-
await sliceRoutePlugin("workspaceSessionRoutes"),
|
|
1181
|
-
);
|
|
1182
|
-
await tenantScope.register(await sliceRoutePlugin("sessionRoutes"));
|
|
1183
|
-
// Org-billing routes (#3701) — org-scoped read + Stripe customer-portal.
|
|
1184
|
-
await tenantScope.register(await sliceRoutePlugin("billingRoutes"));
|
|
1185
|
-
// The workspace Offerings routes (#8926 D1) — the Library → Offerings
|
|
1186
|
-
// tab's read and its selection write, beside the billing routes that
|
|
1187
|
-
// write the Stripe connection they need.
|
|
1188
|
-
await tenantScope.register(await sliceRoutePlugin("workspaceOfferingsRoutes"));
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// REVIEW-SURFACE routes — a sibling tenant scope, gated on EITHER module
|
|
1193
|
-
// (#5677). The waitlist operator dashboard and the project-listings
|
|
1194
|
-
// dashboard were registered inside the multi-tenant scope above, which was
|
|
1195
|
-
// too narrow: `signal-collectors` (Pro) is the module that SELLS the
|
|
1196
|
-
// instruments these routes read back — flows, deliverables, project
|
|
1197
|
-
// listings, waitlist experiments — so a Pro customer could collect
|
|
1198
|
-
// submissions and not read them. That was an accident of registration
|
|
1199
|
-
// scope, not a pricing decision (#5677). Widening the gate is the
|
|
1200
|
-
// correction.
|
|
1201
|
-
//
|
|
1202
|
-
// This is a REGISTRATION change, not a tenant-isolation change. The scope
|
|
1203
|
-
// registers `tenantPreHandlerForBuild()`, which on a multi-tenant build is
|
|
1204
|
-
// byte-for-byte the guard these two route plugins already ran behind, and
|
|
1205
|
-
// on a build without the module is the base-build guard with the same
|
|
1206
|
-
// default-deny posture over the seeded default Workspace. Each plugin
|
|
1207
|
-
// registers exactly once — they live here and nowhere else, so the scopes
|
|
1208
|
-
// cannot collide on a path — and every OTHER route in the scope above
|
|
1209
|
-
// (workspaceSessionRoutes, sessionRoutes, billingRoutes) stays
|
|
1210
|
-
// multi-tenant-only: those are Organization/Workspace administration, not
|
|
1211
|
-
// a site's own submissions.
|
|
1212
|
-
if (
|
|
1213
|
-
isModuleEnabled("multi-tenant") ||
|
|
1214
|
-
isModuleEnabled("signal-collectors")
|
|
1215
|
-
) {
|
|
1216
|
-
await authScope.register(async (tenantScope) => {
|
|
1217
|
-
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
1218
|
-
// Waitlist operator-dashboard read API (#4665 D7, B0) — tenant-scoped
|
|
1219
|
-
// list/comparison/signups reads over A1's waitlist-experiment rows.
|
|
1220
|
-
await tenantScope.register(
|
|
1221
|
-
await sliceRoutePlugin("waitlistExperimentRoutes"),
|
|
1222
|
-
);
|
|
1223
|
-
// Project-listings dashboard (#4998) — tenant-scoped list/get/patch
|
|
1224
|
-
// over the founder's-application-row lifecycle.
|
|
1225
|
-
const { projectListingTenantRoutes } =
|
|
1226
|
-
await import("./routes/project-listings/index.js");
|
|
1227
|
-
await tenantScope.register(projectListingTenantRoutes);
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// The magnet Admin authoring surface (#6157 S5c) — its OWN sibling
|
|
1232
|
-
// tenant scope (D3), not a line inside the review-surface scope above:
|
|
1233
|
-
// its module-exclusion row stays its own, and the scope can be read,
|
|
1234
|
-
// moved or gated without disturbing `waitlistExperimentRoutes` /
|
|
1235
|
-
// `projectListingTenantRoutes`. Gated `multi-tenant || signal-collectors`
|
|
1236
|
-
// (D4), VERBATIM the review-surface expression above — not narrowed to
|
|
1237
|
-
// `(multi-tenant || signal-collectors) && <tenancy module>`, which would
|
|
1238
|
-
// put the magnet surface out of reach of `marketing-site` (the Pro
|
|
1239
|
-
// flagship template, which takes `signal-collectors` WITHOUT
|
|
1240
|
-
// `multi-tenant`). The delivery is brought up to the gate instead
|
|
1241
|
-
// (`packages/create-ailk/src/module-architecture.ts` D14/D15/D16), the
|
|
1242
|
-
// same correction #5677 already made once for this exact scope.
|
|
1243
|
-
if (
|
|
1244
|
-
isModuleEnabled("multi-tenant") ||
|
|
1245
|
-
isModuleEnabled("signal-collectors")
|
|
1246
|
-
) {
|
|
1247
|
-
await authScope.register(async (tenantScope) => {
|
|
1248
|
-
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
1249
|
-
await tenantScope.register(await sliceRoutePlugin("magnetRoutes"));
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
// The appointments read (#6085, brief H7) — a sibling tenant scope,
|
|
1254
|
-
// gated on `scheduling` alone (§ D6). NOT the anonymous schedule scope
|
|
1255
|
-
// below (`isModuleEnabled("scheduling")`'s OTHER site, § 1.9/§ D6): a
|
|
1256
|
-
// prospect booking a call has no account, so that scope carries no
|
|
1257
|
-
// auth preHandler, and mounting this read there would publish every
|
|
1258
|
-
// attendee's name and email to the internet. `tenantPreHandlerForBuild()`
|
|
1259
|
-
// is the SAME default-deny guard the review-surface scope above uses —
|
|
1260
|
-
// on a build with no `multi-tenant` module it fails closed unless
|
|
1261
|
-
// `AILK_OPERATOR_EMAIL` is pinned (`middleware/default-tenant.ts`).
|
|
1262
|
-
//
|
|
1263
|
-
// The meetings config route (#6183 S9b) shares this exact scope and
|
|
1264
|
-
// gate, for the same reason: its GET reaches
|
|
1265
|
-
// apps/api/src/services/scheduling/ (D24's listEventTypes join), which
|
|
1266
|
-
// is undeliverable to a scaffold without this module — see
|
|
1267
|
-
// routes/workspaces/meetings.ts's own header.
|
|
1268
|
-
if (isModuleEnabled("scheduling")) {
|
|
1269
|
-
await authScope.register(async (tenantScope) => {
|
|
1270
|
-
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
1271
|
-
await tenantScope.register(await sliceRoutePlugin("listAppointmentsRoute"));
|
|
1272
|
-
await tenantScope.register(await sliceRoutePlugin("workspaceMeetingsRoutes"));
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// Workspace SETTINGS routes — a sibling tenant scope, gated on EITHER
|
|
1277
|
-
// module (#5371 D2). The site-config routes were registered inside the
|
|
1278
|
-
// multi-tenant scope above, which was too narrow: nothing in their logic
|
|
1279
|
-
// is multi-tenant-specific, and since Phase 2 `resolveActiveTenant`
|
|
1280
|
-
// resolves a single-tenant session's TenantContext from the default
|
|
1281
|
-
// Organization/Workspace exactly like a multi-tenant one. Widening the
|
|
1282
|
-
// gate is the correction; duplicating the route file would encode the
|
|
1283
|
-
// original mis-scoping twice.
|
|
1284
|
-
//
|
|
1285
|
-
// This is a REGISTRATION change, not a tenant-isolation change: the scope
|
|
1286
|
-
// registers its own `tenantPreHandlerForBuild()`, the same default-deny
|
|
1287
|
-
// guard the scopes above use. The remaining routes in the multi-tenant
|
|
1288
|
-
// scope above (workspaceSessionRoutes, sessionRoutes, billingRoutes) stay
|
|
1289
|
-
// multi-tenant-only.
|
|
1290
|
-
//
|
|
1291
|
-
// #5677's selector fixes ONE of this scope's two stripped-scaffold
|
|
1292
|
-
// problems and not the other, so do not read it as making a
|
|
1293
|
-
// `--modules workspace` scaffold work. It removes the guard's dependency
|
|
1294
|
-
// on `./middleware/tenant.js` (a multi-tenant slice file, which threw
|
|
1295
|
-
// ERR_MODULE_NOT_FOUND during construction). The three route plugins
|
|
1296
|
-
// below are still reached through slice loads, and
|
|
1297
|
-
// `routes/workspaces/config.ts` remains a multi-tenant `ownedFiles` entry
|
|
1298
|
-
// — `general.ts` and `members.ts` are workspace-owned, `config.ts` is not
|
|
1299
|
-
// — so that scaffold still has no `routes/workspaces/` directory and
|
|
1300
|
-
// still throws here. Fixing it is the same one-line class of change #5677
|
|
1301
|
-
// made for the review surface: co-own `config.ts` under `workspace` in
|
|
1302
|
-
// MODULE_SLICES. Out of scope for #5677, which owns the review surface.
|
|
1303
|
-
//
|
|
1304
|
-
// A build with BOTH modules enabled registers each of the three route
|
|
1305
|
-
// plugins exactly once: they live here and nowhere else, so the two
|
|
1306
|
-
// scopes cannot collide on a path.
|
|
1307
|
-
if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
|
|
1308
|
-
await authScope.register(async (tenantScope) => {
|
|
1309
|
-
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
1310
|
-
await tenantScope.register(
|
|
1311
|
-
await sliceRoutePlugin("workspaceConfigRoutes"),
|
|
1312
|
-
);
|
|
1313
|
-
// The Pro workspace module's own two routes (#5371): the workspace
|
|
1314
|
-
// name, and the read-only member roster.
|
|
1315
|
-
await tenantScope.register(
|
|
1316
|
-
await sliceRoutePlugin("workspaceGeneralRoutes"),
|
|
1317
|
-
);
|
|
1318
|
-
await tenantScope.register(
|
|
1319
|
-
await sliceRoutePlugin("workspaceMembersRoutes"),
|
|
1320
|
-
);
|
|
1321
|
-
// The Inbox message API (#6216 S7e) — the SAME scope, the SAME
|
|
1322
|
-
// default-deny guard: list, item read, and the handled stamp.
|
|
1323
|
-
await tenantScope.register(
|
|
1324
|
-
await sliceRoutePlugin("workspaceMessageRoutes"),
|
|
1325
|
-
);
|
|
1326
|
-
// The Inbox submissions and subscribers reads (#7535) — the SAME
|
|
1327
|
-
// scope and guard: every flow completion and waitlist signup (list,
|
|
1328
|
-
// item read, handled stamp), and the read-only newsletter signups.
|
|
1329
|
-
await tenantScope.register(
|
|
1330
|
-
await sliceRoutePlugin("workspaceSubmissionRoutes"),
|
|
1331
|
-
);
|
|
1332
|
-
await tenantScope.register(
|
|
1333
|
-
await sliceRoutePlugin("workspaceSubscriberRoutes"),
|
|
1334
|
-
);
|
|
1335
|
-
// The Inbox orders read (#9170) — the SAME scope and guard: the
|
|
1336
|
-
// workspace's purchases (list, item read), behind `inbox-read`.
|
|
1337
|
-
await tenantScope.register(
|
|
1338
|
-
await sliceRoutePlugin("workspaceOrderRoutes"),
|
|
1339
|
-
);
|
|
1340
|
-
// The People reads (#852 D8) — the SAME scope and guard: the
|
|
1341
|
-
// workspace's clients and one Person with its derived timeline,
|
|
1342
|
-
// behind `people-read` (owner, admin, a granted agent).
|
|
1343
|
-
await tenantScope.register(
|
|
1344
|
-
await sliceRoutePlugin("workspacePeopleRoutes"),
|
|
1345
|
-
);
|
|
1346
|
-
// The member area's operator routes (#7877 D9) — the SAME scope and
|
|
1347
|
-
// guard: a client's pieces, plan and outbound messages, and the
|
|
1348
|
-
// workspace's declared piece stages. Writes need `people-write`
|
|
1349
|
-
// (owner, admin; never an agent).
|
|
1350
|
-
await tenantScope.register(
|
|
1351
|
-
await sliceRoutePlugin("workspacePieceRoutes"),
|
|
1352
|
-
);
|
|
1353
|
-
// #8171 D8 — the Requests read: the inbound member rows, behind
|
|
1354
|
-
// `inbox-read`; the content only to a role holding `people-read`.
|
|
1355
|
-
await tenantScope.register(
|
|
1356
|
-
await sliceRoutePlugin("workspaceRequestRoutes"),
|
|
1357
|
-
);
|
|
1358
|
-
// #9254 DD5 — the Callbacks read: the request-and-confirm rows the
|
|
1359
|
-
// Inbox lists under Interested, behind `inbox-read`.
|
|
1360
|
-
await tenantScope.register(
|
|
1361
|
-
await sliceRoutePlugin("workspaceCallbackRoutes"),
|
|
1362
|
-
);
|
|
1363
|
-
// #9287 DD2 — Start, Get and Extend trial, behind `billing-manage`
|
|
1364
|
-
// (the owner alone; never an agent).
|
|
1365
|
-
await tenantScope.register(
|
|
1366
|
-
await sliceRoutePlugin("workspaceTrialRoutes"),
|
|
1367
|
-
);
|
|
1368
|
-
await tenantScope.register(
|
|
1369
|
-
await sliceRoutePlugin("workspacePlanRoutes"),
|
|
1370
|
-
);
|
|
1371
|
-
await tenantScope.register(
|
|
1372
|
-
await sliceRoutePlugin("workspaceOutboundMessageRoutes"),
|
|
1373
|
-
);
|
|
1374
|
-
// The agent seat (#3591 PR-B) — the SAME scope and guard: owners and
|
|
1375
|
-
// admins list, add, re-key and switch off agents (`agents-manage`),
|
|
1376
|
-
// and an agent reads its own context at GET /v1/agent/context.
|
|
1377
|
-
await tenantScope.register(
|
|
1378
|
-
await sliceRoutePlugin("workspaceAgentRoutes"),
|
|
1379
|
-
);
|
|
1380
|
-
// The asset store's MANAGE routes — list, PATCH, DELETE (#6093 D16).
|
|
1381
|
-
// Registered HERE, in the plain tenant scope, and NOT inside the
|
|
1382
|
-
// upload's own nested strict-rate-limit scope below: a JSON
|
|
1383
|
-
// list/patch/delete has no 20 MB body to bound, and inheriting that
|
|
1384
|
-
// per-IP ceiling would throttle the Admin's own list against its
|
|
1385
|
-
// siblings.
|
|
1386
|
-
await tenantScope.register(
|
|
1387
|
-
await sliceRoutePlugin("workspaceAssetManageRoutes"),
|
|
1388
|
-
);
|
|
1389
|
-
// The workspace asset store's upload half (#5854). It belongs in THIS
|
|
1390
|
-
// scope and not beside the public routes below, because the two
|
|
1391
|
-
// refusals its AC names — no granted tenant, and a `:workspaceId` that
|
|
1392
|
-
// is not the session's — are `tenantPreHandlerForBuild()`'s to make,
|
|
1393
|
-
// before any handler runs and so before any write. Its raw-body parser
|
|
1394
|
-
// and its `onRequest` size/type hook are declared INSIDE that plugin,
|
|
1395
|
-
// so they encapsulate to it and reach neither the public read below nor
|
|
1396
|
-
// any sibling here.
|
|
1397
|
-
//
|
|
1398
|
-
// Its own rate-limit scope, at the strict per-IP ceiling the leads and
|
|
1399
|
-
// scheduling scopes use (#5854 security review round 1, MEDIUM). Auth
|
|
1400
|
-
// and tenant are `preHandler`s, so an UNAUTHENTICATED caller still
|
|
1401
|
-
// reaches the parser and can make the server buffer up to 20 MB before
|
|
1402
|
-
// the 401. `rateLimitMiddleware` is an `onRequest` hook, so scoping it
|
|
1403
|
-
// here is what bounds how often that can be done — the cap bounds one
|
|
1404
|
-
// body, this bounds the rate. Nested so the ceiling applies to the
|
|
1405
|
-
// upload alone and not to the config/general/members siblings above.
|
|
1406
|
-
await tenantScope.register(async (assetUploadScope) => {
|
|
1407
|
-
assetUploadScope.register(rateLimitMiddleware, {
|
|
1408
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1409
|
-
});
|
|
1410
|
-
await assetUploadScope.register(
|
|
1411
|
-
await sliceRoutePlugin("workspaceAssetUploadRoute"),
|
|
1412
|
-
);
|
|
1413
|
-
});
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// The editor's tenant-guarded content-write surface (#5853, roadmap D4).
|
|
1418
|
-
// Its OWN sibling scope (D9) — not nested inside the workspace/multi-tenant
|
|
1419
|
-
// scope above, so its module-exclusion row stays its own. The gate is
|
|
1420
|
-
// `content-write` AND a tenancy module: the route file imports
|
|
1421
|
-
// `lib/tenant-db.ts`, which only the `workspace` / `multi-tenant` slices
|
|
1422
|
-
// deliver, so a scaffold enabling `content-write` without either would
|
|
1423
|
-
// throw ERR_MODULE_NOT_FOUND at build() — the exact defect
|
|
1424
|
-
// `tools/lint-slice-registration.ts` (required in CI) refuses. D9's
|
|
1425
|
-
// "content-write alone" premise was falsified by that lint on PR #5921;
|
|
1426
|
-
// the path itself is workspace-scoped (`/v1/workspaces/:workspaceId/…`),
|
|
1427
|
-
// so requiring a tenancy module is coherent, not a narrowing.
|
|
1428
|
-
// `tenantPreHandlerForBuild()` still selects the base-build
|
|
1429
|
-
// default-workspace guard on a `workspace`-only build. The plugin is
|
|
1430
|
-
// registered here and nowhere else, so no scope can collide on a path.
|
|
1431
|
-
//
|
|
1432
|
-
// The module flags are read ONCE, here, at build time (D10): on a build
|
|
1433
|
-
// that excludes them the routes are never registered and Fastify's own 404
|
|
1434
|
-
// answers — no auth, no tenant resolution, no handler-level
|
|
1435
|
-
// `isModuleEnabled`.
|
|
1436
|
-
if (
|
|
1437
|
-
isModuleEnabled("content-write") &&
|
|
1438
|
-
(isModuleEnabled("workspace") || isModuleEnabled("multi-tenant"))
|
|
1439
|
-
) {
|
|
1440
|
-
await authScope.register(async (tenantScope) => {
|
|
1441
|
-
// D12: the authenticated ceiling (600/min), scope-local. The global
|
|
1442
|
-
// limiter above is the unauth 60/min, which an autosaving editor would
|
|
1443
|
-
// trip against its own user.
|
|
1444
|
-
await tenantScope.register(rateLimit, {
|
|
1445
|
-
max: CONTENT_RATE_LIMIT_AUTH,
|
|
1446
|
-
timeWindow: CONTENT_RATE_LIMIT_WINDOW,
|
|
1447
|
-
});
|
|
1448
|
-
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
1449
|
-
await tenantScope.register(
|
|
1450
|
-
await sliceRoutePlugin("workspaceContentRoutes"),
|
|
1451
|
-
);
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
});
|
|
1455
|
-
|
|
1456
|
-
// Public routes — no auth required.
|
|
1457
|
-
//
|
|
1458
|
-
// The workspace asset store's READ half (#5854), under the SAME predicate as
|
|
1459
|
-
// its upload above rather than unconditionally: with no upload path in the
|
|
1460
|
-
// build it would be a permanent 404 advertised on both self-description
|
|
1461
|
-
// surfaces. It cannot join the block above — that block is nested in
|
|
1462
|
-
// `authScope`, and an `<img src>` carries no session.
|
|
1463
|
-
if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
|
|
1464
|
-
await server.register(await sliceRoutePlugin("publicAssetReadRoute"));
|
|
1465
|
-
}
|
|
1466
|
-
await server.register(listTypesRoute);
|
|
1467
|
-
// Before `listPagesRoute`: `search`, `offerings`, `pricing` and
|
|
1468
|
-
// `shown-offerings` must never be read as a `:type`.
|
|
1469
|
-
await server.register(searchContentRoute);
|
|
1470
|
-
// The Organization's Stripe catalog source (#8922), so a stale snapshot
|
|
1471
|
-
// refreshes on the service's bounded, never-forced path (#8924 D7, #9124;
|
|
1472
|
-
// the bounds are `refreshWorkspaceSnapshot`'s). The two agent twins read
|
|
1473
|
-
// the shown set through the same read with these same options, so all
|
|
1474
|
-
// three refresh together (#8933 D5).
|
|
1475
|
-
const shownSetRouteOptions: ShownOfferingsRouteOptions = {
|
|
1476
|
-
sourceFor: stripeCatalogSourceFor,
|
|
1477
|
-
};
|
|
1478
|
-
await server.register(offeringsRoute, shownSetRouteOptions);
|
|
1479
|
-
await server.register(pricingRoute, shownSetRouteOptions);
|
|
1480
|
-
await server.register(shownOfferingsRoute, shownSetRouteOptions);
|
|
1481
|
-
await server.register(listPagesRoute);
|
|
1482
|
-
await server.register(getPageRoute);
|
|
1483
|
-
// The hub's sign-in page names the spoke that sent a visitor (#8790 D2):
|
|
1484
|
-
// a public read of one client's name + origin from the registry. No auth —
|
|
1485
|
-
// the visitor is signed out by definition when the page asks.
|
|
1486
|
-
await server.register(hubSignInPublicRoutes);
|
|
1487
|
-
// Checkout launcher + reader — commerce PRIMITIVES (D-64, #5796; the GET
|
|
1488
|
-
// added by #6084). Registered UNCONDITIONALLY and gated at CALL time on
|
|
1489
|
-
// the JWT `commerce` claim, the same posture content-write has
|
|
1490
|
-
// (requirePaidTier above), so adding commerce to a live site is a licence
|
|
1491
|
-
// change and lands no files on its repo. Without the claim the gate
|
|
1492
|
-
// answers the module-disabled 404 — byte-identical to the build that
|
|
1493
|
-
// never registered the route. `checkoutRoutes` (`routes/checkout/index.ts`)
|
|
1494
|
-
// registers BOTH `POST /v1/checkout/sessions` and
|
|
1495
|
-
// `GET /v1/checkout/sessions/:id` — the GET in its own nested,
|
|
1496
|
-
// rate-limited sub-scope (D16), so this preHandler applies to both while
|
|
1497
|
-
// the GET's own limiter applies to it alone.
|
|
1498
|
-
//
|
|
1499
|
-
// #8450 § 9 SEC-14 — a LIVE process gives both routes their own per-IP
|
|
1500
|
-
// `checkout` bucket on top of the global limiter, because live, the public
|
|
1501
|
-
// GET spends one read of the shared live Stripe account per request. It
|
|
1502
|
-
// lowers the per-IP ceiling per instance (the POST's from the global 20 to
|
|
1503
|
-
// 10; the POST and GET now also share one count); like every limiter here
|
|
1504
|
-
// it is in-memory, so it does not bound the total across serverless
|
|
1505
|
-
// instances. `onRequest`, so it counts before the claim gate. Accepted
|
|
1506
|
-
// residual: a server-side caller — apps/web's server actions, an MCP host's
|
|
1507
|
-
// `purchase` tool — puts all its buyers on its own egress IP, so they share
|
|
1508
|
-
// one bucket; `AILK_RATE_LIMIT_CHECKOUT_PER_MINUTE` raises the ceiling. A
|
|
1509
|
-
// test-mode process registers nothing new.
|
|
1510
|
-
await server.register(async (checkoutScope) => {
|
|
1511
|
-
if (stripeMode === "live") {
|
|
1512
|
-
await checkoutScope.register(rateLimitMiddleware, {
|
|
1513
|
-
scope: "checkout",
|
|
1514
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1515
|
-
});
|
|
1516
|
-
}
|
|
1517
|
-
checkoutScope.addHook("preHandler", requireCommerceClaim);
|
|
1518
|
-
await checkoutScope.register(await sliceRoutePlugin("checkoutRoutes"));
|
|
1519
|
-
});
|
|
1520
|
-
// Request-and-confirm (#5970, D-67 / SUB-100) — the tenant's confirm form
|
|
1521
|
-
// and the Payment Link it mints. A money path, so it sits INSIDE the
|
|
1522
|
-
// commerce claim (DECISION-LOG D-63; D-67 assigns request-and-confirm to
|
|
1523
|
-
// the Commerce rung rather than exempting it), and the gate is applied by
|
|
1524
|
-
// COMPOSITION here — the checkout scope above, line for line. That is what
|
|
1525
|
-
// lets the route files themselves read no licence claim at all (#5970 AC-4).
|
|
1526
|
-
//
|
|
1527
|
-
// Rate-limited at the leads scope's own strict value: the confirm link is
|
|
1528
|
-
// unauthenticated (the token IS the credential), so it belongs on the same
|
|
1529
|
-
// footing as the anonymous surface that mails it. Note the same caveat
|
|
1530
|
-
// `server.ts` records elsewhere — `trustProxy` is on, so an IP limiter is
|
|
1531
|
-
// evadable; the bound that actually holds is per ROW, one live credential
|
|
1532
|
-
// per request, spent on use.
|
|
1533
|
-
await server.register(async (requestsScope) => {
|
|
1534
|
-
requestsScope.register(rateLimitMiddleware, {
|
|
1535
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1536
|
-
});
|
|
1537
|
-
// `requireCommerceClaim` short-circuits with `reply.send`, so a wrapper is
|
|
1538
|
-
// the only place the unclaimed case can be OBSERVED: the preHandler ends
|
|
1539
|
-
// the request before any handler runs, and the gate itself logs only on
|
|
1540
|
-
// `undetermined` (`lib/require-commerce-claim.ts`).
|
|
1541
|
-
//
|
|
1542
|
-
// Spec D3 names this line as one of two bounds on the residual it
|
|
1543
|
-
// discloses — a site configured for request-and-confirm but carrying no
|
|
1544
|
-
// `commerce` claim emails a tenant a confirm link that answers 404. The
|
|
1545
|
-
// spec puts the log in the route, where it cannot be emitted; this is the
|
|
1546
|
-
// registration, which is where it can (#5970 acceptance review, advisory
|
|
1547
|
-
// 4). Without it the misconfiguration is visible only in the tenant's
|
|
1548
|
-
// inbox, which is the one place the operator is not looking.
|
|
1549
|
-
requestsScope.addHook("preHandler", async (request, reply) => {
|
|
1550
|
-
await requireCommerceClaim(request, reply);
|
|
1551
|
-
if (reply.sent) {
|
|
1552
|
-
request.log.warn(
|
|
1553
|
-
{ outcome: "confirm_link_unclaimed", path: request.routeOptions.url },
|
|
1554
|
-
"request-confirm: a confirm link was opened on a site without the commerce claim",
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
});
|
|
1558
|
-
await requestsScope.register(requestRoutes);
|
|
1559
|
-
});
|
|
1560
|
-
// Deliverable download — a capability-token URL (#3961 D5); the token
|
|
1561
|
-
// itself is the sole gate, so no auth preHandler applies. Module-conditional
|
|
1562
|
-
// (signal-collectors, D-48): gated PDFs and reports.
|
|
1563
|
-
if (isModuleEnabled("signal-collectors")) {
|
|
1564
|
-
await server.register(await sliceRoutePlugin("deliverableDownloadRoute"));
|
|
1565
|
-
// #8848 D5 — the ungated file read, anonymous and keyed by slug, so it
|
|
1566
|
-
// gets its OWN encapsulated scope: `rateLimitMiddleware` breaks
|
|
1567
|
-
// encapsulation, so it is never installed on the grant route's
|
|
1568
|
-
// registration above, whose budget stays as it is. With the hub's
|
|
1569
|
-
// forward-key digests (the route is on FORWARDED_VISITOR_ROUTES), a
|
|
1570
|
-
// site's proxied reads are counted per visitor (SEC-8848-C).
|
|
1571
|
-
await server.register(async (deliverableOpenScope) => {
|
|
1572
|
-
deliverableOpenScope.register(rateLimitMiddleware, {
|
|
1573
|
-
perIpPerMinute: 60,
|
|
1574
|
-
scope: "deliverableopen",
|
|
1575
|
-
trustedKeyDigests: visitorForwardKeyDigests,
|
|
1576
|
-
});
|
|
1577
|
-
await deliverableOpenScope.register(await sliceRoutePlugin("deliverableOpenRoute"));
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// Webhooks routes — raw-body parser scoped to this sub-router only so the
|
|
1582
|
-
// global JSON parser on all other routes is unaffected.
|
|
1583
|
-
await server.register(async (webhooksScope) => {
|
|
1584
|
-
webhooksScope.addContentTypeParser(
|
|
1585
|
-
"application/json",
|
|
1586
|
-
{ parseAs: "buffer" },
|
|
1587
|
-
(_req, body, done) => {
|
|
1588
|
-
done(null, body);
|
|
1589
|
-
},
|
|
1590
|
-
);
|
|
1591
|
-
// Slack's block_actions payload arrives as a urlencoded `payload` form
|
|
1592
|
-
// field (#4999) — raw buffer here too, so the signature is verified over
|
|
1593
|
-
// the EXACT bytes Slack signed, never a re-serialized parse.
|
|
1594
|
-
webhooksScope.addContentTypeParser(
|
|
1595
|
-
"application/x-www-form-urlencoded",
|
|
1596
|
-
{ parseAs: "buffer" },
|
|
1597
|
-
(_req, body, done) => {
|
|
1598
|
-
done(null, body);
|
|
1599
|
-
},
|
|
1600
|
-
);
|
|
1601
|
-
// Stripe webhook receiver + customer mapping — commerce PRIMITIVES
|
|
1602
|
-
// (D-64, #5796), registered UNCONDITIONALLY alongside checkout above.
|
|
1603
|
-
// NO `requireCommerceClaim` preHandler here, deliberately: D3 puts
|
|
1604
|
-
// signature verification BEFORE the claim check, so the handler itself
|
|
1605
|
-
// consults `hasCommerceClaim()` once the signature is settled — a signed
|
|
1606
|
-
// event on an unclaimed site is acked 200 and dropped rather than
|
|
1607
|
-
// retried forever, while an unsigned body gets the module-disabled 404.
|
|
1608
|
-
// See routes/webhooks/README.md § Commerce claim.
|
|
1609
|
-
await webhooksScope.register(await sliceRoutePlugin("webhooksRoutes"));
|
|
1610
|
-
// Project-listing operator decision (Slack Approve/Reject) — the "hand an
|
|
1611
|
-
// operator a decision" half of D-48's grouping sentence. Module-conditional
|
|
1612
|
-
// (signal-collectors).
|
|
1613
|
-
if (isModuleEnabled("signal-collectors")) {
|
|
1614
|
-
await webhooksScope.register(await sliceRoutePlugin("slackActionsRoute"));
|
|
1615
|
-
}
|
|
1616
|
-
});
|
|
1617
|
-
|
|
1618
|
-
// Auth transactional-email relay (#5962 § D3) — its OWN scope, deliberately
|
|
1619
|
-
// OUTSIDE `authScope`: the caller is `apps/web`'s Better Auth callbacks
|
|
1620
|
-
// acting for an anonymous visitor who is by definition not signed in, so an
|
|
1621
|
-
// auth preHandler in front of it would reject every legitimate call. The
|
|
1622
|
-
// gate is the HMAC signature over the raw body plus the 300 s replay window,
|
|
1623
|
-
// both verified inside the handler.
|
|
1624
|
-
//
|
|
1625
|
-
// Raw-body parser scoped to this sub-router only, for the same reason
|
|
1626
|
-
// `webhooksScope` carries one: the signature must be checked over the EXACT
|
|
1627
|
-
// bytes the caller signed, never a re-serialized parse.
|
|
1628
|
-
//
|
|
1629
|
-
// `DEFAULT_PER_IP_PER_MINUTE_STRICT` (10/min) — the constant `leadsScope`
|
|
1630
|
-
// below already uses for its anonymous surface. It is defence in depth, not
|
|
1631
|
-
// the control: an unsigned body never reaches a send, and per-victim volume
|
|
1632
|
-
// is bounded by `lib/auth-email-budget.ts`.
|
|
1633
|
-
await server.register(async (authEmailScope) => {
|
|
1634
|
-
authEmailScope.register(rateLimitMiddleware, {
|
|
1635
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1636
|
-
});
|
|
1637
|
-
authEmailScope.addContentTypeParser(
|
|
1638
|
-
"application/json",
|
|
1639
|
-
{ parseAs: "buffer" },
|
|
1640
|
-
(_req, body, done) => {
|
|
1641
|
-
done(null, body);
|
|
1642
|
-
},
|
|
1643
|
-
);
|
|
1644
|
-
await authEmailScope.register(transactionalEmailRoute);
|
|
1645
|
-
});
|
|
1646
|
-
|
|
1647
|
-
// The hub signs in its spokes (#7894 D5) — `token` and `revoke`. Their OWN
|
|
1648
|
-
// scope, deliberately OUTSIDE `authScope`: the caller is a spoke's server,
|
|
1649
|
-
// authenticated by its client secret AND its spoke key inside the service,
|
|
1650
|
-
// not by a person's session — an auth preHandler in front would reject every
|
|
1651
|
-
// legitimate call. `scope: "hubsignin"` namespaces the buckets (the scope
|
|
1652
|
-
// name admits no hyphen). The per-IP limit keys on `req.ip` under
|
|
1653
|
-
// `trustProxy`, so it is a backstop only; the control is the per-client
|
|
1654
|
-
// bucket the service keys on the VERIFIED client (SEC-7894-8).
|
|
1655
|
-
await server.register(async (hubSignInScope) => {
|
|
1656
|
-
hubSignInScope.register(rateLimitMiddleware, {
|
|
1657
|
-
perIpPerMinute: 60,
|
|
1658
|
-
scope: "hubsignin",
|
|
1659
|
-
});
|
|
1660
|
-
await hubSignInScope.register(hubSignInClientRoutes);
|
|
1661
|
-
});
|
|
1662
|
-
|
|
1663
|
-
// Lead-capture status (#6730 D4) — a cheap read the web page makes before
|
|
1664
|
-
// choosing the form or the contact email. Top level, NOT in `leadsScope`:
|
|
1665
|
-
// the web server's single egress IP already spends that scope's strict
|
|
1666
|
-
// 10/min on form submissions. The global unauth ceiling above applies.
|
|
1667
|
-
await server.register(leadCaptureStatusRoute);
|
|
1668
|
-
|
|
1669
|
-
// The consent floor (#8438 D5, D9) — the cookie banner's log and the
|
|
1670
|
-
// unsubscribe landing. Top level, outside every module gate: consent
|
|
1671
|
-
// logging is part of the never-gated privacy floor. Each is in its OWN
|
|
1672
|
-
// rate-limited scope (`consent`, `unsubscribe`) so filling one never blocks
|
|
1673
|
-
// the other; with a forward key set, a spoke's visitors are counted one by
|
|
1674
|
-
// one (both routes are on FORWARDED_VISITOR_ROUTES). Without one, the
|
|
1675
|
-
// server's own IP is the bucket — the posture of the lead routes. Neither
|
|
1676
|
-
// is behind `authScope`: the caller is a visitor, or a person in their mail.
|
|
1677
|
-
await server.register(async (consentScope) => {
|
|
1678
|
-
consentScope.register(rateLimitMiddleware, {
|
|
1679
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1680
|
-
scope: "consent",
|
|
1681
|
-
trustedKeyDigests: visitorForwardKeyDigests,
|
|
1682
|
-
});
|
|
1683
|
-
await consentScope.register(consentCookieRoute);
|
|
1684
|
-
});
|
|
1685
|
-
await server.register(async (unsubscribeScope) => {
|
|
1686
|
-
unsubscribeScope.register(rateLimitMiddleware, {
|
|
1687
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1688
|
-
scope: "unsubscribe",
|
|
1689
|
-
trustedKeyDigests: visitorForwardKeyDigests,
|
|
1690
|
-
});
|
|
1691
|
-
await unsubscribeScope.register(consentUnsubscribeRoute);
|
|
1692
|
-
});
|
|
1693
|
-
// #8765 D2 — the sign-up agreements' evidence, beside the consent floor:
|
|
1694
|
-
// anonymous, outside every module gate (sign-up and consent are never-gate
|
|
1695
|
-
// surfaces). Its own scope, `signupevidence`, so an anonymous burst never
|
|
1696
|
-
// shares a bucket with the cookie banner's log (SEC-8765-D). Its main
|
|
1697
|
-
// caller is the web's own server, whose cache keeps it to one call a minute
|
|
1698
|
-
// per enabled locale per instance. #8805: a call with a verified site key
|
|
1699
|
-
// is counted per key (DEFAULT_SIGNUPEVIDENCE_PER_KEY_PER_MINUTE,
|
|
1700
|
-
// AILK_RATE_LIMIT_SIGNUPEVIDENCE_PER_KEY_PER_MINUTE), never per IP; any
|
|
1701
|
-
// other call keeps 10 a minute per IP.
|
|
1702
|
-
await server.register(async (signUpEvidenceScope) => {
|
|
1703
|
-
signUpEvidenceScope.register(rateLimitMiddleware, {
|
|
1704
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1705
|
-
perKeyPerMinute: DEFAULT_SIGNUPEVIDENCE_PER_KEY_PER_MINUTE,
|
|
1706
|
-
scope: "signupevidence",
|
|
1707
|
-
siteKeyOf: signUpEvidenceSiteKey,
|
|
1708
|
-
});
|
|
1709
|
-
await signUpEvidenceScope.register(consentSignUpRoute);
|
|
1710
|
-
});
|
|
1711
|
-
|
|
1712
|
-
// Leads routes — rate-limit middleware scoped to this sub-router. Own
|
|
1713
|
-
// default (10/min, pre-#4730) rather than inheriting the wider
|
|
1714
|
-
// flows-sized default (#5111, DEF-004 — #4730's shared-default bump was
|
|
1715
|
-
// never meant to loosen abuse protection here).
|
|
1716
|
-
//
|
|
1717
|
-
// `scope: "leads"` (#7394): this scope's buckets are its own, not shared
|
|
1718
|
-
// with the other scopes' bare-IP count, and AILK_RATE_LIMIT_LEADS_PER_MINUTE
|
|
1719
|
-
// (and ..._PER_KEY_PER_MINUTE, default 300) override the code values. With
|
|
1720
|
-
// `trustedKeyDigests`, a keyed spoke's three lead writes are counted per
|
|
1721
|
-
// forwarded visitor; the project-listing routes below still key on `req.ip`.
|
|
1722
|
-
await server.register(async (leadsScope) => {
|
|
1723
|
-
leadsScope.register(rateLimitMiddleware, {
|
|
1724
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1725
|
-
scope: "leads",
|
|
1726
|
-
trustedKeyDigests: visitorForwardKeyDigests,
|
|
1727
|
-
});
|
|
1728
|
-
await leadsScope.register(leadSubmitRoute);
|
|
1729
|
-
await leadsScope.register(leadSubscribeRoute);
|
|
1730
|
-
await leadsScope.register(leadCallbackRoute);
|
|
1731
|
-
// Project-listings anonymous surface (#4998, reshaped by #5036) — the
|
|
1732
|
-
// three-operation draft lifecycle plus the public page-render read, all
|
|
1733
|
-
// on the same rate-limited, identity-blind posture as the leads routes
|
|
1734
|
-
// above. Module-conditional (signal-collectors, D-48): the listing
|
|
1735
|
-
// instrument's application-lifecycle half.
|
|
1736
|
-
//
|
|
1737
|
-
// Do NOT read that rate limit as a bound on anonymous draft creation. It
|
|
1738
|
-
// keys on `req.ip`, and `trustProxy` is on (see the note at the top of this
|
|
1739
|
-
// file), so a caller reaching the API directly can vary
|
|
1740
|
-
// `X-Forwarded-For` and get a fresh budget per request. That is a
|
|
1741
|
-
// pre-existing property of the limiter, not of these routes; what these
|
|
1742
|
-
// routes do is keep the cost of an unbounded call small — the Lead write
|
|
1743
|
-
// with its operator notification waits for `submit` (PR #5042 security
|
|
1744
|
-
// review, finding 4).
|
|
1745
|
-
//
|
|
1746
|
-
// `start` DOES now send one email (#5406, the founder's resume link), so
|
|
1747
|
-
// the older form of this note — "`start` writes one row and sends
|
|
1748
|
-
// nothing" — no longer holds. That send is opt-in per site, and where it
|
|
1749
|
-
// is on it is bounded in the send path rather than per IP
|
|
1750
|
-
// (`services/project-listing-resume-email.ts`), precisely because this IP
|
|
1751
|
-
// limiter cannot carry the weight of an outbound-mail decision. Two bounds,
|
|
1752
|
-
// both counted in the database so they hold across instances: PER RECIPIENT
|
|
1753
|
-
// (PR #5411 security review, finding 1) and, since #5414, PER SITE across
|
|
1754
|
-
// every recipient — the residual the first one left, and the one that
|
|
1755
|
-
// decides whether a flood can cost the site its sending domain.
|
|
1756
|
-
if (isModuleEnabled("signal-collectors")) {
|
|
1757
|
-
await leadsScope.register(
|
|
1758
|
-
await sliceRoutePlugin("projectListingStartRoute"),
|
|
1759
|
-
);
|
|
1760
|
-
await leadsScope.register(
|
|
1761
|
-
await sliceRoutePlugin("projectListingDraftPatchRoute"),
|
|
1762
|
-
);
|
|
1763
|
-
await leadsScope.register(
|
|
1764
|
-
await sliceRoutePlugin("projectListingSubmitRoute"),
|
|
1765
|
-
);
|
|
1766
|
-
// #5407 — the resume link's verification write. Anonymous, and gated
|
|
1767
|
-
// on the resume token AND the nonce that only the resume email
|
|
1768
|
-
// carries, so it belongs in the same rate-limited scope as the other
|
|
1769
|
-
// three.
|
|
1770
|
-
await leadsScope.register(
|
|
1771
|
-
await sliceRoutePlugin("projectListingVerifyEmailRoute"),
|
|
1772
|
-
);
|
|
1773
|
-
await leadsScope.register(
|
|
1774
|
-
await sliceRoutePlugin("projectListingPublicRoute"),
|
|
1775
|
-
);
|
|
1776
|
-
}
|
|
1777
|
-
});
|
|
1778
|
-
|
|
1779
|
-
// The listicle directory read (#7521 D3) — anonymous, keyed by the public
|
|
1780
|
-
// site key, and rendered by the web server on every page view of a landing
|
|
1781
|
-
// page. Its OWN scope, NOT `leadsScope`: that scope's 10/min per IP would be
|
|
1782
|
-
// spent by the web server's SSR reads from one egress address, and the
|
|
1783
|
-
// directory would vanish past ~10 views a minute (SEC-7521-D1). 120/min,
|
|
1784
|
-
// overridable by AILK_RATE_LIMIT_DIRECTORY_PER_MINUTE (#7394's named-scope
|
|
1785
|
-
// rule); the web side memoises each landing page for 30 s per process.
|
|
1786
|
-
// Module-conditional (signal-collectors, D-48), so a build without the
|
|
1787
|
-
// module has no `/directory` and the web extension's read gets a 404.
|
|
1788
|
-
await server.register(async (directoryScope) => {
|
|
1789
|
-
directoryScope.register(rateLimitMiddleware, {
|
|
1790
|
-
perIpPerMinute: 120,
|
|
1791
|
-
scope: "directory",
|
|
1792
|
-
});
|
|
1793
|
-
if (isModuleEnabled("signal-collectors")) {
|
|
1794
|
-
await directoryScope.register(
|
|
1795
|
-
await sliceRoutePlugin("projectListingDirectoryRoute"),
|
|
1796
|
-
);
|
|
1797
|
-
}
|
|
1798
|
-
});
|
|
1799
|
-
|
|
1800
|
-
// Flows routes — anonymous surface; rate-limit middleware scoped to this
|
|
1801
|
-
// sub-router, matching the leads scope above (#3956 D4 Clarifications:
|
|
1802
|
-
// "follows whatever the existing anonymous /v1/leads/* routes do today").
|
|
1803
|
-
// Keeps the wider, env-var-driven default (#4730 — see
|
|
1804
|
-
// apps/api/src/routes/flows/README.md § Rate limit) — this is the ONE
|
|
1805
|
-
// scope #4730's bump was actually sized for; unchanged by #5111.
|
|
1806
|
-
await server.register(async (flowsScope) => {
|
|
1807
|
-
flowsScope.register(rateLimitMiddleware);
|
|
1808
|
-
// Flow engine reads (#3956) — module-conditional (signal-collectors,
|
|
1809
|
-
// D-48). The scope itself and its rate limiter above stay unconditional
|
|
1810
|
-
// (D2) — commerce's recommender routes below need them too.
|
|
1811
|
-
if (isModuleEnabled("signal-collectors")) {
|
|
1812
|
-
await flowsScope.register(await sliceRoutePlugin("getFlowRoute"));
|
|
1813
|
-
await flowsScope.register(await sliceRoutePlugin("submitFlowStepRoute"));
|
|
1814
|
-
await flowsScope.register(
|
|
1815
|
-
await sliceRoutePlugin("getFlowAggregateRoute"),
|
|
1816
|
-
);
|
|
1817
|
-
await flowsScope.register(
|
|
1818
|
-
await sliceRoutePlugin("putFlowDefinitionRoute"),
|
|
1819
|
-
);
|
|
1820
|
-
}
|
|
1821
|
-
// The recommender's email-moment routes (#4841) — same anonymous,
|
|
1822
|
-
// rate-limited posture as the three above. Module-conditional (commerce,
|
|
1823
|
-
// D-47): the recommender leaves Pro flows and moves to commerce whole.
|
|
1824
|
-
if (isModuleEnabled("commerce")) {
|
|
1825
|
-
await flowsScope.register(
|
|
1826
|
-
await sliceRoutePlugin("saveRecommendationRoute"),
|
|
1827
|
-
);
|
|
1828
|
-
await flowsScope.register(
|
|
1829
|
-
await sliceRoutePlugin("checkoutRecommendationRoute"),
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
});
|
|
1833
|
-
|
|
1834
|
-
// AEO score routes (#4666) — anonymous surface, same posture as the flows
|
|
1835
|
-
// scope above: a waitlist respondent has no account. Rate-limited because
|
|
1836
|
-
// each call fans out to the respondent's own site (an external fetch), the
|
|
1837
|
-
// same reason the schedule scope below is rate-limited for a third party.
|
|
1838
|
-
// Own default (10/min, pre-#4730) — see the leads scope's comment above
|
|
1839
|
-
// (#5111, DEF-004).
|
|
1840
|
-
await server.register(async (aeoScope) => {
|
|
1841
|
-
aeoScope.register(rateLimitMiddleware, {
|
|
1842
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1843
|
-
});
|
|
1844
|
-
await aeoScope.register(scoreAeoRoute);
|
|
1845
|
-
await aeoScope.register(exampleAeoRoute);
|
|
1846
|
-
});
|
|
1847
|
-
|
|
1848
|
-
// Schedule routes (#4416) — anonymous surface, same posture as the flows
|
|
1849
|
-
// and leads scopes above: a prospect booking a call has no account, and the
|
|
1850
|
-
// calLink in the request names the event, so there is no tenant scope to
|
|
1851
|
-
// bind. Rate-limited because each call fans out to a third-party provider.
|
|
1852
|
-
// Own default (10/min, pre-#4730) — see the leads scope's comment above
|
|
1853
|
-
// (#5111, DEF-004). Module-conditional (S1, #5088): the scheduling module
|
|
1854
|
-
// owns this whole route set, so an OSS build that excludes it registers
|
|
1855
|
-
// neither the routes nor their imports.
|
|
1856
|
-
//
|
|
1857
|
-
// `scope: "schedule"` with `trustedKeyDigests` (#7633, the relay's security
|
|
1858
|
-
// review): a site's same-origin relay sends every visitor's availability
|
|
1859
|
-
// and booking call from its own server, so with the forwarding key each
|
|
1860
|
-
// visitor is counted separately, and the key as a whole is capped at
|
|
1861
|
-
// DEFAULT_SCHEDULE_PER_KEY_PER_MINUTE. The scope's buckets are its own,
|
|
1862
|
-
// and AILK_RATE_LIMIT_SCHEDULE_PER_MINUTE / ..._PER_KEY_PER_MINUTE override
|
|
1863
|
-
// the code values, as the leads scope's do.
|
|
1864
|
-
if (isModuleEnabled("scheduling")) {
|
|
1865
|
-
await server.register(async (scheduleScope) => {
|
|
1866
|
-
scheduleScope.register(rateLimitMiddleware, {
|
|
1867
|
-
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1868
|
-
scope: "schedule",
|
|
1869
|
-
trustedKeyDigests: visitorForwardKeyDigests,
|
|
1870
|
-
perKeyPerMinute: DEFAULT_SCHEDULE_PER_KEY_PER_MINUTE,
|
|
1871
|
-
});
|
|
1872
|
-
await scheduleScope.register(
|
|
1873
|
-
await sliceRoutePlugin("listAvailabilityRoute"),
|
|
1874
|
-
);
|
|
1875
|
-
await scheduleScope.register(await sliceRoutePlugin("getEventMetaRoute"));
|
|
1876
|
-
await scheduleScope.register(
|
|
1877
|
-
await sliceRoutePlugin("createBookingRoute"),
|
|
1878
|
-
);
|
|
1879
|
-
// #6183 S9b D2/D12 — the enabled-meeting-slugs public read. Same scope
|
|
1880
|
-
// as the three routes above: it inherits the module gate and the
|
|
1881
|
-
// strict per-IP rate limit (§9 SEC-4).
|
|
1882
|
-
await scheduleScope.register(
|
|
1883
|
-
await sliceRoutePlugin("meetingTypesRoute"),
|
|
1884
|
-
);
|
|
1885
|
-
// #8843 D1 — the meetings read: the same gate and per-IP limit.
|
|
1886
|
-
await scheduleScope.register(
|
|
1887
|
-
await sliceRoutePlugin("listMeetingsRoute"),
|
|
1888
|
-
);
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// Test-auth route — triple-gated (NODE_ENV !== production AND
|
|
1893
|
-
// AILK_TEST_AUTH_ENABLED === 'true' AND AILK_TEST_AUTH_USER_ALLOWLIST
|
|
1894
|
-
// non-empty). When any gate fails, the route is never registered and
|
|
1895
|
-
// Fastify returns its default 404 — the surface NEVER advertises itself
|
|
1896
|
-
// in production. See #517 / docs/internal/roadmaps/e2e-testing-roadmap.md.
|
|
1897
|
-
if (shouldMountTestAuth()) {
|
|
1898
|
-
await server.register(registerTestAuthRoute);
|
|
1899
|
-
server.log.warn(
|
|
1900
|
-
"test-auth route MOUNTED — only safe in dev/test environments",
|
|
1901
|
-
);
|
|
126
|
+
export async function build(): Promise<FastifyInstance> {
|
|
127
|
+
const modules: ApiModule[] = [];
|
|
128
|
+
for (const name of OPTIONAL_MODULES) {
|
|
129
|
+
const { specifier, load } = MODULE_OBJECTS[name];
|
|
130
|
+
const mod = (await loadSlice(specifier, load)) as { module?: ApiModule };
|
|
131
|
+
if (!mod.module) {
|
|
132
|
+
throw new Error(`module export missing from ${specifier}`);
|
|
133
|
+
}
|
|
134
|
+
modules.push(mod.module);
|
|
1902
135
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
136
|
+
const hub = await loadInstalledHub();
|
|
137
|
+
// One line at boot, never a key (#9571 D5); `artifact-boots:check` asserts
|
|
138
|
+
// the traced artifact reads `installed`.
|
|
139
|
+
logger.info({ hub: hub ? "installed" : "absent" }, "[boot] hub package");
|
|
140
|
+
if (hub === undefined) return buildServer({ modules, topology: "hub", config });
|
|
141
|
+
return buildServer({ modules, topology: "hub", config, hub });
|
|
1907
142
|
}
|