openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
65 lines (64 loc) • 2.58 kB
JavaScript
import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js";
import { t as normalizeControlPlaneIdentityPart } from "./control-plane-identity-CDhSlyN5.js";
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 = normalizeControlPlaneIdentityPart(client?.connect?.device?.id, "unknown-device");
const clientIp = normalizeControlPlaneIdentityPart(client?.clientIp, "unknown-ip");
if (deviceId === "unknown-device" && clientIp === "unknown-ip") {
const connId = normalizeControlPlaneIdentityPart(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)}`;
const bucket = controlPlaneBuckets.get(key);
if (!bucket || nowMs - bucket.windowStartMs >= 6e4) {
if (!controlPlaneBuckets.has(key) && controlPlaneBuckets.size >= CONTROL_PLANE_BUCKET_MAX_ENTRIES) pruneMapToMaxSize(controlPlaneBuckets, 9999);
controlPlaneBuckets.set(key, {
count: 1,
windowStartMs: nowMs
});
return {
allowed: true,
retryAfterMs: 0,
remaining: 29,
key
};
}
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 };