-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(webapp): share rate limit bucket across additional API keys per environment #4508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c345394
e9f4728
a2651e5
0eae5e1
8c6e59b
191b9b7
a4971d2
e5f3de0
cd2a0c0
eb0e8f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: fix | ||
| --- | ||
|
|
||
| API rate limits now apply per environment, so creating extra API keys no longer increases how many requests an environment can make. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -301,6 +301,89 @@ export async function findEnvironmentByApiKeyWithResolution( | |
| return resolveEnvironmentByApiKey(apiKey, branchName, tx, additionalApiKeyLookupEnabled); | ||
| } | ||
|
|
||
| export type PrivateApiKeyRateLimitScope = { | ||
| environmentId: string; | ||
| apiRateLimiterConfig: unknown; | ||
| }; | ||
|
|
||
| export async function resolvePrivateApiKeyRateLimitScope( | ||
| apiKey: string, | ||
| tx: PrismaClientOrTransaction = $replica | ||
| ): Promise<PrivateApiKeyRateLimitScope | null> { | ||
| const now = new Date(); | ||
|
|
||
| if (isAdditionalApiKey(apiKey)) { | ||
| const match = await tx.apiKey.findFirst({ | ||
| where: { | ||
| keyHash: hashApiKey(apiKey), | ||
| revokedAt: null, | ||
| OR: [{ expiresAt: null }, { expiresAt: { gt: now } }], | ||
| }, | ||
| select: { | ||
| runtimeEnvironment: { | ||
| select: { | ||
| id: true, | ||
| project: { select: { deletedAt: true } }, | ||
| organization: { select: { apiRateLimiterConfig: true } }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| if (!match?.runtimeEnvironment || match.runtimeEnvironment.project.deletedAt) { | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| environmentId: match.runtimeEnvironment.id, | ||
| apiRateLimiterConfig: match.runtimeEnvironment.organization.apiRateLimiterConfig, | ||
| }; | ||
| } | ||
|
|
||
| const environment = await tx.runtimeEnvironment.findFirst({ | ||
| where: { apiKey }, | ||
| select: { | ||
| id: true, | ||
| project: { select: { deletedAt: true } }, | ||
| organization: { select: { apiRateLimiterConfig: true } }, | ||
| }, | ||
| }); | ||
|
Comment on lines
+343
to
+350
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Preview/dev branch keys resolve to different buckets than the parent root key The identifier is resolved purely from the raw key: the root path does Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| if (environment) { | ||
| if (environment.project.deletedAt) { | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| environmentId: environment.id, | ||
| apiRateLimiterConfig: environment.organization.apiRateLimiterConfig, | ||
| }; | ||
| } | ||
|
|
||
| const revokedApiKey = await tx.revokedApiKey.findFirst({ | ||
| where: { apiKey, expiresAt: { gt: now } }, | ||
| select: { | ||
| runtimeEnvironment: { | ||
| select: { | ||
| id: true, | ||
| project: { select: { deletedAt: true } }, | ||
| organization: { select: { apiRateLimiterConfig: true } }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const revokedEnvironment = revokedApiKey?.runtimeEnvironment; | ||
| if (!revokedEnvironment || revokedEnvironment.project.deletedAt) { | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| environmentId: revokedEnvironment.id, | ||
| apiRateLimiterConfig: revokedEnvironment.organization.apiRateLimiterConfig, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @deprecated We don't use public API keys (`pk_*` tokens) anymore — public | ||
| * access goes through public JWTs (see `isPublicJWT` / `validatePublicJwtKey`). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,9 +82,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { | |
| presenter.call({ | ||
| organizationId: project.organizationId, | ||
| projectId: project.id, | ||
| environmentId: environment.id, | ||
| // API traffic for a branch is bucketed on the parent environment id. | ||
| environmentId: environment.parentEnvironmentId ?? environment.id, | ||
|
Comment on lines
+85
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Limits page shows another environment's batch limit and queue numbers when viewing a branch The limits page now looks up all of an environment's numbers under the parent environment ( Why the single environmentId argument is now overloaded
Only the API bucket is keyed on the parent environment id (the middleware resolves the identifier from the root API key, which belongs to the parent — see So passing the parent id makes the batch tokens and queue size (and the concurrency fields read for the engine query) resolve against the parent instead of the branch. Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| environmentType: environment.type, | ||
| environmentApiKey: environment.apiKey, | ||
| }) | ||
| ); | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.