Something went wrong. Try again.
A local-first event pipeline for independent agents, built on Jazz.
Something went wrong. Try again.
3.1 kB · 82 lines
TypeScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283export interface RateLimitDecision { allowed: boolean; retryAfterSeconds: number;}
interface Bucket { startedAt: number; count: number;}
export interface FixedWindowRateLimiterOptions { maxRequests: number; windowMs: number; maxKeys: number; now?: () => number;}
export class FixedWindowRateLimiter { private readonly buckets = new Map<string, Bucket>(); private readonly now: () => number;
constructor(private readonly options: FixedWindowRateLimiterOptions) { if (!Number.isSafeInteger(options.maxRequests) || options.maxRequests < 1) throw new Error("Rate limit maxRequests must be positive"); if (!Number.isSafeInteger(options.windowMs) || options.windowMs < 1_000) throw new Error("Rate limit windowMs must be at least one second"); if (!Number.isSafeInteger(options.maxKeys) || options.maxKeys < 1 || options.maxKeys > 100_000) throw new Error("Rate limit maxKeys is invalid"); this.now = options.now ?? Date.now; }
check(key: string): RateLimitDecision { const now = this.now(); this.prune(now); let bucket = this.buckets.get(key); if (!bucket) { if (this.buckets.size >= this.options.maxKeys) { return { allowed: false, retryAfterSeconds: Math.ceil(this.options.windowMs / 1_000) }; } bucket = { startedAt: now, count: 0 }; this.buckets.set(key, bucket); } const elapsed = now - bucket.startedAt; if (elapsed >= this.options.windowMs) { bucket.startedAt = now; bucket.count = 0; } bucket.count += 1; const retryAfterSeconds = Math.max(1, Math.ceil((bucket.startedAt + this.options.windowMs - now) / 1_000)); return { allowed: bucket.count <= this.options.maxRequests, retryAfterSeconds }; }
private prune(now: number): void { for (const [key, bucket] of this.buckets) { if (now - bucket.startedAt >= this.options.windowMs) this.buckets.delete(key); } }}
export interface OAuthRouteRateLimiter { login(clientKey: string): RateLimitDecision; callback(clientKey: string): RateLimitDecision;}
export function createOAuthRouteRateLimiter(now: () => number = Date.now): OAuthRouteRateLimiter { const loginGlobal = new FixedWindowRateLimiter({ maxRequests: 30, windowMs: 10 * 60_000, maxKeys: 1, now }); const loginClient = new FixedWindowRateLimiter({ maxRequests: 6, windowMs: 10 * 60_000, maxKeys: 1_024, now }); const callbackGlobal = new FixedWindowRateLimiter({ maxRequests: 60, windowMs: 10 * 60_000, maxKeys: 1, now }); const callbackClient = new FixedWindowRateLimiter({ maxRequests: 12, windowMs: 10 * 60_000, maxKeys: 1_024, now }); return { login(clientKey) { return combine(loginGlobal.check("global"), loginClient.check(clientKey)); }, callback(clientKey) { return combine(callbackGlobal.check("global"), callbackClient.check(clientKey)); }, };}
function combine(global: RateLimitDecision, client: RateLimitDecision): RateLimitDecision { return { allowed: global.allowed && client.allowed, retryAfterSeconds: Math.max(global.retryAfterSeconds, client.retryAfterSeconds), };}