openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
60 lines (59 loc) • 2.48 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs";
const CONTROL_PLANE_RATE_LIMIT_WINDOW_MS = 6e4;
const CONTROL_PLANE_BUCKET_MAX_STALE_MS = 3e5;
/** Hard cap to prevent memory DoS from rapid unique-key injection (CWE-400). */
const CONTROL_PLANE_BUCKET_MAX_ENTRIES = 1e4;
const controlPlaneBuckets = /* @__PURE__ */ new Map();
/** Builds a stable throttle key while avoiding shared fallback buckets for anonymous clients. */
function resolveControlPlaneRateLimitKey(client) {
const deviceId = normalizeOptionalString(client?.connect?.device?.id) ?? "unknown-device";
const clientIp = normalizeOptionalString(client?.clientIp) ?? "unknown-ip";
if (deviceId === "unknown-device" && clientIp === "unknown-ip") {
const connId = normalizeOptionalString(client?.connId);
if (connId) return `${deviceId}|${clientIp}|conn=${connId}`;
}
return `${deviceId}|${clientIp}`;
}
/** Consumes one write budget unit and reports retry state for gateway error responses. */
function consumeControlPlaneWriteBudget(params) {
const nowMs = params.nowMs ?? Date.now();
const key = `${params.method}|${resolveControlPlaneRateLimitKey(params.client)}`;
let bucket = controlPlaneBuckets.get(key);
if (!bucket || nowMs - bucket.windowStartMs >= 6e4) {
if (!controlPlaneBuckets.has(key) && controlPlaneBuckets.size >= CONTROL_PLANE_BUCKET_MAX_ENTRIES) pruneMapToMaxSize(controlPlaneBuckets, 9999);
bucket = {
count: 0,
windowStartMs: nowMs
};
controlPlaneBuckets.set(key, bucket);
}
if (bucket.count >= 30) return {
allowed: false,
retryAfterMs: Math.max(0, bucket.windowStartMs + CONTROL_PLANE_RATE_LIMIT_WINDOW_MS - nowMs),
remaining: 0,
key
};
bucket.count += 1;
return {
allowed: true,
retryAfterMs: 0,
remaining: Math.max(0, 30 - bucket.count),
key
};
}
/**
* Remove buckets whose rate-limit window expired more than
* CONTROL_PLANE_BUCKET_MAX_STALE_MS ago. Called periodically
* by the gateway maintenance timer to prevent unbounded growth.
*/
function pruneStaleControlPlaneBuckets(nowMs = Date.now()) {
let pruned = 0;
for (const [key, bucket] of controlPlaneBuckets) if (nowMs - bucket.windowStartMs > CONTROL_PLANE_BUCKET_MAX_STALE_MS) {
controlPlaneBuckets.delete(key);
pruned += 1;
}
return pruned;
}
//#endregion
export { consumeControlPlaneWriteBudget as n, pruneStaleControlPlaneBuckets as r, CONTROL_PLANE_RATE_LIMIT_WINDOW_MS as t };