02de2e693f
Most deploy-flow API calls shared the general per-environment rate limit bucket with all of that environment's runtime traffic, so an org with heavy API usage could intermittently 429 its own deploys; the `/api/v*/deployments` endpoints themselves were fully exempt from rate limits as a stopgap ([#2774](https://github.com/triggerdotdev/trigger.dev/pull/2774)), which promised a dedicated limiter as the follow-up. This is that follow-up: the whole deploy-flow group now runs on its own budget, separate from runtime API limits. ### Design A new `deploymentRateLimiter` covers every endpoint the deploy flow depends on: the `/api/v*/deployments` group, the env API key exchange (`/api/v1/projects/:ref/:env`), build-time env var resolution and sync (`/envvars`, `/envvars/:slug/import`), preview branches, `/api/v1/remote-build-provider-status` and `/api/v1/artifacts`. The general API limiter whitelists the same shared path list, so exactly one limiter applies to each path and the two can't drift apart. Buckets are keyed per environment for environment API keys and per token for the PAT-authenticated phase of a CLI deploy (whoami, key exchange, branches). The deploy budget is controlled via the `DEPLOYMENT_RATE_LIMIT_*` env vars.
54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
import { env } from "~/env.server";
|
|
import { resolvePrivateApiKeyRateLimitScope } from "~/models/runtimeEnvironment.server";
|
|
import { authorizationRateLimitMiddleware } from "./authorizationRateLimitMiddleware.server";
|
|
import { deploymentApiPaths } from "./deploymentApiPaths.server";
|
|
import type { Duration } from "./rateLimiter.server";
|
|
|
|
export const deploymentRateLimiter = authorizationRateLimitMiddleware({
|
|
redis: {
|
|
port: env.RATE_LIMIT_REDIS_PORT,
|
|
host: env.RATE_LIMIT_REDIS_HOST,
|
|
username: env.RATE_LIMIT_REDIS_USERNAME,
|
|
password: env.RATE_LIMIT_REDIS_PASSWORD,
|
|
tlsDisabled: env.RATE_LIMIT_REDIS_TLS_DISABLED === "true",
|
|
clusterMode: env.RATE_LIMIT_REDIS_CLUSTER_MODE_ENABLED === "1",
|
|
},
|
|
keyPrefix: "deployment",
|
|
defaultLimiter: {
|
|
type: "tokenBucket",
|
|
refillRate: env.DEPLOYMENT_RATE_LIMIT_REFILL_RATE,
|
|
interval: env.DEPLOYMENT_RATE_LIMIT_REFILL_INTERVAL as Duration,
|
|
maxTokens: env.DEPLOYMENT_RATE_LIMIT_MAX,
|
|
},
|
|
limiterCache: {
|
|
fresh: 60_000 * 10,
|
|
stale: 60_000 * 20,
|
|
maxItems: 1000,
|
|
},
|
|
limiterConfigOverride: async (authorizationValue) => {
|
|
const rawApiKey = authorizationValue.replace(/^Bearer /, "");
|
|
|
|
if (!rawApiKey.startsWith("tr_")) {
|
|
return;
|
|
}
|
|
|
|
const scope = await resolvePrivateApiKeyRateLimitScope(rawApiKey);
|
|
|
|
if (!scope) {
|
|
return;
|
|
}
|
|
|
|
// Identifier only: the org's apiRateLimiterConfig governs the general API
|
|
// limiter, not the deploy budget.
|
|
return {
|
|
identifier: scope.environmentId,
|
|
};
|
|
},
|
|
pathMatchers: deploymentApiPaths,
|
|
log: {
|
|
rejections: env.DEPLOYMENT_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
|
|
requests: env.DEPLOYMENT_RATE_LIMIT_REQUEST_LOGS_ENABLED === "1",
|
|
limiter: env.DEPLOYMENT_RATE_LIMIT_LIMITER_LOGS_ENABLED === "1",
|
|
},
|
|
});
|