fix(serve-http): make OAuth /token rate limit configurable via env (#3114)

Adds GBRAIN_OAUTH_TOKEN_RATE_LIMIT_MAX and
GBRAIN_OAUTH_TOKEN_RATE_LIMIT_WINDOW_MS to tune the /token
client_credentials limiter (default unchanged: 50 req / 15 min).
Invalid, zero, or negative values fall back to the default.

Takeover of #2501 (mechanical rebase onto master after #2625 shifted
the surrounding context in serve-http.ts). Fixes #2463.

Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: techtony2018 <techtony2018@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Time Attakc
2026-07-23 12:29:24 -07:00
committed by GitHub
co-authored by Garry Tan techtony2018 Claude Fable 5
parent 66f4cb6d82
commit 58606cc924
2 changed files with 68 additions and 3 deletions
+22 -3
View File
@@ -113,6 +113,24 @@ export function shouldSuppressBootstrapPrint(opts: {
return !opts.isTty;
}
export type OAuthTokenRateLimitConfig = {
windowMs: number;
max: number;
};
function parsePositiveIntEnv(value: string | undefined, fallback: number): number {
if (value === undefined) return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
export function resolveOAuthTokenRateLimit(env: NodeJS.ProcessEnv = process.env): OAuthTokenRateLimitConfig {
return {
windowMs: parsePositiveIntEnv(env.GBRAIN_OAUTH_TOKEN_RATE_LIMIT_WINDOW_MS, 15 * 60 * 1000),
max: parsePositiveIntEnv(env.GBRAIN_OAUTH_TOKEN_RATE_LIMIT_MAX, 50),
};
}
export type ProbeHealthResult =
| { ok: true; status: 200; body: { status: 'ok'; version: string; engine: string; [k: string]: unknown } }
| { ok: false; status: 503; body: { error: 'service_unavailable'; error_description: string } };
@@ -633,12 +651,13 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
// Custom client_credentials handler (before mcpAuthRouter)
// SDK's token handler only supports authorization_code and refresh_token
// ---------------------------------------------------------------------------
const oauthTokenRateLimit = resolveOAuthTokenRateLimit();
const ccRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50,
windowMs: oauthTokenRateLimit.windowMs,
max: oauthTokenRateLimit.max,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'too_many_requests', error_description: 'Rate limit exceeded. Try again in 15 minutes.' },
message: { error: 'too_many_requests', error_description: 'Rate limit exceeded. Try again later.' },
});
// Magic-link rate limiter: 10 requests/min/IP. The bootstrap token is
@@ -0,0 +1,46 @@
/**
* Tests for resolveOAuthTokenRateLimit() in src/commands/serve-http.ts.
*
* The /token client_credentials limiter should keep the historical default
* while letting operators tune busy remote MCP hosts without patching source.
*/
import { describe, test, expect } from 'bun:test';
import { resolveOAuthTokenRateLimit } from '../src/commands/serve-http.ts';
describe('resolveOAuthTokenRateLimit', () => {
test('unset env keeps the historical 50 requests per 15 minutes default', () => {
expect(resolveOAuthTokenRateLimit({})).toEqual({
windowMs: 15 * 60 * 1000,
max: 50,
});
});
test('env overrides allow a busy host to use 200 requests per minute', () => {
expect(resolveOAuthTokenRateLimit({
GBRAIN_OAUTH_TOKEN_RATE_LIMIT_WINDOW_MS: '60000',
GBRAIN_OAUTH_TOKEN_RATE_LIMIT_MAX: '200',
})).toEqual({
windowMs: 60_000,
max: 200,
});
});
test('blank, non-numeric, zero, and negative values fall back safely', () => {
expect(resolveOAuthTokenRateLimit({
GBRAIN_OAUTH_TOKEN_RATE_LIMIT_WINDOW_MS: '',
GBRAIN_OAUTH_TOKEN_RATE_LIMIT_MAX: 'nope',
})).toEqual({
windowMs: 15 * 60 * 1000,
max: 50,
});
expect(resolveOAuthTokenRateLimit({
GBRAIN_OAUTH_TOKEN_RATE_LIMIT_WINDOW_MS: '0',
GBRAIN_OAUTH_TOKEN_RATE_LIMIT_MAX: '-10',
})).toEqual({
windowMs: 15 * 60 * 1000,
max: 50,
});
});
});