diff --git a/src/gateway/control-plane-audit.ts b/src/gateway/control-plane-audit.ts index 2f0ff7c21afb..e420587b3539 100644 --- a/src/gateway/control-plane-audit.ts +++ b/src/gateway/control-plane-audit.ts @@ -1,5 +1,6 @@ // Gateway control-plane audit helpers. // Extracts stable actor identity and compact changed-path summaries for audit logs. +import { normalizeControlPlaneIdentityPart } from "./control-plane-identity.js"; import type { GatewayClient } from "./server-methods/types.js"; /** Stable actor fields included in control-plane audit and rate-limit logs. */ @@ -10,21 +11,13 @@ export type ControlPlaneActor = { connId: string; }; -function normalizePart(value: unknown, fallback: string): string { - if (typeof value !== "string") { - return fallback; - } - const normalized = value.trim(); - return normalized.length > 0 ? normalized : fallback; -} - /** Extracts audit identity from a possibly missing or partially connected client. */ export function resolveControlPlaneActor(client: GatewayClient | null): ControlPlaneActor { return { - actor: normalizePart(client?.connect?.client?.id, "unknown-actor"), - deviceId: normalizePart(client?.connect?.device?.id, "unknown-device"), - clientIp: normalizePart(client?.clientIp, "unknown-ip"), - connId: normalizePart(client?.connId, "unknown-conn"), + actor: normalizeControlPlaneIdentityPart(client?.connect?.client?.id, "unknown-actor"), + deviceId: normalizeControlPlaneIdentityPart(client?.connect?.device?.id, "unknown-device"), + clientIp: normalizeControlPlaneIdentityPart(client?.clientIp, "unknown-ip"), + connId: normalizeControlPlaneIdentityPart(client?.connId, "unknown-conn"), }; } diff --git a/src/gateway/control-plane-identity.ts b/src/gateway/control-plane-identity.ts new file mode 100644 index 000000000000..a1e54ad66d9d --- /dev/null +++ b/src/gateway/control-plane-identity.ts @@ -0,0 +1,8 @@ +/** Normalizes an optional control-plane identity field without creating empty keys. */ +export function normalizeControlPlaneIdentityPart(value: unknown, fallback: string): string { + if (typeof value !== "string") { + return fallback; + } + const normalized = value.trim(); + return normalized.length > 0 ? normalized : fallback; +} diff --git a/src/gateway/control-plane-rate-limit.ts b/src/gateway/control-plane-rate-limit.ts index f2123564b847..bde2c5f07280 100644 --- a/src/gateway/control-plane-rate-limit.ts +++ b/src/gateway/control-plane-rate-limit.ts @@ -1,5 +1,6 @@ // Control-plane rate limiting bounds write-side RPC attempts per device/IP and // caps bucket growth against unique-key memory pressure. +import { normalizeControlPlaneIdentityPart } from "./control-plane-identity.js"; import type { GatewayClient } from "./server-methods/types.js"; const CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS = 3; @@ -16,21 +17,13 @@ type Bucket = { const controlPlaneBuckets = new Map(); -function normalizePart(value: unknown, fallback: string): string { - if (typeof value !== "string") { - return fallback; - } - const normalized = value.trim(); - return normalized.length > 0 ? normalized : fallback; -} - /** Builds a stable throttle key while avoiding shared fallback buckets for anonymous clients. */ export function resolveControlPlaneRateLimitKey(client: GatewayClient | null): string { - const deviceId = normalizePart(client?.connect?.device?.id, "unknown-device"); - const clientIp = normalizePart(client?.clientIp, "unknown-ip"); + const deviceId = normalizeControlPlaneIdentityPart(client?.connect?.device?.id, "unknown-device"); + const clientIp = normalizeControlPlaneIdentityPart(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, ""); + const connId = normalizeControlPlaneIdentityPart(client?.connId, ""); if (connId) { return `${deviceId}|${clientIp}|conn=${connId}`; }