Files
cfw-autumn/server/tests/integration/scopes/scope-403.test.ts
2026-05-08 14:38:52 +08:00

5415 lines
161 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import crypto from "node:crypto";
import { AppEnv, apiKeys } from "@autumn/shared";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { eq } from "drizzle-orm";
import { createHonoApp } from "@/initHono.js";
import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
import { generateId } from "@/utils/genUtils.js";
/*
* scope-403.test.ts
*
* Exhaustive per-route coverage that every scope-gated route actually
* enforces its declared scope requirement. Drives the real Hono app
* (createHonoApp()) with API keys seeded into the test database and
* asserts 403 + ErrCode.InsufficientScopes ("insufficient_scopes") on
* every route whose declared requirement is NOT satisfied by the
* failing key.
*
* Data is INLINED — routes.json and scope-decisions.json are snapshots
* captured at test-generation time. Regenerate the ROUTES const from
* the route scanner output and SCOPE_DECISIONS from the user-authored
* scope-decisions.json if either drifts.
*
* The scope middleware fail-opens on ctx.scopes.length === 0 (legacy
* keys without scopes), so we also have a targeted fail-open block and
* a public-route block that proves the scope gate correctly short-
* circuits for routes declared `public`.
*/
// ---------------------------------------------------------------------------
// Inlined from apps/scope-picker/routes.json (generated by the route scanner).
// Snapshot — regenerate via the scanner if routes change.
// ---------------------------------------------------------------------------
const ROUTES = [
{
handlerName: "handleGetAdminCustomerBlockConfig",
handlerFile: "src/internal/admin/handleGetAdminCustomerBlockConfig.ts",
method: "GET",
path: "/admin/customer-block-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/customer-block-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertAdminCustomerBlockConfig",
handlerFile: "src/internal/admin/handleUpsertAdminCustomerBlockConfig.ts",
method: "PUT",
path: "/admin/customer-block-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/customer-block-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetAdminEdgeConfigSources",
handlerFile: "src/internal/admin/handleGetAdminEdgeConfigSources.ts",
method: "GET",
path: "/admin/edge-config-sources",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/edge-config-sources"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetAdminFeatureFlagsConfig",
handlerFile: "src/internal/admin/handleGetAdminFeatureFlagsConfig.ts",
method: "GET",
path: "/admin/feature-flags-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/feature-flags-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertAdminFeatureFlagsConfig",
handlerFile: "src/internal/admin/handleUpsertAdminFeatureFlagsConfig.ts",
method: "PUT",
path: "/admin/feature-flags-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/feature-flags-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetInvoiceLineItems",
handlerFile: "src/internal/admin/handleGetInvoiceLineItems.ts",
method: "POST",
path: "/admin/invoice-line-items",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/invoice-line-items"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetMasterStripeAccount",
handlerFile: "src/internal/admin/handleGetMasterStripeAccount.ts",
method: "GET",
path: "/admin/master-stripe-account",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/master-stripe-account"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListOAuthClients",
handlerFile: "src/internal/admin/handleListOAuthClients.ts",
method: "GET",
path: "/admin/oauth-clients",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/oauth-clients"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetAdminOrgLimitsConfig",
handlerFile: "src/internal/admin/handleGetAdminOrgLimitsConfig.ts",
method: "GET",
path: "/admin/org-limits-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/org-limits-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertAdminOrgLimitsConfig",
handlerFile: "src/internal/admin/handleUpsertAdminOrgLimitsConfig.ts",
method: "PUT",
path: "/admin/org-limits-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/org-limits-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOrgMember",
handlerFile: "src/internal/admin/handleGetOrgMember.ts",
method: "GET",
path: "/admin/org-member",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/org-member"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListAdminOrgs",
handlerFile: "src/internal/admin/handleListAdminOrgs.ts",
method: "GET",
path: "/admin/orgs",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/orgs"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetAdminOrgRequestBlock",
handlerFile: "src/internal/admin/handleGetAdminOrgRequestBlock.ts",
method: "GET",
path: "/admin/orgs/:org_id/request-block",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/orgs/:org_id/request-block"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertAdminOrgRequestBlock",
handlerFile: "src/internal/admin/handleUpsertAdminOrgRequestBlock.ts",
method: "PUT",
path: "/admin/orgs/:org_id/request-block",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/orgs/:org_id/request-block"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetAdminRedisV2CacheConfig",
handlerFile: "src/internal/admin/handleGetAdminRedisV2CacheConfig.ts",
method: "GET",
path: "/admin/redis-v2-cache-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/redis-v2-cache-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertAdminRedisV2CacheConfig",
handlerFile: "src/internal/admin/handleUpsertAdminRedisV2CacheConfig.ts",
method: "PUT",
path: "/admin/redis-v2-cache-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/redis-v2-cache-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetAdminRequestBlockConfig",
handlerFile: "src/internal/admin/handleGetAdminRequestBlockConfig.ts",
method: "GET",
path: "/admin/request-block-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/request-block-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertAdminRequestBlockConfig",
handlerFile: "src/internal/admin/handleUpsertAdminRequestBlockConfig.ts",
method: "PUT",
path: "/admin/request-block-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/request-block-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetRollouts",
handlerFile: "src/internal/admin/rollouts/handleGetRollouts.ts",
method: "GET",
path: "/admin/rollouts",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/rollouts"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteRollout",
handlerFile: "src/internal/admin/rollouts/handleDeleteRollout.ts",
method: "DELETE",
path: "/admin/rollouts/:rollout_id",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/rollouts/:rollout_id"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateRollout",
handlerFile: "src/internal/admin/rollouts/handleUpdateRollout.ts",
method: "PUT",
path: "/admin/rollouts/:rollout_id",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/rollouts/:rollout_id"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteRolloutOrg",
handlerFile: "src/internal/admin/rollouts/handleDeleteRolloutOrg.ts",
method: "DELETE",
path: "/admin/rollouts/:rollout_id/orgs/:org_id",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/rollouts/:rollout_id/orgs/:org_id"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateRolloutOrg",
handlerFile: "src/internal/admin/rollouts/handleUpdateRolloutOrg.ts",
method: "PUT",
path: "/admin/rollouts/:rollout_id/orgs/:org_id",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/rollouts/:rollout_id/orgs/:org_id"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetAdminStripeSyncConfig",
handlerFile: "src/internal/admin/handleGetAdminStripeSyncConfig.ts",
method: "GET",
path: "/admin/stripe-sync-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/stripe-sync-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertAdminStripeSyncConfig",
handlerFile: "src/internal/admin/handleUpsertAdminStripeSyncConfig.ts",
method: "PUT",
path: "/admin/stripe-sync-config",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/stripe-sync-config"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListAdminUsers",
handlerFile: "src/internal/admin/handleListAdminUsers.ts",
method: "GET",
path: "/admin/users",
style: "REST",
group: "admin",
mountChain: ["", "admin", "/users"],
sourceRouterFile: "src/internal/admin/adminRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetCheckout",
handlerFile: "src/internal/checkouts/handlers/handleGetCheckout.ts",
method: "GET",
path: "/checkouts/:checkout_id",
style: "REST",
group: "checkouts",
mountChain: ["", "/checkouts", "/:checkout_id"],
sourceRouterFile: "src/internal/checkouts/checkoutRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleConfirmCheckout",
handlerFile: "src/internal/checkouts/handlers/handleConfirmCheckout.ts",
method: "POST",
path: "/checkouts/:checkout_id/confirm",
style: "REST",
group: "checkouts",
mountChain: ["", "/checkouts", "/:checkout_id/confirm"],
sourceRouterFile: "src/internal/checkouts/checkoutRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePreviewCheckout",
handlerFile: "src/internal/checkouts/handlers/handlePreviewCheckout.ts",
method: "POST",
path: "/checkouts/:checkout_id/preview",
style: "REST",
group: "checkouts",
mountChain: ["", "/checkouts", "/:checkout_id/preview"],
sourceRouterFile: "src/internal/checkouts/checkoutRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateOAuthApiKeys",
handlerFile: "src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts",
method: "POST",
path: "/cli/api-keys",
style: "REST",
group: "cli",
mountChain: ["/cli", "/api-keys"],
sourceRouterFile: "src/internal/dev/cli/cliRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOrgConsents",
handlerFile: "src/internal/misc/consent/handlers/handleGetOrgConsents.ts",
method: "GET",
path: "/consents",
style: "REST",
group: "consents",
mountChain: ["", "/consents", "/"],
sourceRouterFile: "src/internal/misc/consent/consentRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRevokeConsent",
handlerFile: "src/internal/misc/consent/handlers/handleRevokeConsent.ts",
method: "DELETE",
path: "/consents/:consent_id",
style: "REST",
group: "consents",
mountChain: ["", "/consents", "/:consent_id"],
sourceRouterFile: "src/internal/misc/consent/consentRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetConsentApiKeys",
handlerFile:
"src/internal/misc/consent/handlers/handleGetConsentApiKeys.ts",
method: "GET",
path: "/consents/:consent_id/api-keys",
style: "REST",
group: "consents",
mountChain: ["", "/consents", "/:consent_id/api-keys"],
sourceRouterFile: "src/internal/misc/consent/consentRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetCustomer",
handlerFile: "src/internal/customers/internalHandlers/handleGetCustomer.ts",
method: "GET",
path: "/customers/:customer_id",
style: "REST",
group: "customers",
mountChain: ["", "/customers", "/:customer_id"],
sourceRouterFile: "src/internal/customers/internalCusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetInvoiceLineItems",
handlerFile: "src/internal/admin/handleGetInvoiceLineItems.ts",
method: "POST",
path: "/customers/:customer_id/invoice-line-items",
style: "REST",
group: "customers",
mountChain: ["", "/customers", "/:customer_id/invoice-line-items"],
sourceRouterFile: "src/internal/customers/internalCusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetCustomerProduct",
handlerFile:
"src/internal/customers/internalHandlers/handleGetCustomerProduct.ts",
method: "GET",
path: "/customers/:customer_id/product/:product_id",
style: "REST",
group: "customers",
mountChain: ["", "/customers", "/:customer_id/product/:product_id"],
sourceRouterFile: "src/internal/customers/internalCusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetCusReferrals",
handlerFile:
"src/internal/customers/internalHandlers/handleGetCusReferrals.ts",
method: "GET",
path: "/customers/:customer_id/referrals",
style: "REST",
group: "customers",
mountChain: ["", "/customers", "/:customer_id/referrals"],
sourceRouterFile: "src/internal/customers/internalCusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetCustomerSchedule",
handlerFile:
"src/internal/customers/internalHandlers/handleGetCustomerSchedule.ts",
method: "GET",
path: "/customers/:customer_id/schedule",
style: "REST",
group: "customers",
mountChain: ["", "/customers", "/:customer_id/schedule"],
sourceRouterFile: "src/internal/customers/internalCusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetFullCustomers",
handlerFile:
"src/internal/customers/internalHandlers/handleGetFullCustomers.ts",
method: "POST",
path: "/customers/all/full_customers",
style: "REST",
group: "customers",
mountChain: ["", "/customers", "/all/full_customers"],
sourceRouterFile: "src/internal/customers/internalCusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSearchCustomers",
handlerFile:
"src/internal/customers/internalHandlers/handleSearchCustomers.ts",
method: "POST",
path: "/customers/all/search",
style: "REST",
group: "customers",
mountChain: ["", "/customers", "/all/search"],
sourceRouterFile: "src/internal/customers/internalCusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateSecretKey",
handlerFile: "src/internal/dev/handlers/handleCreateSecretKey.ts",
method: "POST",
path: "/dev/api_key",
style: "REST",
group: "dev",
mountChain: ["", "/dev", "/api_key"],
sourceRouterFile: "src/internal/dev/devRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteSecretKey",
handlerFile: "src/internal/dev/handlers/handleDeleteSecretKey.ts",
method: "DELETE",
path: "/dev/api_key/:key_id",
style: "REST",
group: "dev",
mountChain: ["", "/dev", "/api_key/:key_id"],
sourceRouterFile: "src/internal/dev/devRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCliStripe",
handlerFile: "src/internal/dev/handlers/handleCliStripe.ts",
method: "POST",
path: "/dev/cli/stripe",
style: "REST",
group: "dev",
mountChain: ["", "/dev", "/cli/stripe"],
sourceRouterFile: "src/internal/dev/devRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetDevData",
handlerFile: "src/internal/dev/handlers/handleGetDevData.ts",
method: "GET",
path: "/dev/data",
style: "REST",
group: "dev",
mountChain: ["", "/dev", "/data"],
sourceRouterFile: "src/internal/dev/devRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateOtp",
handlerFile: "src/internal/dev/handlers/handleCreateOtp.ts",
method: "POST",
path: "/dev/otp",
style: "REST",
group: "dev",
mountChain: ["", "/dev", "/otp"],
sourceRouterFile: "src/internal/dev/devRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOtp",
handlerFile: "src/internal/dev/handlers/handleGetOtp.ts",
method: "GET",
path: "/dev/otp/:otp",
style: "REST",
group: "dev",
mountChain: ["", "/dev", "/otp/:otp"],
sourceRouterFile: "src/internal/dev/devRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSubmitFeedback",
handlerFile: "src/internal/misc/feedback/handleSubmitFeedback.ts",
method: "POST",
path: "/feedback",
style: "REST",
group: "feedback",
mountChain: ["", "/feedback", ""],
sourceRouterFile: "src/internal/misc/feedback/feedbackRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRedirectToInvoice",
handlerFile: "src/internal/invoices/handlers/handleRedirectToInvoice.ts",
method: "GET",
path: "/invoices/hosted_invoice_url/:invoiceId",
style: "REST",
group: "invoices",
mountChain: ["", "/invoices", "/hosted_invoice_url/:invoiceId"],
sourceRouterFile: "src/internal/invoices/invoiceRouter.ts",
routeKind: "createRoute",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteOrg",
handlerFile: "src/internal/orgs/handlers/crudHandlers/handleDeleteOrg.ts",
method: "DELETE",
path: "/organization",
style: "REST",
group: "organization",
mountChain: ["", "organization", ""],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOrg",
handlerFile: "src/internal/orgs/handlers/crudHandlers/handleGetOrg.ts",
method: "GET",
path: "/organization",
style: "REST",
group: "organization",
mountChain: ["", "organization", ""],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetInvites",
handlerFile:
"src/internal/orgs/handlers/memberHandlers/handleGetInvites.ts",
method: "GET",
path: "/organization/invites",
style: "REST",
group: "organization",
mountChain: ["", "organization", "/invites"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOrgMembers",
handlerFile:
"src/internal/orgs/handlers/memberHandlers/handleGetOrgMembers.ts",
method: "GET",
path: "/organization/members",
style: "REST",
group: "organization",
mountChain: ["", "organization", "/members"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRemoveMember",
handlerFile:
"src/internal/orgs/handlers/memberHandlers/handleRemoveMember.ts",
method: "POST",
path: "/organization/remove-member",
style: "REST",
group: "organization",
mountChain: ["", "organization", "/remove-member"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetUploadUrl",
handlerFile: "src/internal/orgs/handlers/handleGetUploadUrl.ts",
method: "GET",
path: "/organization/upload_url",
style: "REST",
group: "organization",
mountChain: ["", "organization", "/upload_url"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "POST",
path: "/pricing-agent/chat",
style: "REST",
group: "pricing-agent",
mountChain: ["", "/pricing-agent", "/chat"],
sourceRouterFile: "src/internal/misc/pricingAgent/pricingAgentRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "handleSetupPreviewOrg",
handlerFile:
"src/internal/misc/pricingAgent/handlers/handleSetupPreviewOrg.ts",
method: "POST",
path: "/pricing-agent/preview/setup",
style: "REST",
group: "pricing-agent",
mountChain: ["", "/pricing-agent", "/preview/setup"],
sourceRouterFile: "src/internal/misc/pricingAgent/pricingAgentRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSyncPreviewPricing",
handlerFile:
"src/internal/misc/pricingAgent/handlers/handleSyncPreviewPricing.ts",
method: "POST",
path: "/pricing-agent/preview/sync",
style: "REST",
group: "pricing-agent",
mountChain: ["", "/pricing-agent", "/preview/sync"],
sourceRouterFile: "src/internal/misc/pricingAgent/pricingAgentRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetProductCount",
handlerFile:
"src/internal/products/internalHandlers/handleGetProductCount.ts",
method: "GET",
path: "/products/:productId/count",
style: "REST",
group: "products",
mountChain: ["", "/products", "/:productId/count"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetProductInternal",
handlerFile:
"src/internal/products/internalHandlers/handleGetProductInternal.ts",
method: "GET",
path: "/products/:productId/data",
style: "REST",
group: "products",
mountChain: ["", "/products", "/:productId/data"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetProductDeleteInfo",
handlerFile: "src/internal/products/handlers/handleGetProductDeleteInfo.ts",
method: "GET",
path: "/products/:productId/info",
style: "REST",
group: "products",
mountChain: ["", "/products", "/:productId/info"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCopyEnvironment",
handlerFile:
"src/internal/products/handlers/handleCopyEnvironment/handleCopyEnvironment.ts",
method: "POST",
path: "/products/copy_to_production",
style: "REST",
group: "products",
mountChain: ["", "/products", "/copy_to_production"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetFeatures",
handlerFile: "src/internal/products/internalHandlers/handleGetFeatures.ts",
method: "GET",
path: "/products/features",
style: "REST",
group: "products",
mountChain: ["", "/products", "/features"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handler",
handlerFile: null,
method: "GET",
path: "/products/has_entity_feature_id",
style: "REST",
group: "products",
mountChain: ["", "/products", "/has_entity_feature_id"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "handleGetMigrations",
handlerFile:
"src/internal/products/internalHandlers/handleGetMigrations.ts",
method: "GET",
path: "/products/migrations",
style: "REST",
group: "products",
mountChain: ["", "/products", "/migrations"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetProductCounts",
handlerFile:
"src/internal/products/internalHandlers/handleGetProductCounts.ts",
method: "GET",
path: "/products/product_counts",
style: "REST",
group: "products",
mountChain: ["", "/products", "/product_counts"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetProducts",
handlerFile: "src/internal/products/internalHandlers/handleGetProducts.ts",
method: "GET",
path: "/products/products",
style: "REST",
group: "products",
mountChain: ["", "/products", "/products"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetRewards",
handlerFile: "src/internal/products/internalHandlers/handleGetRewards.ts",
method: "GET",
path: "/products/rewards",
style: "REST",
group: "products",
mountChain: ["", "/products", "/rewards"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetStripeCoupons",
handlerFile:
"src/internal/products/internalHandlers/handleGetStripeCoupons.ts",
method: "GET",
path: "/products/stripe_coupons",
style: "REST",
group: "products",
mountChain: ["", "/products", "/stripe_coupons"],
sourceRouterFile: "src/internal/products/internalProductRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetEventNames",
handlerFile:
"src/internal/analytics/internalHandlers/handleGetEventNames.ts",
method: "GET",
path: "/query/event_names",
style: "REST",
group: "query",
mountChain: ["", "/query", "/event_names"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListEventNames",
handlerFile:
"src/internal/analytics/internalHandlers/handleListEventNames.ts",
method: "GET",
path: "/query/event_names/list",
style: "REST",
group: "query",
mountChain: ["", "/query", "/event_names/list"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleInternalAggregateEvents",
handlerFile:
"src/internal/analytics/internalHandlers/handleInternalAggregateEvents.ts",
method: "POST",
path: "/query/events",
style: "REST",
group: "query",
mountChain: ["", "/query", "/events"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleInternalListRawEvents",
handlerFile:
"src/internal/analytics/internalHandlers/handleInternalListRawEvents.ts",
method: "POST",
path: "/query/raw",
style: "REST",
group: "query",
mountChain: ["", "/query", "/raw"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleArpc",
handlerFile:
"src/internal/analytics/internalHandlers/handleRevenueAnalytics.ts",
method: "POST",
path: "/query/revenue/arpc",
style: "REST",
group: "query",
mountChain: ["", "/query", "/revenue/arpc"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRevenueByProduct",
handlerFile:
"src/internal/analytics/internalHandlers/handleRevenueAnalytics.ts",
method: "POST",
path: "/query/revenue/by-product",
style: "REST",
group: "query",
mountChain: ["", "/query", "/revenue/by-product"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCustomerLeaderboard",
handlerFile:
"src/internal/analytics/internalHandlers/handleRevenueAnalytics.ts",
method: "POST",
path: "/query/revenue/customer-leaderboard",
style: "REST",
group: "query",
mountChain: ["", "/query", "/revenue/customer-leaderboard"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleEstimatedMrr",
handlerFile:
"src/internal/analytics/internalHandlers/handleRevenueAnalytics.ts",
method: "POST",
path: "/query/revenue/estimated-mrr",
style: "REST",
group: "query",
mountChain: ["", "/query", "/revenue/estimated-mrr"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleInvoiceStatus",
handlerFile:
"src/internal/analytics/internalHandlers/handleRevenueAnalytics.ts",
method: "POST",
path: "/query/revenue/invoice-status",
style: "REST",
group: "query",
mountChain: ["", "/query", "/revenue/invoice-status"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRevenueProductShare",
handlerFile:
"src/internal/analytics/internalHandlers/handleRevenueAnalytics.ts",
method: "POST",
path: "/query/revenue/product-share",
style: "REST",
group: "query",
mountChain: ["", "/query", "/revenue/product-share"],
sourceRouterFile: "src/internal/analytics/internalAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetViews",
handlerFile: "src/internal/misc/savedViews/handlers/handleGetViews.ts",
method: "GET",
path: "/saved_views",
style: "REST",
group: "saved_views",
mountChain: ["", "/saved_views", ""],
sourceRouterFile: "src/internal/misc/savedViews/savedViewsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteView",
handlerFile: "src/internal/misc/savedViews/handlers/handleDeleteView.ts",
method: "DELETE",
path: "/saved_views/:viewId",
style: "REST",
group: "saved_views",
mountChain: ["", "/saved_views", "/:viewId"],
sourceRouterFile: "src/internal/misc/savedViews/savedViewsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSaveView",
handlerFile: "src/internal/misc/savedViews/handlers/handleSaveView.ts",
method: "POST",
path: "/saved_views/save",
style: "REST",
group: "saved_views",
mountChain: ["", "/saved_views", "/save"],
sourceRouterFile: "src/internal/misc/savedViews/savedViewsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetTrmnlDeviceId",
handlerFile: "src/internal/misc/trmnl/handlers/handleGetTrmnlDeviceId.ts",
method: "GET",
path: "/trmnl/device_id",
style: "REST",
group: "trmnl",
mountChain: ["", "/trmnl", "/device_id"],
sourceRouterFile: "src/internal/misc/trmnl/trmnlRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePostTrmnlDeviceId",
handlerFile: "src/internal/misc/trmnl/handlers/handlePostTrmnlDeviceId.ts",
method: "POST",
path: "/trmnl/device_id",
style: "REST",
group: "trmnl",
mountChain: ["", "/trmnl", "/device_id"],
sourceRouterFile: "src/internal/misc/trmnl/trmnlRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGenerateTrmnlScreen",
handlerFile:
"src/internal/misc/trmnl/handlers/handleGenerateTrmnlScreen.ts",
method: "POST",
path: "/trmnl/screen",
style: "REST",
group: "trmnl",
mountChain: ["", "/trmnl", "/screen"],
sourceRouterFile: "src/internal/misc/trmnl/trmnlRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePostEntityRequest",
handlerFile: null,
method: "POST",
path: "/v1",
style: "REST",
group: "v1",
mountChain: ["/v1", "", ""],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
// Removed: `DELETE /v1/:entity_id → handleDeleteEntity` is a phantom
// route — the scanner picked up a commented-out `.delete(...)` line in
// `entityRouter.ts`. The route is not actually registered at runtime, so
// a test against it would 401/404, not 403. Re-add only if the route is
// uncommented in the router.
{
handlerName: "handleAttach",
handlerFile: "src/internal/billing/attach/handleAttach.ts",
method: "POST",
path: "/v1/attach",
style: "REST",
group: "v1/attach",
mountChain: ["/v1", "", "/attach"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleAttachPreview",
handlerFile:
"src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts",
method: "POST",
path: "/v1/attach/preview",
style: "REST",
group: "v1/attach",
mountChain: ["/v1", "", "/attach/preview"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCheck",
handlerFile: "src/internal/api/check/handleCheck.ts",
method: "POST",
path: "/v1/balances.check",
style: "RPC",
group: "v1/balances",
mountChain: ["/v1", "", "", "/balances.check"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateBalance",
handlerFile: "src/internal/balances/handlers/handleCreateBalance.ts",
method: "POST",
path: "/v1/balances.create",
style: "RPC",
group: "v1/balances",
mountChain: ["/v1", "", "", "/balances.create"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteBalance",
handlerFile: "src/internal/balances/handlers/handleDeleteBalance.ts",
method: "POST",
path: "/v1/balances.delete",
style: "RPC",
group: "v1/balances",
mountChain: ["/v1", "", "", "/balances.delete"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleFinalizeLock",
handlerFile: "src/internal/balances/handlers/handleFinalizeLock.ts",
method: "POST",
path: "/v1/balances.finalize",
style: "RPC",
group: "v1/balances",
mountChain: ["/v1", "", "", "/balances.finalize"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleTrack",
handlerFile: "src/internal/balances/handlers/handleTrack.ts",
method: "POST",
path: "/v1/balances.track",
style: "RPC",
group: "v1/balances",
mountChain: ["/v1", "", "", "/balances.track"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateBalance",
handlerFile: "src/internal/balances/handlers/handleUpdateBalance.ts",
method: "POST",
path: "/v1/balances.update",
style: "RPC",
group: "v1/balances",
mountChain: ["/v1", "", "", "/balances.update"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateBalance",
handlerFile: "src/internal/balances/handlers/handleCreateBalance.ts",
method: "POST",
path: "/v1/balances/create",
style: "REST",
group: "v1/balances",
mountChain: ["/v1", "", "/balances/create"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListBalances",
handlerFile: "src/internal/balances/handlers/handleListBalances.ts",
method: "GET",
path: "/v1/balances/list",
style: "REST",
group: "v1/balances",
mountChain: ["/v1", "", "/balances/list"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateBalance",
handlerFile: "src/internal/balances/handlers/handleUpdateBalance.ts",
method: "POST",
path: "/v1/balances/update",
style: "REST",
group: "v1/balances",
mountChain: ["/v1", "", "/balances/update"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleAttachV2",
handlerFile: "src/internal/billing/v2/handlers/handleAttachV2.ts",
method: "POST",
path: "/v1/billing.attach",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.attach"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateSchedule",
handlerFile: "src/internal/billing/v2/handlers/handleCreateSchedule.ts",
method: "POST",
path: "/v1/billing.create_schedule",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.create_schedule"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleMultiAttach",
handlerFile: "src/internal/billing/v2/handlers/handleMultiAttach.ts",
method: "POST",
path: "/v1/billing.multi_attach",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.multi_attach"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleOpenCustomerPortalV2",
handlerFile:
"src/internal/customers/handlers/handleBillingPortal/handleOpenCustomerPortalV2.ts",
method: "POST",
path: "/v1/billing.open_customer_portal",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.open_customer_portal"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePreviewAttach",
handlerFile: "src/internal/billing/v2/handlers/handlePreviewAttach.ts",
method: "POST",
path: "/v1/billing.preview_attach",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.preview_attach"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePreviewCreateSchedule",
handlerFile:
"src/internal/billing/v2/handlers/handlePreviewCreateSchedule.ts",
method: "POST",
path: "/v1/billing.preview_create_schedule",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.preview_create_schedule"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePreviewMultiAttach",
handlerFile: "src/internal/billing/v2/handlers/handlePreviewMultiAttach.ts",
method: "POST",
path: "/v1/billing.preview_multi_attach",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.preview_multi_attach"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePreviewUpdateSubscription",
handlerFile:
"src/internal/billing/v2/handlers/handlePreviewUpdateSubscription.ts",
method: "POST",
path: "/v1/billing.preview_update",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.preview_update"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSetupPaymentV2",
handlerFile: "src/internal/billing/v2/handlers/handleSetupPaymentV2.ts",
method: "POST",
path: "/v1/billing.setup_payment",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.setup_payment"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSync",
handlerFile: "src/internal/billing/v2/handlers/handleSync.ts",
method: "POST",
path: "/v1/billing.sync",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.sync"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSyncProposals",
handlerFile: "src/internal/billing/v2/handlers/handleSyncProposals.ts",
method: "POST",
path: "/v1/billing.sync_proposals",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.sync_proposals"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateSubscription",
handlerFile: "src/internal/billing/v2/handlers/handleUpdateSubscription.ts",
method: "POST",
path: "/v1/billing.update",
style: "RPC",
group: "v1/billing",
mountChain: ["/v1", "", "", "/billing.update"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCancelV2",
handlerFile: "src/internal/customers/cancel/handleCancelV2.ts",
method: "POST",
path: "/v1/cancel",
style: "REST",
group: "v1/cancel",
mountChain: ["/v1", "", "/cancel"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCheck",
handlerFile: "src/internal/api/check/handleCheck.ts",
method: "POST",
path: "/v1/check",
style: "REST",
group: "v1/check",
mountChain: ["/v1", "", "/check"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleLegacyApiCheckout",
handlerFile: "src/internal/billing/checkout/handleLegacyApiCheckout.ts",
method: "POST",
path: "/v1/checkout",
style: "REST",
group: "v1/checkout",
mountChain: ["/v1", "", "/checkout"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPricingTable",
handlerFile:
"src/internal/misc/components/handlers/handleGetPricingTable.ts",
method: "GET",
path: "/v1/components/pricing_table",
style: "REST",
group: "v1/components",
mountChain: ["/v1", "/components", "/pricing_table"],
sourceRouterFile: "src/internal/misc/components/componentsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleNukeOrganisationConfiguration",
handlerFile:
"src/internal/misc/configs/handlers/handleNukeOrganisationConfiguration.ts",
method: "DELETE",
path: "/v1/configs/nuke",
style: "REST",
group: "v1/configs",
mountChain: ["/v1", "/configs", "/nuke"],
sourceRouterFile: "src/internal/misc/configs/configsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePushOrganisationConfiguration",
handlerFile:
"src/internal/misc/configs/handlers/handlePushOrganisationConfiguration.ts",
method: "POST",
path: "/v1/configs/push",
style: "REST",
group: "v1/configs",
mountChain: ["/v1", "/configs", "/push"],
sourceRouterFile: "src/internal/misc/configs/configsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListCustomers",
handlerFile: "src/internal/customers/handlers/handleListCustomers.ts",
method: "GET",
path: "/v1/customers",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", ""],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePostCustomer",
handlerFile:
"src/internal/customers/handlers/handleGetOrCreateCustomer/handleGetOrCreateCustomer.ts",
method: "POST",
path: "/v1/customers",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", ""],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteCustomerV2",
handlerFile: "src/internal/customers/handlers/handleDeleteCustomer.ts",
method: "POST",
path: "/v1/customers.delete",
style: "RPC",
group: "v1/customers",
mountChain: ["/v1", "", "", "/customers.delete"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOrCreateCustomerV2",
handlerFile:
"src/internal/customers/handlers/handleGetOrCreateCustomer/handleGetOrCreateCustomerV2.ts",
method: "POST",
path: "/v1/customers.get_or_create",
style: "RPC",
group: "v1/customers",
mountChain: ["/v1", "", "", "/customers.get_or_create"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListCustomersV2",
handlerFile: "src/internal/customers/handlers/handleListCustomersV2.ts",
method: "POST",
path: "/v1/customers.list",
style: "RPC",
group: "v1/customers",
mountChain: ["/v1", "", "", "/customers.list"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateCustomerV2",
handlerFile:
"src/internal/customers/handlers/handleUpdateCustomer/handleUpdateCustomerV2.ts",
method: "POST",
path: "/v1/customers.update",
style: "RPC",
group: "v1/customers",
mountChain: ["/v1", "", "", "/customers.update"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteCustomer",
handlerFile: "src/internal/customers/handlers/handleDeleteCustomer.ts",
method: "DELETE",
path: "/v1/customers/:customer_id",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetCustomerV2",
handlerFile: "src/internal/customers/handlers/handleGetCustomerV2.ts",
method: "GET",
path: "/v1/customers/:customer_id",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateCustomer",
handlerFile:
"src/internal/customers/handlers/handleUpdateCustomer/handleUpdateCustomer.ts",
method: "PATCH",
path: "/v1/customers/:customer_id",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateCustomer",
handlerFile:
"src/internal/customers/handlers/handleUpdateCustomer/handleUpdateCustomer.ts",
method: "POST",
path: "/v1/customers/:customer_id",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateBalancesV2",
handlerFile: "src/internal/customers/handlers/handleUpdateBalancesV2.ts",
method: "POST",
path: "/v1/customers/:customer_id/balances",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id/balances"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetBillingPortal",
handlerFile:
"src/internal/customers/handlers/handleBillingPortal/handleGetBillingPortal.ts",
method: "GET",
path: "/v1/customers/:customer_id/billing_portal",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id/billing_portal"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateBillingPortal",
handlerFile:
"src/internal/customers/handlers/handleBillingPortal/handleCreateBillingPortal.ts",
method: "POST",
path: "/v1/customers/:customer_id/billing_portal",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id/billing_portal"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleAddCouponToCusV2",
handlerFile: "src/internal/customers/handlers/handleAddCouponToCusV2.ts",
method: "POST",
path: "/v1/customers/:customer_id/coupons/:coupon_id",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id/coupons/:coupon_id"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListEntities",
handlerFile: "src/internal/entities/handlers/handleListEntities.ts",
method: "GET",
path: "/v1/customers/:customer_id/entities",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "", "/customers/:customer_id/entities"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateEntity",
handlerFile:
"src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts",
method: "POST",
path: "/v1/customers/:customer_id/entities",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "", "/customers/:customer_id/entities"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteEntity",
handlerFile:
"src/internal/entities/handlers/handleDeleteEntity/handleDeleteEntity.ts",
method: "DELETE",
path: "/v1/customers/:customer_id/entities/:entity_id",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "", "/customers/:customer_id/entities/:entity_id"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetEntity",
handlerFile:
"src/internal/entities/handlers/handleGetEntity/handleGetEntity.ts",
method: "GET",
path: "/v1/customers/:customer_id/entities/:entity_id",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "", "/customers/:customer_id/entities/:entity_id"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRefundInvoice",
handlerFile:
"src/internal/customers/handlers/handleRefundInvoice/handleRefundInvoice.ts",
method: "POST",
path: "/v1/customers/:customer_id/invoices/:stripe_invoice_id/refund",
style: "REST",
group: "v1/customers",
mountChain: [
"/v1",
"/customers",
"/:customer_id/invoices/:stripe_invoice_id/refund",
],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleTransferProductV2",
handlerFile: "src/internal/customers/handlers/handleTransferProductV2.ts",
method: "POST",
path: "/v1/customers/:customer_id/transfer",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/:customer_id/transfer"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleClearCustomerCache",
handlerFile: "src/internal/customers/handlers/handleClearCustomerCache.ts",
method: "POST",
path: "/v1/customers/clear_cache",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "/clear_cache"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListCustomersV2",
handlerFile: "src/internal/customers/handlers/handleListCustomersV2.ts",
method: "POST",
path: "/v1/customers/list",
style: "REST",
group: "v1/customers",
mountChain: ["/v1", "/customers", "list"],
sourceRouterFile: "src/internal/customers/cusRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "GET",
path: "/v1/debug/heap-snapshot",
style: "REST",
group: "v1/debug",
mountChain: ["/v1/debug", "/heap-snapshot"],
sourceRouterFile: "src/internal/misc/debug/debugRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "GET",
path: "/v1/debug/memory",
style: "REST",
group: "v1/debug",
mountChain: ["/v1/debug", "/memory"],
sourceRouterFile: "src/internal/misc/debug/debugRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "POST",
path: "/v1/debug/pg-health",
style: "REST",
group: "v1/debug",
mountChain: ["/v1/debug", "/pg-health"],
sourceRouterFile: "src/internal/misc/debug/debugRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "POST",
path: "/v1/debug/pool-test",
style: "REST",
group: "v1/debug",
mountChain: ["/v1/debug", "/pool-test"],
sourceRouterFile: "src/internal/misc/debug/debugRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "POST",
path: "/v1/debug/redis-failover",
style: "REST",
group: "v1/debug",
mountChain: ["/v1/debug", "/redis-failover"],
sourceRouterFile: "src/internal/misc/debug/debugRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "GET",
path: "/v1/debug/statement-timeout",
style: "REST",
group: "v1/debug",
mountChain: ["/v1/debug", "/statement-timeout"],
sourceRouterFile: "src/internal/misc/debug/debugRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "handleCreateEntityV2",
handlerFile:
"src/internal/entities/handlers/handleCreateEntity/handleCreateEntityV2.ts",
method: "POST",
path: "/v1/entities.create",
style: "RPC",
group: "v1/entities",
mountChain: ["/v1", "", "", "/entities.create"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteEntityV2",
handlerFile:
"src/internal/entities/handlers/handleDeleteEntity/handleDeleteEntityV2.ts",
method: "POST",
path: "/v1/entities.delete",
style: "RPC",
group: "v1/entities",
mountChain: ["/v1", "", "", "/entities.delete"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetEntityV2",
handlerFile:
"src/internal/entities/handlers/handleGetEntity/handleGetEntityV2.ts",
method: "POST",
path: "/v1/entities.get",
style: "RPC",
group: "v1/entities",
mountChain: ["/v1", "", "", "/entities.get"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListEntitiesV2",
handlerFile: "src/internal/entities/handlers/handleListEntitiesV2.ts",
method: "POST",
path: "/v1/entities.list",
style: "RPC",
group: "v1/entities",
mountChain: ["/v1", "", "", "/entities.list"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateEntity",
handlerFile:
"src/internal/entities/handlers/handleUpdateEntity/handleUpdateEntity.ts",
method: "POST",
path: "/v1/entities.update",
style: "RPC",
group: "v1/entities",
mountChain: ["/v1", "", "", "/entities.update"],
sourceRouterFile: "src/internal/entities/entityRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCheck",
handlerFile: "src/internal/api/check/handleCheck.ts",
method: "POST",
path: "/v1/entitled",
style: "REST",
group: "v1/entitled",
mountChain: ["/v1", "", "/entitled"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleTrack",
handlerFile: "src/internal/balances/handlers/handleTrack.ts",
method: "POST",
path: "/v1/events",
style: "REST",
group: "v1/events",
mountChain: ["/v1", "", "/events"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleExternalAggregateEvents",
handlerFile:
"src/internal/events/handlers/handleExternalAggregateEvents.ts",
method: "POST",
path: "/v1/events.aggregate",
style: "RPC",
group: "v1/events",
mountChain: ["/v1", "", "", "events.aggregate"],
sourceRouterFile: "src/internal/events/eventsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleExternalListEvents",
handlerFile: "src/internal/events/handlers/handleExternalListEvents.ts",
method: "POST",
path: "/v1/events.list",
style: "RPC",
group: "v1/events",
mountChain: ["/v1", "", "", "events.list"],
sourceRouterFile: "src/internal/events/eventsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleExternalAggregateEvents",
handlerFile:
"src/internal/events/handlers/handleExternalAggregateEvents.ts",
method: "POST",
path: "/v1/events/aggregate",
style: "REST",
group: "v1/events",
mountChain: ["/v1", "/events", "aggregate"],
sourceRouterFile: "src/internal/events/eventsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleExternalListEvents",
handlerFile: "src/internal/events/handlers/handleExternalListEvents.ts",
method: "POST",
path: "/v1/events/list",
style: "REST",
group: "v1/events",
mountChain: ["/v1", "/events", "list"],
sourceRouterFile: "src/internal/events/eventsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListFeaturesV1",
handlerFile:
"src/internal/features/handlers/handleListFeatures/handleListFeaturesV1.ts",
method: "GET",
path: "/v1/features",
style: "REST",
group: "v1/features",
mountChain: ["/v1", "/features", ""],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateFeatureV1",
handlerFile:
"src/internal/features/handlers/handleCreateFeature/handleCreateFeatureV1.ts",
method: "POST",
path: "/v1/features",
style: "REST",
group: "v1/features",
mountChain: ["/v1", "/features", ""],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateFeatureV2",
handlerFile:
"src/internal/features/handlers/handleCreateFeature/handleCreateFeatureV2.ts",
method: "POST",
path: "/v1/features.create",
style: "RPC",
group: "v1/features",
mountChain: ["/v1", "", "", "/features.create"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteFeatureV2",
handlerFile:
"src/internal/features/handlers/handleDeleteFeature/handleDeleteFeatureV2.ts",
method: "POST",
path: "/v1/features.delete",
style: "RPC",
group: "v1/features",
mountChain: ["/v1", "", "", "/features.delete"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetFeatureV2",
handlerFile:
"src/internal/features/handlers/handleGetFeature/handleGetFeatureV2.ts",
method: "POST",
path: "/v1/features.get",
style: "RPC",
group: "v1/features",
mountChain: ["/v1", "", "", "/features.get"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListFeaturesV1",
handlerFile:
"src/internal/features/handlers/handleListFeatures/handleListFeaturesV1.ts",
method: "POST",
path: "/v1/features.list",
style: "RPC",
group: "v1/features",
mountChain: ["/v1", "", "", "/features.list"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateFeatureV2",
handlerFile:
"src/internal/features/handlers/handleUpdateFeature/handleUpdateFeatureV2.ts",
method: "POST",
path: "/v1/features.update",
style: "RPC",
group: "v1/features",
mountChain: ["/v1", "", "", "/features.update"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteFeatureV1",
handlerFile:
"src/internal/features/handlers/handleDeleteFeature/handleDeleteFeatureV1.ts",
method: "DELETE",
path: "/v1/features/:feature_id",
style: "REST",
group: "v1/features",
mountChain: ["/v1", "/features", "/:feature_id"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetFeatureV1",
handlerFile:
"src/internal/features/handlers/handleGetFeature/handleGetFeatureV1.ts",
method: "GET",
path: "/v1/features/:feature_id",
style: "REST",
group: "v1/features",
mountChain: ["/v1", "/features", "/:feature_id"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateFeatureV1",
handlerFile:
"src/internal/features/handlers/handleUpdateFeature/handleUpdateFeatureV1.ts",
method: "POST",
path: "/v1/features/:feature_id",
style: "REST",
group: "v1/features",
mountChain: ["/v1", "/features", "/:feature_id"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetFeatureDeletionInfo",
handlerFile:
"src/internal/features/internalHandlers/handleGetFeatureDeletionInfo.ts",
method: "GET",
path: "/v1/features/:feature_id/deletion_info",
style: "REST",
group: "v1/features",
mountChain: ["/v1", "/features", "/:feature_id/deletion_info"],
sourceRouterFile: "src/internal/features/featureRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetStripeInvoice",
handlerFile: "src/internal/invoices/handlers/handleGetStripeInvoice.ts",
method: "GET",
path: "/v1/invoices/:stripe_invoice_id/stripe",
style: "REST",
group: "v1/invoices",
mountChain: ["/v1", "/invoices", "/:stripe_invoice_id/stripe"],
sourceRouterFile: "src/internal/invoices/invoiceRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleMigrateProductV2",
handlerFile: "src/internal/products/handlers/handleMigrateProductV2.ts",
method: "POST",
path: "/v1/migrations",
style: "REST",
group: "v1/migrations",
mountChain: ["/v1", "", "/migrations"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOrg",
handlerFile: "src/internal/orgs/handlers/crudHandlers/handleGetOrg.ts",
method: "GET",
path: "/v1/organization",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", ""],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateOrg",
handlerFile: "src/internal/orgs/handlers/handleUpdateOrg.ts",
method: "PATCH",
path: "/v1/organization",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", ""],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOrgFlags",
handlerFile: "src/internal/orgs/handlers/handleGetOrgFlags.ts",
method: "GET",
path: "/v1/organization/flags",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/flags"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "GET",
path: "/v1/organization/me",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/me"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: false,
},
{
handlerName: "handleResetDefaultAccount",
handlerFile: "src/internal/orgs/handlers/handleResetDefaultAccount.ts",
method: "POST",
path: "/v1/organization/reset_default_account",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/reset_default_account"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetRevenueCatConfig",
handlerFile: "src/internal/orgs/handlers/handleRevenueCatConfig.ts",
method: "GET",
path: "/v1/organization/revenuecat",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/revenuecat"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertRevenueCatConfig",
handlerFile: "src/internal/orgs/handlers/handleRevenueCatConfig.ts",
method: "PATCH",
path: "/v1/organization/revenuecat",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/revenuecat"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetRCMappings",
handlerFile:
"src/external/revenueCat/handlers/handleGetRevenuecatMappings.ts",
method: "GET",
path: "/v1/organization/revenuecat/mappings",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/revenuecat/mappings"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSaveRCMappings",
handlerFile:
"src/external/revenueCat/handlers/handleSaveRevenuecatMappings.ts",
method: "POST",
path: "/v1/organization/revenuecat/mappings",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/revenuecat/mappings"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetRevenueCatProducts",
handlerFile:
"src/external/revenueCat/handlers/handleGetRevenuecatProducts.ts",
method: "POST",
path: "/v1/organization/revenuecat/products",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/revenuecat/products"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteStripe",
handlerFile:
"src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts",
method: "DELETE",
path: "/v1/organization/stripe",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/stripe"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetStripeAccount",
handlerFile:
"src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts",
method: "GET",
path: "/v1/organization/stripe",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/stripe"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleConnectStripe",
handlerFile:
"src/internal/orgs/handlers/stripeHandlers/handleConnectStripe.ts",
method: "POST",
path: "/v1/organization/stripe",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/stripe"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetOAuthUrl",
handlerFile:
"src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts",
method: "GET",
path: "/v1/organization/stripe/oauth_url",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/stripe/oauth_url"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpsertVercelConfig",
handlerFile: "src/internal/orgs/handlers/handleVercelConfig.ts",
method: "PATCH",
path: "/v1/organization/vercel",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/vercel"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetVercelSink",
handlerFile: "src/internal/orgs/handlers/handleVercelConfig.ts",
method: "GET",
path: "/v1/organization/vercel_sink",
style: "REST",
group: "v1/organization",
mountChain: ["/v1", "/organization", "/vercel_sink"],
sourceRouterFile: "src/internal/orgs/orgRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListPlans",
handlerFile: "src/internal/products/handlers/handleListPlans.ts",
method: "GET",
path: "/v1/plans",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", ""],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreatePlan",
handlerFile:
"src/internal/products/handlers/handleCreateProduct/handleCreatePlan.ts",
method: "POST",
path: "/v1/plans",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", ""],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreatePlanV2",
handlerFile:
"src/internal/products/handlers/handleCreateProduct/handleCreatePlanV2.ts",
method: "POST",
path: "/v1/plans.create",
style: "RPC",
group: "v1/plans",
mountChain: ["/v1", "", "", "/plans.create"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeletePlanV2",
handlerFile:
"src/internal/products/handlers/handleDeletePlan/handleDeletePlanV2.ts",
method: "POST",
path: "/v1/plans.delete",
style: "RPC",
group: "v1/plans",
mountChain: ["/v1", "", "", "/plans.delete"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPlanV2",
handlerFile:
"src/internal/products/handlers/handleGetPlan/handleGetPlanV2.ts",
method: "POST",
path: "/v1/plans.get",
style: "RPC",
group: "v1/plans",
mountChain: ["/v1", "", "", "/plans.get"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListPlansV2",
handlerFile:
"src/internal/products/handlers/handleListPlans/handleListPlansV2.ts",
method: "POST",
path: "/v1/plans.list",
style: "RPC",
group: "v1/plans",
mountChain: ["/v1", "", "", "/plans.list"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdatePlanV2",
handlerFile:
"src/internal/products/handlers/handleUpdatePlan/handleUpdatePlanV2.ts",
method: "POST",
path: "/v1/plans.update",
style: "RPC",
group: "v1/plans",
mountChain: ["/v1", "", "", "/plans.update"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeletePlanV1",
handlerFile:
"src/internal/products/handlers/handleDeletePlan/handleDeletePlanV1.ts",
method: "DELETE",
path: "/v1/plans/:product_id",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPlanV1",
handlerFile:
"src/internal/products/handlers/handleGetPlan/handleGetPlanV1.ts",
method: "GET",
path: "/v1/plans/:product_id",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdatePlanV1",
handlerFile:
"src/internal/products/handlers/handleUpdatePlan/handleUpdatePlanV1.ts",
method: "PATCH",
path: "/v1/plans/:product_id",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdatePlanV1",
handlerFile:
"src/internal/products/handlers/handleUpdatePlan/handleUpdatePlanV1.ts",
method: "POST",
path: "/v1/plans/:product_id",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCopyProductV2",
handlerFile:
"src/internal/products/handlers/handleCopyProduct/handleCopyProductV2.ts",
method: "POST",
path: "/v1/plans/:product_id/copy",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id/copy"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPlanDeleteInfo",
handlerFile: "src/internal/products/handlers/handleGetPlanDeleteInfo.ts",
method: "GET",
path: "/v1/plans/:product_id/deletion_info",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id/deletion_info"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePlanHasCustomersV2",
handlerFile: "src/internal/products/handlers/handlePlanHasCustomersV2.ts",
method: "GET",
path: "/v1/plans/:product_id/has_customers",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id/has_customers"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePlanHasCustomersV2",
handlerFile: "src/internal/products/handlers/handlePlanHasCustomersV2.ts",
method: "POST",
path: "/v1/plans/:product_id/has_customers",
style: "REST",
group: "v1/plans",
mountChain: ["/v1", "/plans", "/:product_id/has_customers"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleLegacyPlatformExchange",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleLegacyPlatformExchange.ts",
method: "POST",
path: "/v1/platform/beta/exchange",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/exchange"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPlatformOAuth",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts",
method: "POST",
path: "/v1/platform/beta/oauth_url",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/oauth_url"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateOrganizationStripe",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts",
method: "POST",
path: "/v1/platform/beta/organization/stripe",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/organization/stripe"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeletePlatformOrg",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleDeletePlatformOrg.ts",
method: "DELETE",
path: "/v1/platform/beta/organizations",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/organizations"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListPlatformOrgs",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts",
method: "GET",
path: "/v1/platform/beta/organizations",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/organizations"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreatePlatformOrg",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleCreatePlatformOrg.ts",
method: "POST",
path: "/v1/platform/beta/organizations",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/organizations"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateOrganizationStripe",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts",
method: "POST",
path: "/v1/platform/beta/organizations/stripe",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/organizations/stripe"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "listPlatformUsers",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts",
method: "GET",
path: "/v1/platform/beta/users",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform/beta", "/users"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleLegacyPlatformExchange",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleLegacyPlatformExchange.ts",
method: "POST",
path: "/v1/platform/exchange",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/exchange"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPlatformOAuth",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts",
method: "POST",
path: "/v1/platform/oauth_url",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/oauth_url"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateOrganizationStripe",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts",
method: "POST",
path: "/v1/platform/organization/stripe",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/organization/stripe"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeletePlatformOrg",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleDeletePlatformOrg.ts",
method: "DELETE",
path: "/v1/platform/organizations",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/organizations"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListPlatformOrgs",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts",
method: "GET",
path: "/v1/platform/organizations",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/organizations"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreatePlatformOrg",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleCreatePlatformOrg.ts",
method: "POST",
path: "/v1/platform/organizations",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/organizations"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateOrganizationStripe",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts",
method: "POST",
path: "/v1/platform/organizations/stripe",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/organizations/stripe"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "listPlatformUsers",
handlerFile:
"src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts",
method: "GET",
path: "/v1/platform/users",
style: "REST",
group: "v1/platform",
mountChain: ["/v1", "/platform", "/users"],
sourceRouterFile:
"src/internal/platform/platformBeta/platformBetaRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListPlans",
handlerFile: "src/internal/products/handlers/handleListPlans.ts",
method: "GET",
path: "/v1/products",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", ""],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreatePlan",
handlerFile:
"src/internal/products/handlers/handleCreateProduct/handleCreatePlan.ts",
method: "POST",
path: "/v1/products",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", ""],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeletePlanV1",
handlerFile:
"src/internal/products/handlers/handleDeletePlan/handleDeletePlanV1.ts",
method: "DELETE",
path: "/v1/products/:product_id",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPlanV1",
handlerFile:
"src/internal/products/handlers/handleGetPlan/handleGetPlanV1.ts",
method: "GET",
path: "/v1/products/:product_id",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdatePlanV1",
handlerFile:
"src/internal/products/handlers/handleUpdatePlan/handleUpdatePlanV1.ts",
method: "PATCH",
path: "/v1/products/:product_id",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdatePlanV1",
handlerFile:
"src/internal/products/handlers/handleUpdatePlan/handleUpdatePlanV1.ts",
method: "POST",
path: "/v1/products/:product_id",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCopyProductV2",
handlerFile:
"src/internal/products/handlers/handleCopyProduct/handleCopyProductV2.ts",
method: "POST",
path: "/v1/products/:product_id/copy",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id/copy"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetPlanDeleteInfo",
handlerFile: "src/internal/products/handlers/handleGetPlanDeleteInfo.ts",
method: "GET",
path: "/v1/products/:product_id/deletion_info",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id/deletion_info"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePlanHasCustomersV2",
handlerFile: "src/internal/products/handlers/handlePlanHasCustomersV2.ts",
method: "GET",
path: "/v1/products/:product_id/has_customers",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id/has_customers"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handlePlanHasCustomersV2",
handlerFile: "src/internal/products/handlers/handlePlanHasCustomersV2.ts",
method: "POST",
path: "/v1/products/:product_id/has_customers",
style: "REST",
group: "v1/products",
mountChain: ["/v1", "/products", "/:product_id/has_customers"],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleListPlans",
handlerFile: "src/internal/products/handlers/handleListPlans.ts",
method: "GET",
path: "/v1/products_beta",
style: "REST",
group: "v1/products_beta",
mountChain: ["/v1", "/products_beta", ""],
sourceRouterFile: "src/internal/products/planRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleExternalAggregateEvents",
handlerFile:
"src/internal/events/handlers/handleExternalAggregateEvents.ts",
method: "POST",
path: "/v1/query",
style: "REST",
group: "v1/query",
mountChain: ["/v1", "/query", ""],
sourceRouterFile: "src/internal/analytics/legacyAnalyticsRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetRedemption",
handlerFile:
"src/internal/api/rewards/handlers/referrals/handleGetRedemption.ts",
method: "GET",
path: "/v1/redemptions/:redemption_id",
style: "REST",
group: "v1/redemptions",
mountChain: ["/v1", "/redemptions", "/:redemption_id"],
sourceRouterFile: "src/internal/api/rewards/referralRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetReferralCode",
handlerFile:
"src/internal/api/rewards/handlers/referrals/handleGetReferralCode.ts",
method: "POST",
path: "/v1/referrals.create_code",
style: "RPC",
group: "v1/referrals",
mountChain: ["/v1", "", "", "referrals.create_code"],
sourceRouterFile: "src/internal/api/rewards/referralRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRedeemReferral",
handlerFile:
"src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts",
method: "POST",
path: "/v1/referrals.redeem_code",
style: "RPC",
group: "v1/referrals",
mountChain: ["/v1", "", "", "referrals.redeem_code"],
sourceRouterFile: "src/internal/api/rewards/referralRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetReferralCode",
handlerFile:
"src/internal/api/rewards/handlers/referrals/handleGetReferralCode.ts",
method: "POST",
path: "/v1/referrals/code",
style: "REST",
group: "v1/referrals",
mountChain: ["/v1", "/referrals", "/code"],
sourceRouterFile: "src/internal/api/rewards/referralRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleRedeemReferral",
handlerFile:
"src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts",
method: "POST",
path: "/v1/referrals/redeem",
style: "REST",
group: "v1/referrals",
mountChain: ["/v1", "/referrals", "/redeem"],
sourceRouterFile: "src/internal/api/rewards/referralRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateRewardProgram",
handlerFile:
"src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts",
method: "POST",
path: "/v1/reward_programs",
style: "REST",
group: "v1/reward_programs",
mountChain: ["/v1", "/reward_programs", ""],
sourceRouterFile: "src/internal/api/rewards/rewardProgramRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteRewardProgram",
handlerFile:
"src/internal/api/rewards/handlers/rewardPrograms/handleDeleteRewardProgram.ts",
method: "DELETE",
path: "/v1/reward_programs/:id",
style: "REST",
group: "v1/reward_programs",
mountChain: ["/v1", "/reward_programs", "/:id"],
sourceRouterFile: "src/internal/api/rewards/rewardProgramRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateRewardProgram",
handlerFile:
"src/internal/api/rewards/handlers/rewardPrograms/handleUpdateRewardProgram.ts",
method: "PUT",
path: "/v1/reward_programs/:id",
style: "REST",
group: "v1/reward_programs",
mountChain: ["/v1", "/reward_programs", "/:id"],
sourceRouterFile: "src/internal/api/rewards/rewardProgramRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleCreateCoupon",
handlerFile:
"src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts",
method: "POST",
path: "/v1/rewards",
style: "REST",
group: "v1/rewards",
mountChain: ["/v1", "/rewards", ""],
sourceRouterFile: "src/internal/api/rewards/rewardRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleDeleteCoupon",
handlerFile:
"src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts",
method: "DELETE",
path: "/v1/rewards/:id",
style: "REST",
group: "v1/rewards",
mountChain: ["/v1", "/rewards", "/:id"],
sourceRouterFile: "src/internal/api/rewards/rewardRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleGetCoupon",
handlerFile: "src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts",
method: "GET",
path: "/v1/rewards/:id",
style: "REST",
group: "v1/rewards",
mountChain: ["/v1", "/rewards", "/:id"],
sourceRouterFile: "src/internal/api/rewards/rewardRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleUpdateCoupon",
handlerFile:
"src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts",
method: "POST",
path: "/v1/rewards/:internalId",
style: "REST",
group: "v1/rewards",
mountChain: ["/v1", "/rewards", "/:internalId"],
sourceRouterFile: "src/internal/api/rewards/rewardRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSetupPayment",
handlerFile: "src/internal/billing/handlers/handleSetupPayment.ts",
method: "POST",
path: "/v1/setup_payment",
style: "REST",
group: "v1/setup_payment",
mountChain: ["/v1", "", "/setup_payment"],
sourceRouterFile: "src/internal/billing/billingRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleTrack",
handlerFile: "src/internal/balances/handlers/handleTrack.ts",
method: "POST",
path: "/v1/track",
style: "REST",
group: "v1/track",
mountChain: ["/v1", "", "/track"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "handleSetUsage",
handlerFile: "src/internal/balances/handlers/handleSetUsage.ts",
method: "POST",
path: "/v1/usage",
style: "REST",
group: "v1/usage",
mountChain: ["/v1", "", "/usage"],
sourceRouterFile: "src/internal/balances/balancesRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: false,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "POST",
path: "/webhooks/autumn",
style: "REST",
group: "webhooks",
mountChain: ["/webhooks/autumn", ""],
sourceRouterFile: "src/external/autumn/autumnWebhookRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: true,
},
{
handlerName: "handleStripeWebhookEvent",
handlerFile: null,
method: "POST",
path: "/webhooks/connect/:env",
style: "REST",
group: "webhooks",
mountChain: ["", "/webhooks/connect/:env"],
sourceRouterFile: "src/external/stripe/stripeWebhookRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: true,
},
{
handlerName: "RevenueCatWebhookHonoEnv",
handlerFile: null,
method: "POST",
path: "/webhooks/revenuecat/:orgId/:env",
style: "REST",
group: "webhooks",
mountChain: ["/webhooks/revenuecat", "/:orgId/:env"],
sourceRouterFile: "src/external/revenueCat/revenuecatWebhookRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: true,
},
{
handlerName: "handleStripeWebhookEvent",
handlerFile: null,
method: "POST",
path: "/webhooks/stripe/:orgId/:env",
style: "REST",
group: "webhooks",
mountChain: ["", "/webhooks/stripe/:orgId/:env"],
sourceRouterFile: "src/external/stripe/stripeWebhookRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: true,
},
{
handlerName: "<inline>",
handlerFile: null,
method: "ALL",
path: "/webhooks/vercel/:orgId/:env/*",
style: "REST",
group: "webhooks",
mountChain: ["/webhooks/vercel", "/:orgId/:env/*"],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: true,
},
{
handlerName: "vercelLogMiddleware",
handlerFile: null,
method: "POST",
path: "/webhooks/vercel/:orgId/:env/*",
style: "REST",
group: "webhooks",
mountChain: ["/webhooks/vercel", "/:orgId/:env/*"],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "plain",
needsScopes: false,
isWebhookExempt: true,
},
{
handlerName: "handleDeleteInstallation",
handlerFile:
"src/external/vercel/handlers/installations/handleDeleteInstallation.ts",
method: "DELETE",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleGetInstallation",
handlerFile:
"src/external/vercel/handlers/installations/handleGetInstallation.ts",
method: "GET",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleUpdateVercelBillingPlan",
handlerFile: "src/external/vercel/handlers/handleUpdateBillingPlan.ts",
method: "PATCH",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleUpsertInstallation",
handlerFile:
"src/external/vercel/handlers/installations/handleUpsertInstallation.ts",
method: "PUT",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleListBillingPlansPerInstall",
handlerFile: "src/external/vercel/handlers/handleListBillingPlans.ts",
method: "GET",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/plans",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/plans",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleCreateResourceTransfer",
handlerFile:
"src/external/vercel/handlers/transfers/handleResourceTransfers.ts",
method: "POST",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resource-transfer-requests",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resource-transfer-requests",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleAcceptResourceTransfer",
handlerFile:
"src/external/vercel/handlers/transfers/handleResourceTransfers.ts",
method: "POST",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resource-transfer-requests/:providerClaimId/accept",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resource-transfer-requests/:providerClaimId/accept",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleVerifyResourceTransfer",
handlerFile:
"src/external/vercel/handlers/transfers/handleResourceTransfers.ts",
method: "GET",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resource-transfer-requests/:providerClaimId/verify",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resource-transfer-requests/:providerClaimId/verify",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleCreateResource",
handlerFile:
"src/external/vercel/handlers/resources/handleCreateResource.ts",
method: "POST",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resources",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resources",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleDeleteResource",
handlerFile:
"src/external/vercel/handlers/resources/handleDeleteResource.ts",
method: "DELETE",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resources/:resourceId",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resources/:resourceId",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleGetResource",
handlerFile: "src/external/vercel/handlers/resources/handleGetResource.ts",
method: "GET",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resources/:resourceId",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resources/:resourceId",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleUpdateResource",
handlerFile:
"src/external/vercel/handlers/resources/handleUpdateResource.ts",
method: "PATCH",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resources/:resourceId",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resources/:resourceId",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleRotateResourceSecret",
handlerFile:
"src/external/vercel/handlers/resources/handleRotateResourceSecret.ts",
method: "POST",
path: "/webhooks/vercel/:orgId/:env/v1/installations/:integrationConfigurationId/resources/:resourceId/secrets/rotate",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/installations/:integrationConfigurationId",
"/resources/:resourceId/secrets/rotate",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
{
handlerName: "handleListBillingPlansPerInstall",
handlerFile: "src/external/vercel/handlers/handleListBillingPlans.ts",
method: "GET",
path: "/webhooks/vercel/:orgId/:env/v1/products/:productId/plans",
style: "REST",
group: "webhooks",
mountChain: [
"/webhooks/vercel",
"/:orgId/:env/v1/products/:productId/plans",
],
sourceRouterFile: "src/external/vercel/vercelWebhookRouter.ts",
routeKind: "createRoute",
needsScopes: true,
isWebhookExempt: true,
},
] as const;
// ---------------------------------------------------------------------------
// Inlined from scope-decisions.json (the user-authored scope decisions).
// Regenerate by copying scope-decisions.json contents if decisions change.
// ---------------------------------------------------------------------------
const SCOPE_DECISIONS: Record<
string,
{
decision: "decided" | "skip" | "unknown";
scopes: string[];
shape: "array" | "all" | "any" | "any-and-all";
note?: string;
decidedAt: string;
}
> = {
"GET|/admin/customer-block-config|handleGetAdminCustomerBlockConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:49.622Z",
},
"PUT|/admin/customer-block-config|handleUpsertAdminCustomerBlockConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:50.530Z",
},
"GET|/admin/edge-config-sources|handleGetAdminEdgeConfigSources": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:51.351Z",
},
"GET|/admin/feature-flags-config|handleGetAdminFeatureFlagsConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:52.407Z",
},
"PUT|/admin/feature-flags-config|handleUpsertAdminFeatureFlagsConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:53.392Z",
},
"POST|/admin/invoice-line-items|handleGetInvoiceLineItems": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:53.898Z",
},
"GET|/admin/master-stripe-account|handleGetMasterStripeAccount": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:54.473Z",
},
"GET|/admin/oauth-clients|handleListOAuthClients": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:55.045Z",
},
"GET|/admin/org-limits-config|handleGetAdminOrgLimitsConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:55.674Z",
},
"PUT|/admin/org-limits-config|handleUpsertAdminOrgLimitsConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:56.250Z",
},
"GET|/admin/org-member|handleGetOrgMember": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:56.767Z",
},
"GET|/admin/orgs|handleListAdminOrgs": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:57.278Z",
},
"GET|/admin/orgs/:org_id/request-block|handleGetAdminOrgRequestBlock": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:58.241Z",
},
"PUT|/admin/orgs/:org_id/request-block|handleUpsertAdminOrgRequestBlock": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:58.829Z",
},
"GET|/admin/redis-v2-cache-config|handleGetAdminRedisV2CacheConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:59.305Z",
},
"PUT|/admin/redis-v2-cache-config|handleUpsertAdminRedisV2CacheConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:14:59.886Z",
},
"GET|/admin/request-block-config|handleGetAdminRequestBlockConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:00.471Z",
},
"PUT|/admin/request-block-config|handleUpsertAdminRequestBlockConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:01.015Z",
},
"GET|/admin/rollouts|handleGetRollouts": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:01.597Z",
},
"DELETE|/admin/rollouts/:rollout_id|handleDeleteRollout": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:02.161Z",
},
"PUT|/admin/rollouts/:rollout_id|handleUpdateRollout": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:02.849Z",
},
"DELETE|/admin/rollouts/:rollout_id/orgs/:org_id|handleDeleteRolloutOrg": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:03.871Z",
},
"PUT|/admin/rollouts/:rollout_id/orgs/:org_id|handleUpdateRolloutOrg": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:04.775Z",
},
"GET|/admin/stripe-sync-config|handleGetAdminStripeSyncConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:05.534Z",
},
"PUT|/admin/stripe-sync-config|handleUpsertAdminStripeSyncConfig": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:06.090Z",
},
"GET|/admin/users|handleListAdminUsers": {
decision: "decided",
scopes: ["superuser"],
shape: "array",
decidedAt: "2026-04-24T15:15:07.085Z",
},
"GET|/checkouts/:checkout_id|handleGetCheckout": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "end-customer checkout link \u2014 authd by possession of checkout id, not by scope",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/checkouts/:checkout_id/confirm|handleConfirmCheckout": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "end-customer checkout link \u2014 authd by possession of checkout id, not by scope",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/checkouts/:checkout_id/preview|handlePreviewCheckout": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "end-customer checkout link \u2014 authd by possession of checkout id, not by scope",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/consents|handleGetOrgConsents": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:14:12.894Z",
},
"POST|/cli/api-keys|handleCreateOAuthApiKeys": {
decision: "decided",
scopes: ["public"],
shape: "array",
decidedAt: "2026-04-24T17:14:22.723Z",
},
"DELETE|/consents/:consent_id|handleRevokeConsent": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:15:39.420Z",
},
"GET|/consents/:consent_id/api-keys|handleGetConsentApiKeys": {
decision: "decided",
scopes: ["apiKeys:read"],
shape: "array",
decidedAt: "2026-04-24T15:15:59.421Z",
},
"GET|/customers/:customer_id|handleGetCustomer": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:16:07.564Z",
},
"POST|/customers/:customer_id/invoice-line-items|handleGetInvoiceLineItems": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:16:18.846Z",
},
"GET|/customers/:customer_id/product/:product_id|handleGetCustomerProduct": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:16:28.472Z",
},
"GET|/customers/:customer_id/referrals|handleGetCusReferrals": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:16:29.544Z",
},
"GET|/customers/:customer_id/schedule|handleGetCustomerSchedule": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:16:30.750Z",
},
"POST|/customers/all/full_customers|handleGetFullCustomers": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:16:41.452Z",
},
"POST|/customers/all/search|handleSearchCustomers": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:16:46.476Z",
},
"POST|/dev/api_key|handleCreateSecretKey": {
decision: "decided",
scopes: ["apiKeys:write"],
shape: "array",
decidedAt: "2026-04-24T15:16:52.779Z",
},
"DELETE|/dev/api_key/:key_id|handleDeleteSecretKey": {
decision: "decided",
scopes: ["apiKeys:write"],
shape: "array",
decidedAt: "2026-04-24T15:16:54.341Z",
},
"POST|/dev/cli/stripe|handleCliStripe": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "Mounted on the public router; auth is handled internally via the OTP token in the Authorization header.",
decidedAt: "2026-04-24T15:18:12.814Z",
},
"GET|/dev/data|handleGetDevData": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:21:14.841Z",
},
"POST|/dev/otp|handleCreateOtp": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "DISMOUNT: route is deprecated/unused; delete in follow-up PR",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/dev/otp/:otp|handleGetOtp": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "DISMOUNT: route is deprecated/unused; delete in follow-up PR",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/feedback|handleSubmitFeedback": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "any dashboard user should be able to submit feedback",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/invoices/hosted_invoice_url/:invoiceId|handleRedirectToInvoice": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "public route \u2014 handler comment explicitly says no auth required; authd by possession of invoice id in URL",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"DELETE|/organization|handleDeleteOrg": {
decision: "decided",
scopes: ["owner"],
shape: "array",
decidedAt: "2026-04-24T15:25:14.675Z",
},
"GET|/organization|handleGetOrg": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:25:17.082Z",
},
"GET|/organization/invites|handleGetInvites": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:25:20.371Z",
},
"GET|/organization/members|handleGetOrgMembers": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:25:21.434Z",
},
"POST|/organization/remove-member|handleRemoveMember": {
decision: "decided",
scopes: ["admin", "owner"],
shape: "any",
note: "only admin or owner can remove org members",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/organization/upload_url|handleGetUploadUrl": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:25:47.915Z",
},
"POST|/pricing-agent/preview/setup|handleSetupPreviewOrg": {
decision: "decided",
scopes: ["plans:write", "features:write", "customers:write"],
shape: "all",
note: "pricing-agent preview sandbox setup \u2014 writes plans/features/customers into a per-user preview org",
decidedAt: "2026-04-24T17:16:14.920Z",
},
"GET|/products/:productId/count|handleGetProductCount": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:27:03.949Z",
},
"GET|/products/:productId/data|handleGetProductInternal": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:27:07.130Z",
},
"GET|/products/:productId/info|handleGetProductDeleteInfo": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:27:08.212Z",
},
"POST|/products/copy_to_production|handleCopyEnvironment": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:27:11.067Z",
},
"GET|/products/features|handleGetFeatures": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:27:30.075Z",
},
"GET|/products/migrations|handleGetMigrations": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:28:41.231Z",
},
"GET|/products/product_counts|handleGetProductCounts": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:28:42.588Z",
},
"GET|/products/products|handleGetProducts": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:28:43.871Z",
},
"GET|/products/rewards|handleGetRewards": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:28:45.086Z",
},
"GET|/products/stripe_coupons|handleGetStripeCoupons": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:28:45.870Z",
},
"GET|/query/event_names|handleGetEventNames": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:28:57.508Z",
},
"GET|/query/event_names/list|handleListEventNames": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:29:01.428Z",
},
"POST|/query/events|handleInternalAggregateEvents": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:08.864Z",
},
"POST|/query/raw|handleInternalListRawEvents": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:14.798Z",
},
"POST|/query/revenue/arpc|handleArpc": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:17.908Z",
},
"POST|/query/revenue/by-product|handleRevenueByProduct": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:20.976Z",
},
"POST|/query/revenue/customer-leaderboard|handleCustomerLeaderboard": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:22.863Z",
},
"POST|/query/revenue/estimated-mrr|handleEstimatedMrr": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:24.146Z",
},
"POST|/query/revenue/invoice-status|handleInvoiceStatus": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:26.527Z",
},
"POST|/query/revenue/product-share|handleRevenueProductShare": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:30:28.518Z",
},
"GET|/saved_views|handleGetViews": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "customer-list filter helper \u2014 any dashboard user should be able to use saved views",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"DELETE|/saved_views/:viewId|handleDeleteView": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "customer-list filter helper \u2014 any dashboard user should be able to use saved views",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/saved_views/save|handleSaveView": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "customer-list filter helper \u2014 any dashboard user should be able to use saved views",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/trmnl/device_id|handleGetTrmnlDeviceId": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:31:27.909Z",
},
"POST|/trmnl/device_id|handlePostTrmnlDeviceId": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:31:29.480Z",
},
"POST|/trmnl/screen|handleGenerateTrmnlScreen": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:32:02.692Z",
},
// Removed phantom route (commented-out registration in entityRouter.ts).
"POST|/v1/attach|handleAttach": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:10.141Z",
},
"POST|/v1/attach/preview|handleAttachPreview": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:32:12.673Z",
},
"POST|/v1/balances.check|handleCheck": {
decision: "decided",
scopes: ["balances:read"],
shape: "array",
decidedAt: "2026-04-24T15:32:17.905Z",
},
"POST|/v1/balances.create|handleCreateBalance": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:19.386Z",
},
"POST|/v1/balances.delete|handleDeleteBalance": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:20.324Z",
},
"POST|/v1/balances.finalize|handleFinalizeLock": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:21.294Z",
},
"POST|/v1/balances.track|handleTrack": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:22.265Z",
},
"POST|/v1/balances.update|handleUpdateBalance": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:23.339Z",
},
"POST|/v1/balances/create|handleCreateBalance": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:24.694Z",
},
"GET|/v1/balances/list|handleListBalances": {
decision: "decided",
scopes: ["balances:read"],
shape: "array",
decidedAt: "2026-04-24T15:32:34.295Z",
},
"POST|/v1/balances/update|handleUpdateBalance": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:35.265Z",
},
"POST|/v1/billing.attach|handleAttachV2": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:37.301Z",
},
"POST|/v1/billing.create_schedule|handleCreateSchedule": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:38.219Z",
},
"POST|/v1/billing.multi_attach|handleMultiAttach": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:32:39.040Z",
},
"POST|/v1/billing.open_customer_portal|handleOpenCustomerPortalV2": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:32:44.801Z",
},
"POST|/v1/billing.preview_attach|handlePreviewAttach": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:32:50.473Z",
},
"POST|/v1/billing.preview_create_schedule|handlePreviewCreateSchedule": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:32:59.162Z",
},
"POST|/v1/billing.preview_multi_attach|handlePreviewMultiAttach": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:33:00.970Z",
},
"POST|/v1/billing.preview_update|handlePreviewUpdateSubscription": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:33:02.136Z",
},
"POST|/v1/billing.setup_payment|handleSetupPaymentV2": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:33:04.670Z",
},
"POST|/v1/billing.sync|handleSync": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:33:08.305Z",
},
"POST|/v1/billing.sync_proposals|handleSyncProposals": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:33:39.612Z",
},
"POST|/v1/billing.update|handleUpdateSubscription": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:33:41.349Z",
},
"POST|/v1/cancel|handleCancelV2": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:33:42.627Z",
},
"POST|/v1/check|handleCheck": {
decision: "decided",
scopes: ["balances:read"],
shape: "array",
decidedAt: "2026-04-24T15:33:48.659Z",
},
"POST|/v1/checkout|handleLegacyApiCheckout": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:34:12.007Z",
},
"GET|/v1/components/pricing_table|handleGetPricingTable": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "pricing table is public content for anyone to render",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"DELETE|/v1/configs/nuke|handleNukeOrganisationConfiguration": {
decision: "decided",
scopes: [
"organisation:write",
"plans:write",
"features:write",
"customers:write",
],
shape: "all",
note: "destructive sandbox-only nuke; wipes customers/plans/features/org config. Sandbox-gated at handler level.",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/configs/push|handlePushOrganisationConfiguration": {
decision: "decided",
scopes: ["plans:write", "features:write", "customers:write"],
shape: "all",
note: "pushes plans/features/customers config; does not touch org itself",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/v1/customers|handleListCustomers": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:35:31.988Z",
},
"POST|/v1/customers|handlePostCustomer": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:35:33.621Z",
},
"POST|/v1/customers.delete|handleDeleteCustomerV2": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:35:34.876Z",
},
"POST|/v1/customers.get_or_create|handleGetOrCreateCustomerV2": {
decision: "decided",
scopes: ["customers:write", "customers:read"],
shape: "any",
note: "idk this one is tricky",
decidedAt: "2026-04-24T15:36:03.015Z",
},
"POST|/v1/customers.list|handleListCustomersV2": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:36:05.000Z",
},
"POST|/v1/customers.update|handleUpdateCustomerV2": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:36:06.317Z",
},
"DELETE|/v1/customers/:customer_id|handleDeleteCustomer": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:36:07.751Z",
},
"GET|/v1/customers/:customer_id|handleGetCustomerV2": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:36:12.258Z",
},
"PATCH|/v1/customers/:customer_id|handleUpdateCustomer": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:36:13.636Z",
},
"POST|/v1/customers/:customer_id|handleUpdateCustomer": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:37:57.429Z",
},
"POST|/v1/customers/:customer_id/balances|handleUpdateBalancesV2": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:38:02.645Z",
},
"GET|/v1/customers/:customer_id/billing_portal|handleGetBillingPortal": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:38:11.277Z",
},
"POST|/v1/customers/:customer_id/billing_portal|handleCreateBillingPortal": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:38:17.668Z",
},
"POST|/v1/customers/:customer_id/coupons/:coupon_id|handleAddCouponToCusV2": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:38:24.659Z",
},
"GET|/v1/customers/:customer_id/entities|handleListEntities": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:38:28.091Z",
},
"POST|/v1/customers/:customer_id/entities|handleCreateEntity": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:38:29.178Z",
},
"DELETE|/v1/customers/:customer_id/entities/:entity_id|handleDeleteEntity": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:38:30.248Z",
},
"GET|/v1/customers/:customer_id/entities/:entity_id|handleGetEntity": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:38:31.379Z",
},
"POST|/v1/customers/:customer_id/invoices/:stripe_invoice_id/refund|handleRefundInvoice":
{
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:38:34.496Z",
},
"POST|/v1/customers/:customer_id/transfer|handleTransferProductV2": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:38:39.788Z",
},
"POST|/v1/customers/clear_cache|handleClearCustomerCache": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
note: "invalidates cache \u2014 mutation even though it reads customer data",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/customers/list|handleListCustomersV2": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:39:08.015Z",
},
"POST|/v1/entities.create|handleCreateEntityV2": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:39:13.536Z",
},
"POST|/v1/entities.delete|handleDeleteEntityV2": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:39:14.279Z",
},
"POST|/v1/entities.get|handleGetEntityV2": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-04-24T15:39:15.066Z",
},
"POST|/v1/entities.list|handleListEntitiesV2": {
decision: "decided",
scopes: ["customers:read"],
shape: "array",
decidedAt: "2026-05-08T00:00:00.000Z",
},
"POST|/v1/entities.update|handleUpdateEntity": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:39:16.097Z",
},
"POST|/v1/entitled|handleCheck": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "DISMOUNT: route is deprecated/unused; delete in follow-up PR",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/events|handleTrack": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:40:14.015Z",
},
"POST|/v1/events.aggregate|handleExternalAggregateEvents": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:21.951Z",
},
"POST|/v1/events.list|handleExternalListEvents": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:23.628Z",
},
"POST|/v1/events/aggregate|handleExternalAggregateEvents": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:26.755Z",
},
"POST|/v1/events/list|handleExternalListEvents": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:29.599Z",
},
"GET|/v1/features|handleListFeaturesV1": {
decision: "decided",
scopes: ["features:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:31.185Z",
},
"POST|/v1/features|handleCreateFeatureV1": {
decision: "decided",
scopes: ["features:write"],
shape: "array",
decidedAt: "2026-04-24T15:40:32.183Z",
},
"POST|/v1/features.create|handleCreateFeatureV2": {
decision: "decided",
scopes: ["features:write"],
shape: "array",
decidedAt: "2026-04-24T15:40:32.989Z",
},
"POST|/v1/features.delete|handleDeleteFeatureV2": {
decision: "decided",
scopes: ["features:write"],
shape: "array",
decidedAt: "2026-04-24T15:40:33.780Z",
},
"POST|/v1/features.get|handleGetFeatureV2": {
decision: "decided",
scopes: ["features:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:34.715Z",
},
"POST|/v1/features.list|handleListFeaturesV1": {
decision: "decided",
scopes: ["features:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:35.570Z",
},
"POST|/v1/features.update|handleUpdateFeatureV2": {
decision: "decided",
scopes: ["features:write"],
shape: "array",
decidedAt: "2026-04-24T15:40:36.487Z",
},
"DELETE|/v1/features/:feature_id|handleDeleteFeatureV1": {
decision: "decided",
scopes: ["features:write"],
shape: "array",
decidedAt: "2026-04-24T15:40:37.556Z",
},
"GET|/v1/features/:feature_id|handleGetFeatureV1": {
decision: "decided",
scopes: ["features:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:38.557Z",
},
"POST|/v1/features/:feature_id|handleUpdateFeatureV1": {
decision: "decided",
scopes: ["features:write"],
shape: "array",
decidedAt: "2026-04-24T15:40:40.253Z",
},
"GET|/v1/features/:feature_id/deletion_info|handleGetFeatureDeletionInfo": {
decision: "decided",
scopes: ["features:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:41.648Z",
},
"GET|/v1/invoices/:stripe_invoice_id/stripe|handleGetStripeInvoice": {
decision: "decided",
scopes: ["billing:read"],
shape: "array",
decidedAt: "2026-04-24T15:40:49.153Z",
},
"POST|/v1/migrations|handleMigrateProductV2": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:41:37.994Z",
},
"GET|/v1/organization|handleGetOrg": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:41:41.708Z",
},
"PATCH|/v1/organization|handleUpdateOrg": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:41:42.709Z",
},
"GET|/v1/organization/flags|handleGetOrgFlags": {
decision: "decided",
scopes: ["public"],
shape: "array",
note: "exposes runtime feature flags \u2014 any authd user; auth middlewares upstream have already validated the session",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/organization/reset_default_account|handleResetDefaultAccount": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:00.564Z",
},
"GET|/v1/organization/revenuecat|handleGetRevenueCatConfig": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:03.613Z",
},
"PATCH|/v1/organization/revenuecat|handleUpsertRevenueCatConfig": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:05.342Z",
},
"GET|/v1/organization/revenuecat/mappings|handleGetRCMappings": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:06.753Z",
},
"POST|/v1/organization/revenuecat/mappings|handleSaveRCMappings": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:07.484Z",
},
"POST|/v1/organization/revenuecat/products|handleGetRevenueCatProducts": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:14.571Z",
},
"DELETE|/v1/organization/stripe|handleDeleteStripe": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:16.105Z",
},
"GET|/v1/organization/stripe|handleGetStripeAccount": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:18.543Z",
},
"POST|/v1/organization/stripe|handleConnectStripe": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:21.625Z",
},
"GET|/v1/organization/stripe/oauth_url|handleGetOAuthUrl": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:26.471Z",
},
"PATCH|/v1/organization/vercel|handleUpsertVercelConfig": {
decision: "decided",
scopes: ["organisation:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:29.824Z",
},
"GET|/v1/organization/vercel_sink|handleGetVercelSink": {
decision: "decided",
scopes: ["organisation:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:33.671Z",
},
"GET|/v1/plans|handleListPlans": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:37.356Z",
},
"POST|/v1/plans|handleCreatePlan": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:38.326Z",
},
"POST|/v1/plans.create|handleCreatePlanV2": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:39.131Z",
},
"POST|/v1/plans.delete|handleDeletePlanV2": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:40.176Z",
},
"POST|/v1/plans.get|handleGetPlanV2": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:41.168Z",
},
"POST|/v1/plans.list|handleListPlansV2": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:42.139Z",
},
"POST|/v1/plans.update|handleUpdatePlanV2": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:47.335Z",
},
"DELETE|/v1/plans/:product_id|handleDeletePlanV1": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:48.755Z",
},
"GET|/v1/plans/:product_id|handleGetPlanV1": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:49.507Z",
},
"PATCH|/v1/plans/:product_id|handleUpdatePlanV1": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:53.441Z",
},
"POST|/v1/plans/:product_id|handleUpdatePlanV1": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:54.435Z",
},
"POST|/v1/plans/:product_id/copy|handleCopyProductV2": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:42:55.763Z",
},
"GET|/v1/plans/:product_id/deletion_info|handleGetPlanDeleteInfo": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:42:57.841Z",
},
"GET|/v1/plans/:product_id/has_customers|handlePlanHasCustomersV2": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:43:00.228Z",
},
"POST|/v1/plans/:product_id/has_customers|handlePlanHasCustomersV2": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:43:03.957Z",
},
"POST|/v1/platform/beta/exchange|handleLegacyPlatformExchange": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/beta/oauth_url|handleGetPlatformOAuth": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/beta/organization/stripe|handleUpdateOrganizationStripe": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"DELETE|/v1/platform/beta/organizations|handleDeletePlatformOrg": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/v1/platform/beta/organizations|handleListPlatformOrgs": {
decision: "decided",
scopes: ["platform:read"],
shape: "array",
note: "platform API route \u2014 read",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/beta/organizations|handleCreatePlatformOrg": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/beta/organizations/stripe|handleUpdateOrganizationStripe":
{
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/v1/platform/beta/users|listPlatformUsers": {
decision: "decided",
scopes: ["platform:read"],
shape: "array",
note: "platform API route \u2014 read",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/exchange|handleLegacyPlatformExchange": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/oauth_url|handleGetPlatformOAuth": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/organization/stripe|handleUpdateOrganizationStripe": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"DELETE|/v1/platform/organizations|handleDeletePlatformOrg": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/v1/platform/organizations|handleListPlatformOrgs": {
decision: "decided",
scopes: ["platform:read"],
shape: "array",
note: "platform API route \u2014 read",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/organizations|handleCreatePlatformOrg": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"POST|/v1/platform/organizations/stripe|handleUpdateOrganizationStripe": {
decision: "decided",
scopes: ["platform:write"],
shape: "array",
note: "platform API route \u2014 write",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/v1/platform/users|listPlatformUsers": {
decision: "decided",
scopes: ["platform:read"],
shape: "array",
note: "platform API route \u2014 read",
decidedAt: "2026-04-24T17:11:05.951Z",
},
"GET|/v1/products|handleListPlans": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:44:20.011Z",
},
"POST|/v1/products|handleCreatePlan": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:44:21.972Z",
},
"DELETE|/v1/products/:product_id|handleDeletePlanV1": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:44:22.719Z",
},
"GET|/v1/products/:product_id|handleGetPlanV1": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:44:23.830Z",
},
"PATCH|/v1/products/:product_id|handleUpdatePlanV1": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:44:24.641Z",
},
"POST|/v1/products/:product_id|handleUpdatePlanV1": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:44:25.474Z",
},
"POST|/v1/products/:product_id/copy|handleCopyProductV2": {
decision: "decided",
scopes: ["plans:write"],
shape: "array",
decidedAt: "2026-04-24T15:44:26.527Z",
},
"GET|/v1/products/:product_id/deletion_info|handleGetPlanDeleteInfo": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:44:27.614Z",
},
"GET|/v1/products/:product_id/has_customers|handlePlanHasCustomersV2": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:44:28.678Z",
},
"POST|/v1/products/:product_id/has_customers|handlePlanHasCustomersV2": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:44:32.111Z",
},
"GET|/v1/products_beta|handleListPlans": {
decision: "decided",
scopes: ["plans:read"],
shape: "array",
decidedAt: "2026-04-24T15:44:43.165Z",
},
"POST|/v1/query|handleExternalAggregateEvents": {
decision: "decided",
scopes: ["analytics:read"],
shape: "array",
decidedAt: "2026-04-24T15:44:56.063Z",
},
"GET|/v1/redemptions/:redemption_id|handleGetRedemption": {
decision: "decided",
scopes: ["rewards:read"],
shape: "array",
decidedAt: "2026-04-24T15:45:28.170Z",
},
"POST|/v1/referrals.create_code|handleGetReferralCode": {
decision: "decided",
scopes: ["rewards:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:29.952Z",
},
"POST|/v1/referrals.redeem_code|handleRedeemReferral": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:37.490Z",
},
"POST|/v1/referrals/code|handleGetReferralCode": {
decision: "decided",
scopes: ["rewards:read"],
shape: "array",
decidedAt: "2026-04-24T15:45:41.083Z",
},
"POST|/v1/referrals/redeem|handleRedeemReferral": {
decision: "decided",
scopes: ["customers:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:46.196Z",
},
"POST|/v1/reward_programs|handleCreateRewardProgram": {
decision: "decided",
scopes: ["rewards:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:48.561Z",
},
"DELETE|/v1/reward_programs/:id|handleDeleteRewardProgram": {
decision: "decided",
scopes: ["rewards:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:49.510Z",
},
"PUT|/v1/reward_programs/:id|handleUpdateRewardProgram": {
decision: "decided",
scopes: ["rewards:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:50.515Z",
},
"POST|/v1/rewards|handleCreateCoupon": {
decision: "decided",
scopes: ["rewards:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:51.557Z",
},
"DELETE|/v1/rewards/:id|handleDeleteCoupon": {
decision: "decided",
scopes: ["rewards:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:52.577Z",
},
"GET|/v1/rewards/:id|handleGetCoupon": {
decision: "decided",
scopes: ["rewards:read"],
shape: "array",
decidedAt: "2026-04-24T15:45:53.479Z",
},
"POST|/v1/rewards/:internalId|handleUpdateCoupon": {
decision: "decided",
scopes: ["rewards:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:54.523Z",
},
"POST|/v1/setup_payment|handleSetupPayment": {
decision: "decided",
scopes: ["billing:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:56.296Z",
},
"POST|/v1/track|handleTrack": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:58.333Z",
},
"POST|/v1/usage|handleSetUsage": {
decision: "decided",
scopes: ["balances:write"],
shape: "array",
decidedAt: "2026-04-24T15:45:59.796Z",
},
"POST|/pricing-agent/preview/sync|handleSyncPreviewPricing": {
decision: "decided",
scopes: ["plans:write", "features:write", "customers:write"],
shape: "all",
note: "pricing-agent preview config sync \u2014 writes plans/features/customers into a per-user preview org",
decidedAt: "2026-04-24T17:16:14.920Z",
},
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function generateRandomSuffix(): string {
return crypto.randomBytes(16).toString("hex");
}
// Pick a failing scope — any modern scope NOT in the route's required list.
// If the route requires analytics:read, fall back to plans:read.
function pickFailingScope(required: readonly string[]): string {
if (!required.includes("analytics:read")) return "analytics:read";
return "plans:read";
}
// Substitute any `:paramName` fragment with the string "test".
function synthesisePath(p: string): string {
return p.replace(/:([\w-]+)/g, "test");
}
function hasBody(method: string): boolean {
return method === "POST" || method === "PUT" || method === "PATCH";
}
type ApiKeyRow = typeof apiKeys.$inferInsert;
type DbCli = typeof ctx.db;
async function seedTestKey(
db: DbCli,
orgId: string,
scopes: string[] | null,
nameTag: string,
): Promise<string> {
const suffix = generateRandomSuffix();
const key = `am_sk_test_${suffix}`;
const hashedKey = hashApiKey(key);
const row: ApiKeyRow = {
id: generateId("key"),
org_id: orgId,
user_id: null,
name: `scope-403-test-${nameTag}-${Date.now()}`,
prefix: key.substring(0, 14),
created_at: Date.now(),
env: AppEnv.Sandbox,
hashed_key: hashedKey,
meta: {},
scopes,
};
await db.insert(apiKeys).values(row);
return key;
}
// Route type mirrors the inlined JSON entries.
type Route = {
handlerName: string;
handlerFile: string;
method: string;
path: string;
style: string;
group: string;
mountChain: string[];
sourceRouterFile: string;
routeKind: string;
needsScopes: boolean;
isWebhookExempt: boolean;
};
const TEST_KEY_NAME_PREFIX = "scope-403-test-";
// Gate whole suite on DB availability (mirrors other integration tests).
const describeDb = process.env.TESTS_ORG ? describe : describe.skip;
// ---------------------------------------------------------------------------
// Suite
// ---------------------------------------------------------------------------
describeDb("scope 403 coverage", () => {
let app: ReturnType<typeof createHonoApp>;
// Failing key granted "analytics:read" — any route not requiring
// analytics:read should 403 when hit with this key.
let failingKey: string;
// Alt failing key granted "plans:read" — used for routes that
// require analytics:read (so the declared scope is NOT satisfied).
let altFailingKey: string;
// Legacy empty-scopes key — the fail-open path.
let emptyScopesKey: string;
beforeAll(async () => {
app = createHonoApp();
failingKey = await seedTestKey(
ctx.db,
ctx.org.id,
["analytics:read"],
"analytics-only",
);
altFailingKey = await seedTestKey(
ctx.db,
ctx.org.id,
["plans:read"],
"plans-only",
);
emptyScopesKey = await seedTestKey(
ctx.db,
ctx.org.id,
null,
"legacy-empty",
);
});
afterAll(async () => {
// Wipe every test key inserted by this suite. Uses a LIKE-ish
// filter by name prefix to avoid coupling to the suffix.
const rows = await ctx.db
.select({ id: apiKeys.id, name: apiKeys.name })
.from(apiKeys)
.where(eq(apiKeys.org_id, ctx.org.id));
for (const r of rows) {
if (r.name && r.name.startsWith(TEST_KEY_NAME_PREFIX)) {
await ctx.db.delete(apiKeys).where(eq(apiKeys.id, r.id));
}
}
});
const routesToTest = (
ROUTES as readonly unknown[] as readonly Route[]
).filter((r) => r.needsScopes && !r.isWebhookExempt);
// A route is "API-key reachable" when an incoming `Bearer am_sk_*`
// secret key reaches the scope middleware. In practice that is every
// route mounted under `/v1/*` (apiRouter uses secretKeyMiddleware).
// Non-v1 routes are mounted under internalRouter which uses
// betterAuthMiddleware (session cookies) — a bearer token 401s there
// before the scope middleware runs.
const isApiKeyReachable = (route: Route) => route.path.startsWith("/v1/");
describe("v1 routes (secret-key auth): 403 on mismatched scope", () => {
for (const route of routesToTest) {
if (!isApiKeyReachable(route)) continue;
const key = `${route.method}|${route.path}|${route.handlerName}`;
const decision = SCOPE_DECISIONS[key];
// Missing decision => should not happen (route-scanner invariant),
// but fail LOUDLY rather than silently skipping.
if (!decision) {
test(`[MISSING-DECISION] ${route.method} ${route.path}`, () => {
throw new Error(
`No scope-decision entry found for ${key}. The route-scanner and scope-decisions file are out of sync.`,
);
});
continue;
}
// Undecided flags — assert they don't silently 403, so an
// accidental scope addition gets caught.
if (decision.decision !== "decided") {
test(`[UNDECIDED ${decision.decision}] ${route.method} ${route.path} should not 403 with insufficient_scopes`, async () => {
const res = await app.fetch(
new Request(`http://localhost${synthesisePath(route.path)}`, {
method: route.method,
headers: {
Authorization: `Bearer ${failingKey ?? ""}`,
"Content-Type": "application/json",
},
body: hasBody(route.method) ? "{}" : undefined,
}),
);
if (res.status === 403) {
const body = (await res.json().catch(() => ({}))) as {
code?: string;
};
expect(body.code).not.toBe("insufficient_scopes");
} else {
expect(res.status).not.toBe(403);
}
});
continue;
}
// Public routes are covered by the public-routes block.
if (decision.scopes.includes("public")) continue;
const failingScope = pickFailingScope(decision.scopes);
test(`${route.method} ${route.path} (${route.handlerName}) 403s on missing scope`, async () => {
// Resolve the key inside the async test body — `failingKey`
// and `altFailingKey` are populated by `beforeAll`, which runs
// AFTER the describe callback that schedules tests.
const keyToUse =
failingScope === "plans:read" ? altFailingKey : failingKey;
const res = await app.fetch(
new Request(`http://localhost${synthesisePath(route.path)}`, {
method: route.method,
headers: {
Authorization: `Bearer ${keyToUse}`,
"Content-Type": "application/json",
},
body: hasBody(route.method) ? "{}" : undefined,
}),
);
expect(res.status).toBe(403);
const body = (await res.json().catch(() => ({}))) as { code?: string };
expect(body.code).toBe("insufficient_scopes");
});
}
});
// Non-v1 routes (session-auth only): a bearer API key must NOT be
// accepted. The guarantee we actually care about: the scope
// middleware does not silently pass such a request. Either:
// (a) betterAuthMiddleware 401s (session missing) — expected, OR
// (b) the route is public and doesn't hit the scope gate at all.
// What we MUST NOT see is a 2xx/3xx/handler response with a declared
// non-public scope requirement AND no session — that would mean the
// route is unreachable-by-key by accident but reachable-by-key
// without scope enforcement. The assertion below catches this: for
// any non-v1 route with a declared non-public scope, a bearer key
// must produce either 401 or 403-insufficient_scopes, never a 2xx.
describe("non-v1 routes (session-auth): bearer key cannot bypass auth", () => {
for (const route of routesToTest) {
if (isApiKeyReachable(route)) continue;
const key = `${route.method}|${route.path}|${route.handlerName}`;
const decision = SCOPE_DECISIONS[key];
if (!decision || decision.decision !== "decided") continue;
// Public non-v1 routes are covered by the public-routes block.
if (decision.scopes.includes("public")) continue;
test(`${route.method} ${route.path} (${route.handlerName}) refuses bearer key (no 2xx/3xx)`, async () => {
const res = await app.fetch(
new Request(`http://localhost${synthesisePath(route.path)}`, {
method: route.method,
headers: {
Authorization: `Bearer ${failingKey}`,
"Content-Type": "application/json",
},
body: hasBody(route.method) ? "{}" : undefined,
}),
);
// Must be an auth-layer refusal, not a successful handler run.
// Acceptable codes:
// 401 (session missing — expected for internal routes)
// 403 (scope gate fired — also acceptable)
// 500 (auth-layer error — acceptable too, though noisy)
// NOT acceptable: 2xx / 3xx (handler ran without scope gate) —
// would mean the route is silently reachable via bearer without
// scope enforcement.
expect(res.status).toBeGreaterThanOrEqual(400);
// If the route is a declared gated route and the response is a
// 200-range, that's a silent scope-bypass. Hard-fail with a
// useful message.
if (res.status < 400) {
throw new Error(
`Non-v1 gated route ${route.method} ${route.path} returned ${res.status} to a bearer key — scope gate did not fire. Required scopes: ${decision.scopes.join(", ")}`,
);
}
});
}
});
describe("fail-open semantics (legacy empty-scope keys)", () => {
// Canonical routes across distinct groups. Picked so each sampled
// route is:
// - in ROUTES with needsScopes=true, isWebhookExempt=false
// - NOT a public route (so the scope gate is a real gate)
// - spread across group buckets (admin, customers, plans, billing,
// features, analytics, api-keys, platform)
const canonicalProbes: Array<{
method: string;
path: string;
label: string;
}> = [
{ method: "GET", path: "/v1/customers", label: "v1 customers list" },
{ method: "GET", path: "/v1/products", label: "v1 plans list" },
{ method: "POST", path: "/v1/attach", label: "v1 billing attach" },
{ method: "GET", path: "/v1/features", label: "v1 features list" },
{ method: "POST", path: "/v1/track", label: "v1 balances write" },
{ method: "POST", path: "/v1/setup_payment", label: "v1 billing write" },
{
method: "GET",
path: "/v1/platform/organizations",
label: "v1 platform read",
},
{ method: "POST", path: "/v1/query", label: "v1 query/analytics" },
];
for (const probe of canonicalProbes) {
test(`${probe.method} ${probe.path} (${probe.label}) does not 403 with insufficient_scopes for legacy empty-scope key`, async () => {
const res = await app.fetch(
new Request(`http://localhost${synthesisePath(probe.path)}`, {
method: probe.method,
headers: {
Authorization: `Bearer ${emptyScopesKey}`,
"Content-Type": "application/json",
},
body: hasBody(probe.method) ? "{}" : undefined,
}),
);
if (res.status === 403) {
const body = (await res.json().catch(() => ({}))) as {
code?: string;
};
// A 403 is fine as long as it's NOT the scope middleware
// firing — that would mean the fail-open path regressed.
expect(body.code).not.toBe("insufficient_scopes");
} else {
expect(res.status).not.toBe(403);
}
});
}
});
describe("public routes are publicly accessible (scope gate does not fire)", () => {
const publicRoutes = (ROUTES as readonly unknown[] as readonly Route[])
.filter((r) => r.needsScopes && !r.isWebhookExempt)
.filter((r) => {
const decision =
SCOPE_DECISIONS[`${r.method}|${r.path}|${r.handlerName}`];
return (
decision &&
decision.decision === "decided" &&
decision.scopes.includes("public")
);
});
for (const route of publicRoutes) {
test(`${route.method} ${route.path} (${route.handlerName}) does not 403 with insufficient_scopes when unauthenticated`, async () => {
const res = await app.fetch(
new Request(`http://localhost${synthesisePath(route.path)}`, {
method: route.method,
headers: {
"Content-Type": "application/json",
},
body: hasBody(route.method) ? "{}" : undefined,
}),
);
if (res.status === 403) {
const body = (await res.json().catch(() => ({}))) as {
code?: string;
};
expect(body.code).not.toBe("insufficient_scopes");
} else {
expect(res.status).not.toBe(403);
}
});
}
});
});