refactor: dedupe infra lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 12:45:04 +01:00
parent 8e4eaec394
commit 9716f970a3
15 changed files with 48 additions and 31 deletions

View File

@@ -1,4 +1,5 @@
import { runCommandWithTimeout } from "../process/exec.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { isTailnetIPv4 } from "./tailnet.js";
import { resolveWideAreaDiscoveryDomain } from "./widearea-dns.js";
@@ -279,7 +280,7 @@ function parseDnsSdResolve(stdout: string, instanceName: string): GatewayBonjour
beacon.gatewayPort = parseIntOrNull(txt.gatewayPort);
beacon.sshPort = parseIntOrNull(txt.sshPort);
if (txt.gatewayTls) {
const raw = txt.gatewayTls.trim().toLowerCase();
const raw = normalizeOptionalLowercaseString(txt.gatewayTls);
beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
}
if (txt.gatewayTlsSha256) {
@@ -457,7 +458,7 @@ async function discoverWideAreaViaTailnetDns(
cliPath: txtMap.cliPath || undefined,
};
if (txtMap.gatewayTls) {
const raw = txtMap.gatewayTls.trim().toLowerCase();
const raw = normalizeOptionalLowercaseString(txtMap.gatewayTls);
beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
}
if (txtMap.gatewayTlsSha256) {
@@ -541,7 +542,7 @@ function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] {
current.gatewayPort = parseIntOrNull(txt.gatewayPort);
current.sshPort = parseIntOrNull(txt.sshPort);
if (txt.gatewayTls) {
const raw = txt.gatewayTls.trim().toLowerCase();
const raw = normalizeOptionalLowercaseString(txt.gatewayTls);
current.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
}
if (txt.gatewayTlsSha256) {

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { matchesExecAllowlistPattern } from "./exec-allowlist-pattern.js";
import type { ExecAllowlistEntry } from "./exec-approvals.js";
import { resolveExecWrapperTrustPlan } from "./exec-wrapper-trust-plan.js";
@@ -342,7 +343,9 @@ export function matchAllowlist(
// Use the caller-supplied target platform rather than process.platform so that
// a Linux gateway evaluating a Windows node command applies argPattern correctly.
const effectivePlatform = platform ?? process.platform;
const useArgPattern = String(effectivePlatform).trim().toLowerCase().startsWith("win");
const useArgPattern = normalizeLowercaseStringOrEmpty(String(effectivePlatform)).startsWith(
"win",
);
let pathOnlyMatch: ExecAllowlistEntry | null = null;
for (const entry of entries) {
const pattern = entry.pattern?.trim();

View File

@@ -1,4 +1,5 @@
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
// Build a dynamic prompt for cron events by embedding the actual event content.
// This ensures the model sees the reminder text directly instead of relying on
@@ -72,7 +73,7 @@ function isHeartbeatAckEvent(evt: string): boolean {
}
function isHeartbeatNoiseEvent(evt: string): boolean {
const lower = evt.trim().toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(evt);
if (!lower) {
return false;
}

View File

@@ -1,5 +1,7 @@
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
export function normalizeHostname(hostname: string): string {
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, "");
if (normalized.startsWith("[") && normalized.endsWith("]")) {
return normalized.slice(1, -1);
}

View File

@@ -120,10 +120,7 @@ function looksLikeUnsupportedIpv4Literal(address: string): boolean {
// Returns true for private/internal and special-use non-global addresses.
export function isPrivateIpAddress(address: string, policy?: SsrFPolicy): boolean {
let normalized = address.trim().toLowerCase();
if (normalized.startsWith("[") && normalized.endsWith("]")) {
normalized = normalized.slice(1, -1);
}
const normalized = normalizeHostname(address);
if (!normalized) {
return false;
}

View File

@@ -1,8 +1,11 @@
import { formatCliCommand } from "../cli/command-format.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import type { PortListener, PortListenerKind, PortUsage } from "./ports-types.js";
export function classifyPortListener(listener: PortListener, port: number): PortListenerKind {
const raw = `${listener.commandLine ?? ""} ${listener.command ?? ""}`.trim().toLowerCase();
const raw = normalizeLowercaseStringOrEmpty(
`${listener.commandLine ?? ""} ${listener.command ?? ""}`,
);
if (raw.includes("openclaw")) {
return "gateway";
}
@@ -34,7 +37,7 @@ function parseListenerAddress(address: string): { host: string; port: number } |
if (lastColon <= 0 || lastColon >= normalized.length - 1) {
return null;
}
const host = normalized.slice(0, lastColon).trim().toLowerCase();
const host = normalizeLowercaseStringOrEmpty(normalized.slice(0, lastColon));
const portToken = normalized.slice(lastColon + 1).trim();
if (!/^\d+$/.test(portToken)) {
return null;

View File

@@ -7,6 +7,7 @@ import {
type DeviceIdentity,
} from "./device-identity.js";
import { formatErrorMessage } from "./errors.js";
import { normalizeHostname } from "./net/hostname.js";
export type ApnsRelayPushType = "alert" | "background";
@@ -74,7 +75,7 @@ function readAllowHttp(value: string | undefined): boolean {
}
function isLoopbackRelayHostname(hostname: string): boolean {
const normalized = hostname.trim().toLowerCase();
const normalized = normalizeHostname(hostname);
return (
normalized === "localhost" ||
normalized === "::1" ||

View File

@@ -1,4 +1,5 @@
import process from "node:process";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { restoreTerminalState } from "../terminal/restore.js";
import {
collectErrorGraphCandidates,
@@ -238,7 +239,7 @@ export function isTransientNetworkError(err: unknown): boolean {
continue;
}
const rawMessage = (candidate as { message?: unknown }).message;
const message = typeof rawMessage === "string" ? rawMessage.toLowerCase().trim() : "";
const message = normalizeLowercaseStringOrEmpty(rawMessage);
if (!message) {
continue;
}
@@ -297,7 +298,7 @@ export function isTransientSqliteError(err: unknown): boolean {
(candidate as { errstr?: unknown }).errstr,
];
for (const rawMessage of messageParts) {
const message = typeof rawMessage === "string" ? rawMessage.toLowerCase().trim() : "";
const message = normalizeLowercaseStringOrEmpty(rawMessage);
if (!message) {
continue;
}

View File

@@ -6,6 +6,7 @@ import { materializeGatewayAuthSecretRefs } from "../gateway/auth-config-utils.j
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "../gateway/auth-mode-policy.js";
import { isLoopbackHost, isSecureWebSocketUrl } from "../gateway/net.js";
import { issueDeviceBootstrapToken } from "../infra/device-bootstrap.js";
import { normalizeHostname } from "../infra/net/hostname.js";
import {
pickMatchingExternalInterfaceAddress,
safeNetworkInterfaces,
@@ -78,7 +79,7 @@ function describeSecureMobilePairingFix(source?: string): string {
}
function isPrivateLanHostname(host: string): boolean {
const normalized = host.trim().toLowerCase().replace(/\.+$/, "");
const normalized = normalizeHostname(host);
if (!normalized) {
return false;
}

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
/** Read loose boolean params from tool input that may arrive as booleans or "true"/"false" strings. */
export function readBooleanParam(
params: Record<string, unknown>,
@@ -7,14 +9,12 @@ export function readBooleanParam(
if (typeof raw === "boolean") {
return raw;
}
if (typeof raw === "string") {
const trimmed = raw.trim().toLowerCase();
if (trimmed === "true") {
return true;
}
if (trimmed === "false") {
return false;
}
const normalized = normalizeOptionalLowercaseString(raw);
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
return undefined;
}

View File

@@ -8,6 +8,7 @@ import {
requestBodyErrorToText,
} from "../infra/http-body.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import type { FixedWindowRateLimiter } from "./webhook-memory-guards.js";
export type WebhookBodyReadProfile = "pre-auth" | "post-auth";
@@ -144,7 +145,7 @@ export function isJsonContentType(value: string | string[] | undefined): boolean
if (!first) {
return false;
}
const mediaType = first.split(";", 1)[0]?.trim().toLowerCase();
const mediaType = normalizeOptionalLowercaseString(first.split(";", 1)[0]);
return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
}

View File

@@ -14,7 +14,10 @@ import { writeJsonAtomic } from "../infra/json-files.js";
import { type ConversationRef } from "../infra/outbound/session-binding-service.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveGlobalMap, resolveGlobalSingleton } from "../shared/global-singleton.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { getActivePluginRegistry } from "./runtime.js";
import type {
PluginConversationBinding,
@@ -155,7 +158,7 @@ function resolveApprovalsPath(): string {
}
function normalizeChannel(value: string): string {
return value.trim().toLowerCase();
return normalizeOptionalLowercaseString(value) ?? "";
}
function normalizeConversation(params: PluginBindingConversation): PluginBindingConversation {

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import {
normalizePluginInteractiveNamespace,
resolvePluginInteractiveMatch,
@@ -49,7 +50,7 @@ export function registerPluginInteractiveHandler(
interactiveHandlers.set(key, {
...registration,
namespace,
channel: registration.channel.trim().toLowerCase(),
channel: normalizeOptionalLowercaseString(registration.channel) ?? "",
pluginId,
pluginName: opts?.pluginName,
pluginRoot: opts?.pluginRoot,

View File

@@ -1,5 +1,7 @@
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
export function toPluginInteractiveRegistryKey(channel: string, namespace: string): string {
return `${channel.trim().toLowerCase()}:${namespace.trim()}`;
return `${normalizeOptionalLowercaseString(channel) ?? ""}:${namespace.trim()}`;
}
export function normalizePluginInteractiveNamespace(namespace: string): string {

View File

@@ -1,7 +1,7 @@
import JSON5 from "json5";
import { LEGACY_MANIFEST_KEYS, MANIFEST_KEY } from "../compat/legacy-names.js";
import { parseBooleanValue } from "../utils/boolean.js";
import { readStringValue } from "./string-coerce.js";
import { normalizeOptionalLowercaseString, readStringValue } from "./string-coerce.js";
import { normalizeCsvOrLooseStringList } from "./string-normalization.js";
export function normalizeStringList(input: unknown): string[] {
@@ -105,7 +105,7 @@ export function parseOpenClawManifestInstallBase(
const raw = input as Record<string, unknown>;
const kindRaw =
typeof raw.kind === "string" ? raw.kind : typeof raw.type === "string" ? raw.type : "";
const kind = kindRaw.trim().toLowerCase();
const kind = normalizeOptionalLowercaseString(kindRaw) ?? "";
if (!allowedKinds.includes(kind)) {
return undefined;
}