@@ -8,6 +8,73 @@ import type { Duration } from "./rateLimiter.server";
88
99const BATCH_STREAM_ITEMS_PATH = / ^ \/ a p i \/ v 3 \/ b a t c h e s \/ ( [ ^ / ] + ) \/ i t e m s $ / ;
1010
11+ // Rate-limit key for a delegated (agent/PAT-minted) JWT. Its token value rotates every
12+ // turn, so keying on the token would hand each turn a fresh bucket. Key on env+acting-user
13+ // so the agent's traffic shares one bucket across turns. The `jwt-actor:` prefix keeps it
14+ // off PRIVATE-key buckets, which key on the bare environment id.
15+ export function jwtActorRateLimitIdentifier ( environmentId : string , actorSub : string ) : string {
16+ return `jwt-actor:${ environmentId } :${ actorSub } ` ;
17+ }
18+
19+ // The per-request bucket decision for the API limiter. Exported so the branch below
20+ // (a delegated JWT keys on env+acting-user, everything else keeps its prior key) is
21+ // testable without standing up the middleware and its Redis.
22+ export async function resolveApiRateLimitOverride (
23+ authorizationValue : string
24+ ) : Promise < { config ?: unknown ; identifier ?: string } | undefined > {
25+ const rawApiKey = authorizationValue . replace ( / ^ B e a r e r / , "" ) ;
26+
27+ if ( rawApiKey . startsWith ( "tr_" ) ) {
28+ const scope = await resolvePrivateApiKeyRateLimitScope ( rawApiKey ) ;
29+
30+ if ( ! scope ) {
31+ return ;
32+ }
33+
34+ return {
35+ config : scope . apiRateLimiterConfig ,
36+ identifier : scope . environmentId ,
37+ } ;
38+ }
39+
40+ const authenticatedEnv = await authenticateAuthorizationHeader ( authorizationValue , {
41+ allowPublicKey : true ,
42+ allowJWT : true ,
43+ } ) ;
44+
45+ if ( ! authenticatedEnv || ! authenticatedEnv . ok ) {
46+ return ;
47+ }
48+
49+ if ( authenticatedEnv . type === "PUBLIC_JWT" ) {
50+ const config = {
51+ type : "fixedWindow" ,
52+ window : env . API_RATE_LIMIT_JWT_WINDOW ,
53+ tokens : env . API_RATE_LIMIT_JWT_TOKENS ,
54+ } as const ;
55+
56+ // A delegated JWT (agent/PAT-minted) shares one bucket per env+acting-user across turns.
57+ // A browser realtime JWT carries no `act`, so it keeps the hashed-token fallback.
58+ if ( authenticatedEnv . actor ?. sub ) {
59+ return {
60+ config,
61+ identifier : jwtActorRateLimitIdentifier (
62+ authenticatedEnv . environment . id ,
63+ authenticatedEnv . actor . sub
64+ ) ,
65+ } ;
66+ }
67+
68+ return { config } ;
69+ }
70+
71+ return {
72+ config : authenticatedEnv . environment . organization . apiRateLimiterConfig ,
73+ // Public keys are browser-distributed, so keep them on per-key buckets.
74+ identifier : authenticatedEnv . type === "PRIVATE" ? authenticatedEnv . environment . id : undefined ,
75+ } ;
76+ }
77+
1178export const apiRateLimiter = authorizationRateLimitMiddleware ( {
1279 redis : {
1380 port : env . RATE_LIMIT_REDIS_PORT ,
@@ -29,47 +96,7 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
2996 stale : 60_000 * 20 , // Date is stale after 20 minutes
3097 maxItems : 1000 ,
3198 } ,
32- limiterConfigOverride : async ( authorizationValue ) => {
33- const rawApiKey = authorizationValue . replace ( / ^ B e a r e r / , "" ) ;
34-
35- if ( rawApiKey . startsWith ( "tr_" ) ) {
36- const scope = await resolvePrivateApiKeyRateLimitScope ( rawApiKey ) ;
37-
38- if ( ! scope ) {
39- return ;
40- }
41-
42- return {
43- config : scope . apiRateLimiterConfig ,
44- identifier : scope . environmentId ,
45- } ;
46- }
47-
48- const authenticatedEnv = await authenticateAuthorizationHeader ( authorizationValue , {
49- allowPublicKey : true ,
50- allowJWT : true ,
51- } ) ;
52-
53- if ( ! authenticatedEnv || ! authenticatedEnv . ok ) {
54- return ;
55- }
56-
57- if ( authenticatedEnv . type === "PUBLIC_JWT" ) {
58- return {
59- config : {
60- type : "fixedWindow" ,
61- window : env . API_RATE_LIMIT_JWT_WINDOW ,
62- tokens : env . API_RATE_LIMIT_JWT_TOKENS ,
63- } ,
64- } ;
65- }
66-
67- return {
68- config : authenticatedEnv . environment . organization . apiRateLimiterConfig ,
69- // Public keys are browser-distributed, so keep them on per-key buckets.
70- identifier : authenticatedEnv . type === "PRIVATE" ? authenticatedEnv . environment . id : undefined ,
71- } ;
72- } ,
99+ limiterConfigOverride : resolveApiRateLimitOverride ,
73100 pathMatchers : [ / ^ \/ a p i / ] ,
74101 // Allow /api/v1/tasks/:id/callback/:secret
75102 pathWhiteList : [
0 commit comments