refactor: dedupe gateway helper readers

This commit is contained in:
Peter Steinberger
2026-04-07 08:31:17 +01:00
parent 5eb6921a18
commit ce7ef626b8
8 changed files with 34 additions and 60 deletions

View File

@@ -17,7 +17,11 @@ import {
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
type SsrFPolicy,
} from "openclaw/plugin-sdk/ssrf-runtime";
import { isRecord, resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
import {
isRecord,
normalizeOptionalString,
resolveUserPath,
} from "openclaw/plugin-sdk/text-runtime";
const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
@@ -87,15 +91,6 @@ export function _setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard |
comfyFetchGuard = impl ?? fetchWithSsrFGuard;
}
function readConfigString(config: ComfyProviderConfig, key: string): string | undefined {
const value = config[key];
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
function readConfigBoolean(config: ComfyProviderConfig, key: string): boolean | undefined {
const value = config[key];
return typeof value === "boolean" ? value : undefined;
@@ -161,11 +156,11 @@ export function getComfyCapabilityConfig(
}
export function resolveComfyMode(config: ComfyProviderConfig): ComfyMode {
return readConfigString(config, "mode") === "cloud" ? "cloud" : "local";
return normalizeOptionalString(config.mode) === "cloud" ? "cloud" : "local";
}
function getRequiredConfigString(config: ComfyProviderConfig, key: string): string {
const value = readConfigString(config, key);
const value = normalizeOptionalString(config[key]);
if (!value) {
throw new Error(`models.providers.comfy.${key} is required`);
}
@@ -180,7 +175,7 @@ function resolveComfyWorkflowSource(config: ComfyProviderConfig): {
if (isRecord(workflow)) {
return { workflow: structuredClone(workflow) };
}
const workflowPath = readConfigString(config, "workflowPath");
const workflowPath = normalizeOptionalString(config.workflowPath);
return { workflowPath };
}
@@ -230,7 +225,7 @@ function resolveComfyNetworkPolicy(params: {
return {};
}
const hostname = parsed.hostname.trim().toLowerCase();
const hostname = normalizeOptionalString(parsed.hostname)?.toLowerCase() ?? "";
if (!hostname || !params.allowPrivateNetwork || !isPrivateOrLoopbackHost(hostname)) {
return {};
}

View File

@@ -17,10 +17,6 @@ type GatewayConnectionDetailResolvers = {
resolveGatewayPort?: (cfg?: OpenClawConfig, env?: NodeJS.ProcessEnv) => number;
};
function trimToUndefined(value: string | undefined): string | undefined {
return normalizeOptionalString(value);
}
export function buildGatewayConnectionDetailsWithResolvers(
options: {
config?: OpenClawConfig;
@@ -46,7 +42,7 @@ export function buildGatewayConnectionDetailsWithResolvers(
const cliUrlOverride = normalizeOptionalString(options.url);
const envUrlOverride = cliUrlOverride
? undefined
: trimToUndefined(process.env.OPENCLAW_GATEWAY_URL);
: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_URL);
const urlOverride = cliUrlOverride ?? envUrlOverride;
const remoteUrl = normalizeOptionalString(remote?.url);
const remoteMisconfigured = isRemoteMode && !urlOverride && !remoteUrl;

View File

@@ -20,6 +20,7 @@ import {
type ApnsRelayConfig,
} from "../infra/push-apns.js";
import { roleScopesAllow } from "../shared/operator-scope-compat.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
const APPROVALS_SCOPE = "operator.approvals";
const OPERATOR_ROLE = "operator";
@@ -47,7 +48,7 @@ type ApprovalDeliveryState = {
};
function isIosPlatform(platform: string | undefined): boolean {
const normalized = platform?.trim().toLowerCase() ?? "";
const normalized = normalizeOptionalString(platform)?.toLowerCase() ?? "";
return normalized.startsWith("ios") || normalized.startsWith("ipados");
}

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
export const GATEWAY_CLIENT_IDS = {
WEBCHAT_UI: "webchat-ui",
CONTROL_UI: "openclaw-control-ui",
@@ -53,7 +55,7 @@ const GATEWAY_CLIENT_ID_SET = new Set<GatewayClientId>(Object.values(GATEWAY_CLI
const GATEWAY_CLIENT_MODE_SET = new Set<GatewayClientMode>(Object.values(GATEWAY_CLIENT_MODES));
export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined {
const normalized = raw?.trim().toLowerCase();
const normalized = normalizeOptionalString(raw)?.toLowerCase();
if (!normalized) {
return undefined;
}
@@ -67,7 +69,7 @@ export function normalizeGatewayClientName(raw?: string | null): GatewayClientNa
}
export function normalizeGatewayClientMode(raw?: string | null): GatewayClientMode | undefined {
const normalized = raw?.trim().toLowerCase();
const normalized = normalizeOptionalString(raw)?.toLowerCase();
if (!normalized) {
return undefined;
}

View File

@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "../config/types.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type SecretInputUnresolvedReasonStyle = "generic" | "detailed"; // pragma: allowlist secret
export type ConfiguredSecretInputSource =
@@ -9,14 +10,6 @@ export type ConfiguredSecretInputSource =
| "secretRef" // pragma: allowlist secret
| "fallback";
function trimToUndefined(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function buildUnresolvedReason(params: {
path: string;
style: SecretInputUnresolvedReasonStyle;
@@ -48,7 +41,7 @@ export async function resolveConfiguredSecretInputString(params: {
defaults: params.config.secrets?.defaults,
});
if (!ref) {
return { value: trimToUndefined(params.value) };
return { value: normalizeOptionalString(params.value) };
}
const refLabel = `${ref.source}:${ref.provider}:${ref.id}`;
@@ -68,8 +61,8 @@ export async function resolveConfiguredSecretInputString(params: {
}),
};
}
const trimmed = resolvedValue.trim();
if (trimmed.length === 0) {
const trimmed = normalizeOptionalString(resolvedValue);
if (!trimmed) {
return {
unresolvedRefReason: buildUnresolvedReason({
path: params.path,
@@ -109,7 +102,7 @@ export async function resolveConfiguredSecretInputWithFallback(params: {
value: params.value,
defaults: params.config.secrets?.defaults,
});
const configValue = !ref ? trimToUndefined(params.value) : undefined;
const configValue = !ref ? normalizeOptionalString(params.value) : undefined;
if (configValue) {
return {
value: configValue,

View File

@@ -35,6 +35,7 @@ import { enqueueSystemEvent } from "../infra/system-events.js";
import { getChildLogger } from "../logging.js";
import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type GatewayCronState = {
cron: CronService;
@@ -44,14 +45,6 @@ export type GatewayCronState = {
const CRON_WEBHOOK_TIMEOUT_MS = 10_000;
function trimToOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function redactWebhookUrl(url: string): string {
try {
const parsed = new URL(url);
@@ -71,7 +64,7 @@ function resolveCronWebhookTarget(params: {
legacyNotify?: boolean;
legacyWebhook?: string;
}): CronWebhookTarget | null {
const mode = params.delivery?.mode?.trim().toLowerCase();
const mode = normalizeOptionalString(params.delivery?.mode)?.toLowerCase();
if (mode === "webhook") {
const url = normalizeHttpWebhookUrl(params.delivery?.to);
return url ? { url, source: "delivery" } : null;
@@ -314,7 +307,7 @@ export function buildGatewayCronService(params: {
},
sendCronFailureAlert: async ({ job, text, channel, to, mode, accountId }) => {
const { agentId, cfg: runtimeConfig } = resolveCronAgent(job.agentId);
const webhookToken = trimToOptionalString(params.cfg.cron?.webhookToken);
const webhookToken = normalizeOptionalString(params.cfg.cron?.webhookToken);
// Webhook mode requires a URL - fail closed if missing
if (mode === "webhook" && !to) {
@@ -375,8 +368,8 @@ export function buildGatewayCronService(params: {
onEvent: (evt) => {
params.broadcast("cron", evt, { dropIfSlow: true });
if (evt.action === "finished") {
const webhookToken = trimToOptionalString(params.cfg.cron?.webhookToken);
const legacyWebhook = trimToOptionalString(params.cfg.cron?.webhook);
const webhookToken = normalizeOptionalString(params.cfg.cron?.webhookToken);
const legacyWebhook = normalizeOptionalString(params.cfg.cron?.webhook);
const job = cron.getJob(evt.jobId);
const legacyNotify = (job as { notify?: unknown } | undefined)?.notify === true;
const webhookTarget = resolveCronWebhookTarget({

View File

@@ -1,7 +1,8 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { NodeRegistry } from "./node-registry.js";
const isMobilePlatform = (platform: unknown): boolean => {
const p = typeof platform === "string" ? platform.trim().toLowerCase() : "";
const p = normalizeOptionalString(platform)?.toLowerCase() ?? "";
if (!p) {
return false;
}

View File

@@ -1,4 +1,5 @@
import type { IncomingMessage } from "node:http";
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
import {
AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN,
AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,
@@ -40,19 +41,11 @@ export type ConnectAuthDecision = {
authMethod: GatewayAuthResult["method"];
};
function trimToUndefined(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function resolveSharedConnectAuth(
connectAuth: HandshakeConnectAuth | null | undefined,
): { token?: string; password?: string } | undefined {
const token = trimToUndefined(connectAuth?.token);
const password = trimToUndefined(connectAuth?.password);
const token = normalizeOptionalString(connectAuth?.token);
const password = normalizeOptionalString(connectAuth?.password);
if (!token && !password) {
return undefined;
}
@@ -63,11 +56,11 @@ function resolveDeviceTokenCandidate(connectAuth: HandshakeConnectAuth | null |
token?: string;
source?: DeviceTokenCandidateSource;
} {
const explicitDeviceToken = trimToUndefined(connectAuth?.deviceToken);
const explicitDeviceToken = normalizeOptionalString(connectAuth?.deviceToken);
if (explicitDeviceToken) {
return { token: explicitDeviceToken, source: "explicit-device-token" };
}
const fallbackToken = trimToUndefined(connectAuth?.token);
const fallbackToken = normalizeOptionalString(connectAuth?.token);
if (!fallbackToken) {
return {};
}
@@ -77,7 +70,7 @@ function resolveDeviceTokenCandidate(connectAuth: HandshakeConnectAuth | null |
function resolveBootstrapTokenCandidate(
connectAuth: HandshakeConnectAuth | null | undefined,
): string | undefined {
return trimToUndefined(connectAuth?.bootstrapToken);
return normalizeOptionalString(connectAuth?.bootstrapToken);
}
export async function resolveConnectAuthState(params: {