UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

60 lines (59 loc) • 2.48 kB
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 };