Skip to content

Commit 71091f9

Browse files
committed
fix(webapp): keep environment rate-limit buckets consistent
Use environment identifiers when displaying remaining API capacity and ignore additional keys tied to deleted projects.
1 parent ce668bf commit 71091f9

4 files changed

Lines changed: 34 additions & 17 deletions

File tree

apps/webapp/app/models/runtimeEnvironment.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,13 +323,14 @@ export async function resolvePrivateApiKeyRateLimitScope(
323323
runtimeEnvironment: {
324324
select: {
325325
id: true,
326+
project: { select: { deletedAt: true } },
326327
organization: { select: { apiRateLimiterConfig: true } },
327328
},
328329
},
329330
},
330331
});
331332

332-
if (!match?.runtimeEnvironment) {
333+
if (!match?.runtimeEnvironment || match.runtimeEnvironment.project.deletedAt) {
333334
return null;
334335
}
335336

apps/webapp/app/presenters/v3/LimitsPresenter.server.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Ratelimit } from "@upstash/ratelimit";
22
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
3-
import { createHash } from "node:crypto";
43
import { env } from "~/env.server";
54
import { getCurrentPlan } from "~/services/platform.v3.server";
65
import {
@@ -90,13 +89,11 @@ export class LimitsPresenter extends BasePresenter {
9089
projectId,
9190
environmentId,
9291
environmentType,
93-
environmentApiKey,
9492
}: {
9593
organizationId: string;
9694
projectId: string;
9795
environmentId: string;
9896
environmentType: RuntimeEnvironmentType;
99-
environmentApiKey: string;
10097
}): Promise<LimitsResult> {
10198
// Get organization with all limit-related fields
10299
const organization = await this._replica.organization.findFirstOrThrow({
@@ -168,10 +165,10 @@ export class LimitsPresenter extends BasePresenter {
168165
where: { organizationId },
169166
});
170167

171-
// Get current rate limit tokens for this environment's API key
168+
// Get current rate limit tokens for this environment's API bucket
172169
const apiRateLimitTokens = await getRateLimitRemainingTokens(
173170
"api",
174-
environmentApiKey,
171+
environmentId,
175172
apiRateLimitConfig
176173
);
177174
// Batch rate limiter uses environment ID directly (not hashed) with a different key prefix
@@ -454,20 +451,14 @@ function resolveBatchConcurrencyConfig(batchConcurrencyConfig?: unknown): {
454451

455452
/**
456453
* Query the current remaining tokens for a rate limiter using the Upstash getRemaining method.
457-
* This uses the same configuration and hashing logic as the rate limit middleware.
454+
* The API limiter uses the environment ID as the bucket identifier for private API keys.
458455
*/
459456
async function getRateLimitRemainingTokens(
460457
keyPrefix: string,
461-
apiKey: string,
458+
identifier: string,
462459
config: RateLimiterConfig
463460
): Promise<number | null> {
464461
try {
465-
// Hash the authorization header the same way the rate limiter does
466-
const authorizationValue = `Bearer ${apiKey}`;
467-
const hash = createHash("sha256");
468-
hash.update(authorizationValue);
469-
const hashedKey = hash.digest("hex");
470-
471462
// Create a Ratelimit instance with the same configuration
472463
const limiter = createLimiterFromConfig(config);
473464
const ratelimit = new Ratelimit({
@@ -478,9 +469,9 @@ async function getRateLimitRemainingTokens(
478469
prefix: `ratelimit:${keyPrefix}`,
479470
});
480471

481-
// Use the getRemaining method to get the current remaining tokens
472+
// Use the same identifier as the API rate-limit middleware.
482473
// getRemaining returns a Promise<number>
483-
const remaining = await ratelimit.getRemaining(hashedKey);
474+
const remaining = await ratelimit.getRemaining(identifier);
484475
return remaining;
485476
} catch (error) {
486477
logger.warn("Failed to get rate limit remaining tokens", {

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
8484
projectId: project.id,
8585
environmentId: environment.id,
8686
environmentType: environment.type,
87-
environmentApiKey: environment.apiKey,
8887
})
8988
);
9089

apps/webapp/test/findEnvironmentByApiKey.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,30 @@ describe("findEnvironmentByApiKey — additional and disabled keys", () => {
399399
).resolves.toMatchObject({ id: environment.id });
400400
}
401401
);
402+
403+
postgresTest("does not resolve additional keys for deleted projects", async ({ prisma }) => {
404+
const { organization, project, user } = await createTestOrgProjectWithMember(prisma);
405+
const environment = await createEnv(prisma, project.id, organization.id, {
406+
type: "PRODUCTION",
407+
});
408+
const additional = generateAdditionalApiKey("PRODUCTION").apiKey;
409+
410+
await prisma.apiKey.create({
411+
data: {
412+
name: "Deleted project key",
413+
keyHash: hashApiKey(additional),
414+
lastFour: additional.slice(-4),
415+
runtimeEnvironmentId: environment.id,
416+
createdByUserId: user.id,
417+
presetId: null,
418+
scopes: ["admin"],
419+
},
420+
});
421+
await prisma.project.update({
422+
where: { id: project.id },
423+
data: { deletedAt: new Date() },
424+
});
425+
426+
await expect(resolvePrivateApiKeyRateLimitScope(additional, prisma)).resolves.toBeNull();
427+
});
402428
});

0 commit comments

Comments
 (0)