UNPKG

@vigilbase/cli

Version:

Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.

80 lines (79 loc) • 3.11 kB
/** * Cloudflare request budget (spec §F5). * * Cloudflare allows 1,200 API requests per user per rolling 5 minutes, shared * with the user's dashboard; going over blocks every API call for 5 minutes. * The default spends at most 900 per rolling 5 minutes (configurable 100 to * 1,100). A token bucket with capacity `burst` that refills at * `(limit − burst) / 300 s` never admits more than `limit` calls in any * 300-second window: at most `burst` from the full bucket plus the refill. * Local `--version` and `--dry-run` calls are free. */ export const DEFAULT_REQUESTS_PER_5MIN = 900; export const MIN_REQUESTS_PER_5MIN = 100; export const MAX_REQUESTS_PER_5MIN = 1_100; export const BUDGET_BURST = 8; export const BUDGET_WINDOW_MS = 300_000; export const systemClock = { now: () => Date.now(), sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), }; export function parseRequestBudget(raw) { if (raw === undefined) return DEFAULT_REQUESTS_PER_5MIN; const value = Number(raw); if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < MIN_REQUESTS_PER_5MIN || value > MAX_REQUESTS_PER_5MIN) { throw new Error(`--max-requests-per-5min must be an integer between ${MIN_REQUESTS_PER_5MIN} and ${MAX_REQUESTS_PER_5MIN}.`); } return value; } export class RequestBudget { limit; clock; burst; tokens; updatedAt; refillPerMs; queue = Promise.resolve(); constructor(limit = DEFAULT_REQUESTS_PER_5MIN, clock = systemClock, burst = BUDGET_BURST) { this.limit = limit; this.clock = clock; this.burst = burst; if (!(limit > burst)) throw new Error('The request budget must exceed the burst size.'); this.tokens = burst; this.updatedAt = clock.now(); this.refillPerMs = (limit - burst) / BUDGET_WINDOW_MS; } refill() { const now = this.clock.now(); this.tokens = Math.min(this.burst, this.tokens + (now - this.updatedAt) * this.refillPerMs); this.updatedAt = now; } /** Tokens available now (fractional). */ available() { this.refill(); return this.tokens; } /** Wait for one request slot. Callers are served in order. */ acquire() { const next = this.queue.then(async () => { this.refill(); while (this.tokens < 1) { await this.clock.sleep(Math.max(1, Math.ceil((1 - this.tokens) / this.refillPerMs))); this.refill(); } this.tokens -= 1; }); this.queue = next.catch(() => undefined); return next; } /** Seconds the bucket needs for `requests` calls, ignoring network latency. */ estimateSeconds(requests) { return estimateBudgetSeconds(this.limit, requests, this.burst); } } /** Seconds a full bucket needs to admit `requests` calls at `limit` per 5 minutes. */ export function estimateBudgetSeconds(limit, requests, burst = BUDGET_BURST) { return Math.max(0, ((requests - burst) * BUDGET_WINDOW_MS) / (limit - burst) / 1000); }