/** * User ID resolver for backend reporting. * * The backend `/offload/v1/store` endpoint keys state by `X-User-Id`. * If the plugin config does not provide one, we fall back to the host's * primary non-loopback IPv4 address so each machine still maps to a * stable identifier. Falls back further to `"unknown-host"` on failure. * * The resolved value is cached on first read; IP lookup is cheap but * callers invoke this per request so caching keeps the hot path clean. */ import * as os from "node:os"; let _cachedUserId: string | null = null; let _cachedSource: "config" | "ip" | "fallback" | null = null; /** * Find the first non-loopback, non-internal IPv4 address on the host. * Returns null when the host has no external-facing interface. */ function detectLocalIPv4(): string | null { try { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { const addrs = interfaces[name]; if (!addrs) continue; for (const addr of addrs) { // node >= 18 exposes `family` as "IPv4" / "IPv6"; older versions use 4 / 6. const isV4 = addr.family === "IPv4" || (addr.family as unknown as number) === 4; if (isV4 && !addr.internal && typeof addr.address === "string") { return addr.address; } } } } catch { /* ignore — detection best-effort */ } return null; } /** * Resolve the effective user ID. Priority: * 1. `configuredUserId` from plugin config (trimmed, non-empty) * 2. Primary non-loopback IPv4 address of the host * 3. Literal `"unknown-host"` fallback * * Result and source are cached — subsequent calls are O(1). */ export function resolveUserId(configuredUserId?: string | null): string { if (_cachedUserId) return _cachedUserId; const trimmed = typeof configuredUserId === "string" ? configuredUserId.trim() : ""; if (trimmed) { _cachedUserId = trimmed; _cachedSource = "config"; return _cachedUserId; } const ip = detectLocalIPv4(); if (ip) { _cachedUserId = ip; _cachedSource = "ip"; return _cachedUserId; } _cachedUserId = "unknown-host"; _cachedSource = "fallback"; return _cachedUserId; } /** Returns how the currently-cached user id was resolved (or null if unresolved). */ export function getUserIdSource(): "config" | "ip" | "fallback" | null { return _cachedSource; } /** Testing hook: wipe the cache so the next resolve() re-evaluates. */ export function _resetUserIdCacheForTests(): void { _cachedUserId = null; _cachedSource = null; }