mirror of https://github.com/openclaw/openclaw.git
87 lines
2.3 KiB
TypeScript
87 lines
2.3 KiB
TypeScript
import type { GatewayClient } from "./server-methods/types.js";
|
|
|
|
const CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS = 3;
|
|
const CONTROL_PLANE_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
|
|
type Bucket = {
|
|
count: number;
|
|
windowStartMs: number;
|
|
};
|
|
|
|
const controlPlaneBuckets = new Map<string, Bucket>();
|
|
|
|
function normalizePart(value: unknown, fallback: string): string {
|
|
if (typeof value !== "string") {
|
|
return fallback;
|
|
}
|
|
const normalized = value.trim();
|
|
return normalized.length > 0 ? normalized : fallback;
|
|
}
|
|
|
|
export function resolveControlPlaneRateLimitKey(client: GatewayClient | null): string {
|
|
const deviceId = normalizePart(client?.connect?.device?.id, "unknown-device");
|
|
const clientIp = normalizePart(client?.clientIp, "unknown-ip");
|
|
if (deviceId === "unknown-device" && clientIp === "unknown-ip") {
|
|
// Last-resort fallback: avoid cross-client contention when upstream identity is missing.
|
|
const connId = normalizePart(client?.connId, "");
|
|
if (connId) {
|
|
return `${deviceId}|${clientIp}|conn=${connId}`;
|
|
}
|
|
}
|
|
return `${deviceId}|${clientIp}`;
|
|
}
|
|
|
|
export function consumeControlPlaneWriteBudget(params: {
|
|
client: GatewayClient | null;
|
|
nowMs?: number;
|
|
}): {
|
|
allowed: boolean;
|
|
retryAfterMs: number;
|
|
remaining: number;
|
|
key: string;
|
|
} {
|
|
const nowMs = params.nowMs ?? Date.now();
|
|
const key = resolveControlPlaneRateLimitKey(params.client);
|
|
const bucket = controlPlaneBuckets.get(key);
|
|
|
|
if (!bucket || nowMs - bucket.windowStartMs >= CONTROL_PLANE_RATE_LIMIT_WINDOW_MS) {
|
|
controlPlaneBuckets.set(key, {
|
|
count: 1,
|
|
windowStartMs: nowMs,
|
|
});
|
|
return {
|
|
allowed: true,
|
|
retryAfterMs: 0,
|
|
remaining: CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS - 1,
|
|
key,
|
|
};
|
|
}
|
|
|
|
if (bucket.count >= CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS) {
|
|
const retryAfterMs = Math.max(
|
|
0,
|
|
bucket.windowStartMs + CONTROL_PLANE_RATE_LIMIT_WINDOW_MS - nowMs,
|
|
);
|
|
return {
|
|
allowed: false,
|
|
retryAfterMs,
|
|
remaining: 0,
|
|
key,
|
|
};
|
|
}
|
|
|
|
bucket.count += 1;
|
|
return {
|
|
allowed: true,
|
|
retryAfterMs: 0,
|
|
remaining: Math.max(0, CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS - bucket.count),
|
|
key,
|
|
};
|
|
}
|
|
|
|
export const __testing = {
|
|
resetControlPlaneRateLimitState() {
|
|
controlPlaneBuckets.clear();
|
|
},
|
|
};
|