feat(webapp): share rate limit bucket across additional API keys per environment - #4508
Conversation
🦋 Changeset detectedLatest commit: e9ae728 The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
A number passed to `expirationTime` is a Unix timestamp in seconds, not milliseconds as the JSDoc claimed. Following the old docs produced a token that effectively never expired. Also fail loudly when an additional API key reaches a local self-signing fallback. Those keys are not the environment's JWT signing material, so the token would never verify. Every endpoint that returns a public access token sets `x-trigger-jwt`, so this is unreachable today.
The API key policy methods are optional on the plugin-facing controller contract, so `Pick` over it yields optional members that these call sites would have to guard. Both already receive the LazyController singleton, which has substituted its fail-closed defaults, so point them at HostRbacController and keep the call sites guard-free.
Require both the global issuance switch and organization rollout flag before creating additional keys, while leaving existing credentials available for use and revocation. Show nullable creators and identify SDK v4.5.8 as the first compatible public-token version.
Record bounded outcomes for additional key creation, policy preparation, revocation, and public-token minting.
ba817b9 to
316db63
Compare
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
Use environment identifiers when displaying remaining API capacity and ignore additional keys tied to deleted projects.
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
316db63 to
cd2a0c0
Compare
eb0e8f7 to
e9ae728
Compare
| constructor( | ||
| prismaClient: PrismaReplicaClient = $replica, | ||
| rbacController: ApiKeyPolicyPresenter = rbac | ||
| ) { |
There was a problem hiding this comment.
🟡 Newly created or revoked API keys can be missing from the list right after the change
The API keys page now reads its data from a replica database ($replica default at apps/webapp/app/presenters/v3/ApiKeysPresenter.server.ts:22) while the create and revoke actions on the same page write to the primary, so the list shown immediately after a change can still be the pre-change state.
Impact: After creating a key a user may not see it in the table, and after revoking one it may still show as Active until the page is reloaded.
Read-your-writes across primary/replica on the API keys page
Previously ApiKeysPresenter defaulted to the writer client (prisma). This PR changes the default to $replica and, in the same PR, adds mutations on the same route: createEnvironmentApiKey / revokeEnvironmentApiKey (apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx:260-289) write via the primary prisma client and then the route revalidates/redirects straight back into the loader, which calls the presenter. Under any replication lag the just-created key can be absent from keyEnvironment.apiKeys, and a just-revoked key can still have revokedAt: null (so it renders as Active with a Revoke button). The revoke flow uses redirectWithSuccessMessage, so the user sees "API key revoked" next to a row that still looks active.
If the read must stay on the replica, the mutation responses should either carry the new state or the loader should read the mutated rows from the primary.
Prompt for agents
ApiKeysPresenter's default Prisma client was changed from the writer (`prisma`) to the read replica (`$replica`). The same PR adds create/revoke API key mutations on the route that uses this presenter (apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx). Those mutations write through the primary and then immediately trigger a loader revalidation/redirect that reads through the presenter, so under replication lag the newly created key may be missing from the table and a just-revoked key may still render as Active. Decide whether this page needs read-your-writes: either keep the presenter on the writer for the api key rows, re-read the mutated rows from the primary, or have the action response carry the authoritative post-mutation state.
Was this helpful? React with 👍 or 👎 to provide feedback.
What
Rate-limit the API by environment rather than per API key.
Previously the limiter keyed its bucket on the hash of the full
Authorizationheader — one bucket per key. With additional environment API keys (tr_*_sk_*), an environment can mint many keys and each got its own full bucket, so more keys = higher effective rate limit. This collapses all of an environment's keys onto a single shared per-environment bucket, so the ceiling is exactly the configured limit regardless of key mix.How
authorizationRateLimitMiddlewarenow lets the override return{ config?, identifier? }.identifier, when present, is the rate limit bucket key; otherwise it falls back to the hashedAuthorizationheader (unchanged legacy behavior, still used byengineRateLimiterand any unauthenticated fallthrough).apiRateLimiter's override resolves the environment id and uses it as the identifier:isAdditionalApiKey) resolve via a newresolveAdditionalApiKeyRateLimitScope()— a scope-agnostic keyHash → (environmentId, org limiter config) lookup. It is deliberately permissive (restricted keys resolve too) because it's used only for bucketing, never as an auth decision — request auth still goes through the RBAC bearer controller, which enforces scopes. Revoked/expired keys are excluded so they can't hold a bucket warm.authenticateAuthorizationHeaderand key onenvironment.idtoo.Behavior notes
Tests
{ config }return shape.Base:
feat/multi-keys-surface. Closes TRI-12888.