diff --git a/extensions/device-pair/index.ts b/extensions/device-pair/index.ts index 7af30d6135c..04892e092ad 100644 --- a/extensions/device-pair/index.ts +++ b/extensions/device-pair/index.ts @@ -37,45 +37,49 @@ type ResolveAuthResult = { }; function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null { - const trimmed = raw.trim(); - if (!trimmed) { + const candidate = raw.trim(); + if (!candidate) { return null; } - try { - const parsed = new URL(trimmed); - const scheme = parsed.protocol.replace(":", ""); - if (!scheme) { - return null; - } - const resolvedScheme = scheme === "http" ? "ws" : scheme === "https" ? "wss" : scheme; - if (resolvedScheme !== "ws" && resolvedScheme !== "wss") { - return null; - } - const host = parsed.hostname; - if (!host) { - return null; - } - const port = parsed.port ? `:${parsed.port}` : ""; - return `${resolvedScheme}://${host}${port}`; - } catch { - // Fall through to host:port parsing. + const parsedUrl = parseNormalizedGatewayUrl(candidate); + if (parsedUrl) { + return parsedUrl; } + const hostPort = candidate.split("/", 1)[0]?.trim() ?? ""; + return hostPort ? `${schemeFallback}://${hostPort}` : null; +} - const withoutPath = trimmed.split("/")[0] ?? ""; - if (!withoutPath) { +function parseNormalizedGatewayUrl(raw: string): string | null { + try { + const parsed = new URL(raw); + const scheme = parsed.protocol.slice(0, -1); + const normalizedScheme = scheme === "http" ? "ws" : scheme === "https" ? "wss" : scheme; + if (!(normalizedScheme === "ws" || normalizedScheme === "wss")) { + return null; + } + if (!parsed.hostname) { + return null; + } + return `${normalizedScheme}://${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`; + } catch { return null; } - return `${schemeFallback}://${withoutPath}`; +} + +function parsePositiveInteger(raw: string | undefined): number | null { + if (!raw) { + return null; + } + const parsed = Number.parseInt(raw, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } function resolveGatewayPort(cfg: OpenClawPluginApi["config"]): number { - const envRaw = - process.env.OPENCLAW_GATEWAY_PORT?.trim() || process.env.CLAWDBOT_GATEWAY_PORT?.trim(); - if (envRaw) { - const parsed = Number.parseInt(envRaw, 10); - if (Number.isFinite(parsed) && parsed > 0) { - return parsed; - } + const envPort = + parsePositiveInteger(process.env.OPENCLAW_GATEWAY_PORT?.trim()) ?? + parsePositiveInteger(process.env.CLAWDBOT_GATEWAY_PORT?.trim()); + if (envPort) { + return envPort; } const configPort = cfg.gateway?.port; if (typeof configPort === "number" && Number.isFinite(configPort) && configPort > 0) { @@ -215,25 +219,20 @@ function parsePossiblyNoisyJsonObject(raw: string): Record { function resolveAuth(cfg: OpenClawPluginApi["config"]): ResolveAuthResult { const mode = cfg.gateway?.auth?.mode; const token = - process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || - process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() || - cfg.gateway?.auth?.token?.trim(); + pickFirstDefined([ + process.env.OPENCLAW_GATEWAY_TOKEN, + process.env.CLAWDBOT_GATEWAY_TOKEN, + cfg.gateway?.auth?.token, + ]) ?? undefined; const password = - process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || - process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || - cfg.gateway?.auth?.password?.trim(); + pickFirstDefined([ + process.env.OPENCLAW_GATEWAY_PASSWORD, + process.env.CLAWDBOT_GATEWAY_PASSWORD, + cfg.gateway?.auth?.password, + ]) ?? undefined; - if (mode === "password") { - if (!password) { - return { error: "Gateway auth is set to password, but no password is configured." }; - } - return { password, label: "password" }; - } - if (mode === "token") { - if (!token) { - return { error: "Gateway auth is set to token, but no token is configured." }; - } - return { token, label: "token" }; + if (mode === "token" || mode === "password") { + return resolveRequiredAuth(mode, { token, password }); } if (token) { return { token, label: "token" }; @@ -244,6 +243,30 @@ function resolveAuth(cfg: OpenClawPluginApi["config"]): ResolveAuthResult { return { error: "Gateway auth is not configured (no token or password)." }; } +function pickFirstDefined(candidates: Array): string | null { + for (const value of candidates) { + const trimmed = value?.trim(); + if (trimmed) { + return trimmed; + } + } + return null; +} + +function resolveRequiredAuth( + mode: "token" | "password", + values: { token?: string; password?: string }, +): ResolveAuthResult { + if (mode === "token") { + return values.token + ? { token: values.token, label: "token" } + : { error: "Gateway auth is set to token, but no token is configured." }; + } + return values.password + ? { password: values.password, label: "password" } + : { error: "Gateway auth is set to password, but no password is configured." }; +} + async function resolveGatewayUrl(api: OpenClawPluginApi): Promise { const cfg = api.config; const pluginCfg = (api.pluginConfig ?? {}) as DevicePairPluginConfig;