refactor: dedupe auth session readers

This commit is contained in:
Peter Steinberger
2026-04-07 05:29:28 +01:00
parent 21802f750f
commit 9869941c06
12 changed files with 43 additions and 27 deletions

View File

@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { loadAuthProfileStoreForRuntime } from "./auth-profiles/store.js";
import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js";
import {
@@ -137,7 +138,7 @@ export async function resolveCliAuthEpoch(params: {
authProfileId?: string;
}): Promise<string | undefined> {
const provider = params.provider.trim();
const authProfileId = params.authProfileId?.trim() || undefined;
const authProfileId = normalizeOptionalString(params.authProfileId);
const parts: string[] = [];
const localFingerprint = getLocalCliCredentialFingerprint(provider);

View File

@@ -10,6 +10,7 @@ import { sleepWithAbort } from "../../infra/backoff.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { sanitizeForLog } from "../../terminal/ansi.js";
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";
import { resolveOpenClawAgentDir } from "../agent-paths.js";
@@ -118,7 +119,7 @@ function backfillSessionKey(params: {
sessionKey?: string;
agentId?: string;
}): string | undefined {
const trimmed = params.sessionKey?.trim() || undefined;
const trimmed = normalizeOptionalString(params.sessionKey);
if (trimmed) {
return trimmed;
}
@@ -126,7 +127,7 @@ function backfillSessionKey(params: {
return undefined;
}
try {
const resolved = params.agentId?.trim()
const resolved = normalizeOptionalString(params.agentId)
? resolveStoredSessionKeyForSessionId({
cfg: params.config,
sessionId: params.sessionId,
@@ -136,7 +137,7 @@ function backfillSessionKey(params: {
cfg: params.config,
sessionId: params.sessionId,
});
return resolved.sessionKey?.trim() || undefined;
return normalizeOptionalString(resolved.sessionKey);
} catch (err) {
log.warn(
`[backfillSessionKey] Failed to resolve sessionKey for sessionId=${redactRunIdentifier(sanitizeForLog(params.sessionId))}: ${formatErrorMessage(err)}`,

View File

@@ -15,6 +15,7 @@ import {
logSessionStateChange,
} from "../../logging/diagnostic.js";
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
export type EmbeddedPiQueueHandle = {
kind?: "embedded";
@@ -57,7 +58,8 @@ const embeddedRunState = resolveGlobalSingleton(EMBEDDED_RUN_STATE_KEY, () => ({
modelSwitchRequests: new Map<string, EmbeddedRunModelSwitchRequest>(),
}));
const ACTIVE_EMBEDDED_RUNS =
embeddedRunState.activeRuns ?? (embeddedRunState.activeRuns = new Map<string, EmbeddedPiQueueHandle>());
embeddedRunState.activeRuns ??
(embeddedRunState.activeRuns = new Map<string, EmbeddedPiQueueHandle>());
const ACTIVE_EMBEDDED_RUN_SNAPSHOTS =
embeddedRunState.snapshots ??
(embeddedRunState.snapshots = new Map<string, ActiveEmbeddedRunSnapshot>());
@@ -65,7 +67,8 @@ const ACTIVE_EMBEDDED_RUN_SESSION_IDS_BY_KEY =
embeddedRunState.sessionIdsByKey ??
(embeddedRunState.sessionIdsByKey = new Map<string, string>());
const EMBEDDED_RUN_WAITERS =
embeddedRunState.waiters ?? (embeddedRunState.waiters = new Map<string, Set<EmbeddedRunWaiter>>());
embeddedRunState.waiters ??
(embeddedRunState.waiters = new Map<string, Set<EmbeddedRunWaiter>>());
const EMBEDDED_RUN_MODEL_SWITCH_REQUESTS =
embeddedRunState.modelSwitchRequests ??
(embeddedRunState.modelSwitchRequests = new Map<string, EmbeddedRunModelSwitchRequest>());
@@ -242,8 +245,10 @@ export function requestEmbeddedRunModelSwitch(
EMBEDDED_RUN_MODEL_SWITCH_REQUESTS.set(normalizedSessionId, {
provider,
model,
authProfileId: request.authProfileId?.trim() || undefined,
authProfileIdSource: request.authProfileId?.trim() ? request.authProfileIdSource : undefined,
authProfileId: normalizeOptionalString(request.authProfileId),
authProfileIdSource: normalizeOptionalString(request.authProfileId)
? request.authProfileIdSource
: undefined,
});
diag.debug(
`model switch requested: sessionId=${normalizedSessionId} provider=${provider} model=${model}`,

View File

@@ -12,6 +12,7 @@ import {
type ResolvedBrowserConfig,
} from "../../plugin-sdk/browser-profiles.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { BROWSER_BRIDGES } from "./browser-bridges.js";
import { computeSandboxBrowserConfigHash } from "./config-hash.js";
import { resolveSandboxBrowserDockerCreateConfig } from "./config.js";
@@ -295,8 +296,8 @@ export async function ensureSandboxBrowser(params: {
? resolveProfile(existing.bridge.state.resolved, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME)
: null;
let desiredAuthToken = params.bridgeAuth?.token?.trim() || undefined;
let desiredAuthPassword = params.bridgeAuth?.password?.trim() || undefined;
let desiredAuthToken = normalizeOptionalString(params.bridgeAuth?.token);
let desiredAuthPassword = normalizeOptionalString(params.bridgeAuth?.password);
if (!desiredAuthToken && !desiredAuthPassword) {
// Always require auth for the sandbox bridge server, even if gateway auth
// mode doesn't produce a shared secret (e.g. trusted-proxy).

View File

@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
export const NOVNC_PASSWORD_ENV_KEY = "OPENCLAW_BROWSER_NOVNC_PASSWORD"; // pragma: allowlist secret
const NOVNC_TOKEN_TTL_MS = 60 * 1000;
@@ -65,7 +66,7 @@ export function issueNoVncObserverToken(params: {
const token = crypto.randomBytes(24).toString("hex");
NO_VNC_OBSERVER_TOKENS.set(token, {
noVncPort: params.noVncPort,
password: params.password?.trim() || undefined,
password: normalizeOptionalString(params.password),
expiresAt: now + Math.max(1, params.ttlMs ?? NOVNC_TOKEN_TTL_MS),
});
return token;

View File

@@ -8,6 +8,7 @@ import {
signalVerifiedGatewayPidSync,
} from "../../infra/gateway-processes.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { theme } from "../../terminal/theme.js";
import { formatCliCommand } from "../command-format.js";
import { recoverInstalledLaunchAgent } from "./launchd-recovery.js";
@@ -77,8 +78,8 @@ async function assertUnmanagedGatewayRestartEnabled(port: number): Promise<void>
const probe = await probeGateway({
url: `${scheme}://127.0.0.1:${port}`,
auth: {
token: process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined,
password: process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || undefined,
token: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_TOKEN),
password: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_PASSWORD),
},
timeoutMs: 1_000,
}).catch(() => null);

View File

@@ -14,6 +14,7 @@ import {
import { normalizeFingerprint } from "../infra/tls/fingerprint.js";
import { rawDataToString } from "../infra/ws.js";
import { logDebug, logError } from "../logger.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
@@ -526,7 +527,7 @@ export class GatewayClient {
err instanceof GatewayClientRequestError ? readConnectErrorDetailCode(err.details) : null;
const shouldRetryWithDeviceToken = this.shouldRetryWithStoredDeviceToken({
error: err,
explicitGatewayToken: this.opts.token?.trim() || undefined,
explicitGatewayToken: normalizeOptionalString(this.opts.token),
resolvedDeviceToken,
storedToken: storedToken ?? undefined,
});
@@ -661,10 +662,10 @@ export class GatewayClient {
}
private selectConnectAuth(role: string): SelectedConnectAuth {
const explicitGatewayToken = this.opts.token?.trim() || undefined;
const explicitBootstrapToken = this.opts.bootstrapToken?.trim() || undefined;
const explicitDeviceToken = this.opts.deviceToken?.trim() || undefined;
const authPassword = this.opts.password?.trim() || undefined;
const explicitGatewayToken = normalizeOptionalString(this.opts.token);
const explicitBootstrapToken = normalizeOptionalString(this.opts.bootstrapToken);
const explicitDeviceToken = normalizeOptionalString(this.opts.deviceToken);
const authPassword = normalizeOptionalString(this.opts.password);
const storedAuth = this.loadStoredDeviceAuth(role);
const storedToken = storedAuth?.token ?? null;
const storedScopes = storedAuth?.scopes;

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
type ExplicitGatewayAuth,
isGatewaySecretRefUnavailableError,
@@ -28,8 +29,8 @@ function resolveExplicitProbeAuth(explicitAuth?: ExplicitGatewayAuth): {
token?: string;
password?: string;
} {
const token = explicitAuth?.token?.trim() || undefined;
const password = explicitAuth?.password?.trim() || undefined;
const token = normalizeOptionalString(explicitAuth?.token);
const password = normalizeOptionalString(explicitAuth?.password);
return { token, password };
}

View File

@@ -1,5 +1,6 @@
import type { ReplyPayload } from "../auto-reply/types.js";
import type { InteractiveReply, InteractiveReplyButton } from "../interactive/payload.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
describeNativeExecApprovalClientSetup,
listNativeExecApprovalClientLabels,
@@ -348,9 +349,9 @@ export function buildExecApprovalPendingReplyPayload(
approvalId: params.approvalId,
approvalSlug: params.approvalSlug,
approvalKind: "exec",
agentId: params.agentId?.trim() || undefined,
agentId: normalizeOptionalString(params.agentId),
allowedDecisions,
sessionKey: params.sessionKey?.trim() || undefined,
sessionKey: normalizeOptionalString(params.sessionKey),
},
},
};

View File

@@ -49,6 +49,7 @@ import {
toAgentStoreSessionKey,
} from "../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { escapeRegExp } from "../utils.js";
import { formatErrorMessage, hasErrnoCode } from "./errors.js";
import { isWithinActiveHours } from "./heartbeat-active-hours.js";
@@ -1191,7 +1192,7 @@ export function startHeartbeatRunner(opts: {
const reason = params?.reason;
const requestedAgentId = params?.agentId ? normalizeAgentId(params.agentId) : undefined;
const requestedSessionKey = params?.sessionKey?.trim() || undefined;
const requestedSessionKey = normalizeOptionalString(params?.sessionKey);
const isInterval = reason === "interval";
const startedAt = Date.now();
const now = startedAt;

View File

@@ -19,6 +19,7 @@ import {
isRfc1918Ipv4Address,
parseCanonicalIpAddress,
} from "../shared/net/ip.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
export type PairingSetupPayload = {
@@ -214,11 +215,11 @@ function pickTailnetIPv4(
}
function resolveGatewayTokenFromEnv(env: NodeJS.ProcessEnv): string | undefined {
return env.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
return normalizeOptionalString(env.OPENCLAW_GATEWAY_TOKEN);
}
function resolveGatewayPasswordFromEnv(env: NodeJS.ProcessEnv): string | undefined {
return env.OPENCLAW_GATEWAY_PASSWORD?.trim() || undefined;
return normalizeOptionalString(env.OPENCLAW_GATEWAY_PASSWORD);
}
function resolvePairingSetupAuthLabel(

View File

@@ -9,6 +9,7 @@ import {
type PluginApprovalRequest,
type PluginApprovalResolved,
} from "../infra/plugin-approvals.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
const DEFAULT_ALLOWED_DECISIONS = ["allow-once", "allow-always", "deny"] as const;
@@ -34,9 +35,9 @@ export function buildApprovalPendingReplyPayload(params: {
approvalId: params.approvalId,
approvalSlug: params.approvalSlug,
approvalKind: params.approvalKind ?? "exec",
agentId: params.agentId?.trim() || undefined,
agentId: normalizeOptionalString(params.agentId),
allowedDecisions,
sessionKey: params.sessionKey?.trim() || undefined,
sessionKey: normalizeOptionalString(params.sessionKey),
state: "pending",
},
...params.channelData,