@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
80 lines (79 loc) • 3.11 kB
JavaScript
/**
* 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);
}