refactor: unify gateway SecretRef auth resolution paths

This commit is contained in:
Peter Steinberger
2026-03-07 21:32:42 +00:00
parent 5f26970200
commit fecca6fd8d
9 changed files with 318 additions and 189 deletions

View File

@@ -12,7 +12,6 @@ import type {
import {
hasConfiguredSecretInput,
normalizeSecretInputString,
resolveSecretInputRef,
} from "../../config/types.secrets.js";
import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js";
import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
@@ -27,6 +26,7 @@ import {
trimToUndefined,
} from "../../gateway/credentials.js";
import { resolveGatewayBindHost } from "../../gateway/net.js";
import { resolveRequiredConfiguredSecretRefInputString } from "../../gateway/resolve-configured-secret-input-string.js";
import {
formatPortDiagnostics,
inspectPortUsage,
@@ -35,8 +35,6 @@ import {
} from "../../infra/ports.js";
import { pickPrimaryTailnetIPv4 } from "../../infra/tailnet.js";
import { loadGatewayTlsRuntime } from "../../infra/tls/gateway.js";
import { secretRefKey } from "../../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../../secrets/resolve.js";
import { probeGatewayStatus } from "./probe.js";
import { normalizeListenerAddress, parsePortFromArgs, pickProbeHostForBind } from "./shared.js";
import type { GatewayRpcOpts } from "./types.js";
@@ -127,13 +125,6 @@ async function resolveDaemonProbeToken(params: {
}
const defaults = params.daemonCfg.secrets?.defaults;
const configured = params.daemonCfg.gateway?.auth?.token;
const { ref } = resolveSecretInputRef({
value: configured,
defaults,
});
if (!ref) {
return normalizeSecretInputString(configured);
}
const authMode = params.daemonCfg.gateway?.auth?.mode;
if (authMode === "password" || authMode === "none" || authMode === "trusted-proxy") {
return undefined;
@@ -149,15 +140,16 @@ async function resolveDaemonProbeToken(params: {
return undefined;
}
}
const resolved = await resolveSecretRefValues([ref], {
const resolvedToken = await resolveRequiredConfiguredSecretRefInputString({
config: params.daemonCfg,
env: params.mergedDaemonEnv as NodeJS.ProcessEnv,
value: configured,
path: "gateway.auth.token",
});
const token = trimToUndefined(resolved.get(secretRefKey(ref)));
if (!token) {
throw new Error("gateway.auth.token resolved to an empty or non-string value.");
if (resolvedToken) {
return resolvedToken;
}
return token;
return normalizeSecretInputString(configured);
}
async function resolveDaemonProbePassword(params: {
@@ -176,13 +168,6 @@ async function resolveDaemonProbePassword(params: {
}
const defaults = params.daemonCfg.secrets?.defaults;
const configured = params.daemonCfg.gateway?.auth?.password;
const { ref } = resolveSecretInputRef({
value: configured,
defaults,
});
if (!ref) {
return normalizeSecretInputString(configured);
}
const authMode = params.daemonCfg.gateway?.auth?.mode;
if (authMode === "token" || authMode === "none" || authMode === "trusted-proxy") {
return undefined;
@@ -198,15 +183,16 @@ async function resolveDaemonProbePassword(params: {
return undefined;
}
}
const resolved = await resolveSecretRefValues([ref], {
const resolvedPassword = await resolveRequiredConfiguredSecretRefInputString({
config: params.daemonCfg,
env: params.mergedDaemonEnv as NodeJS.ProcessEnv,
value: configured,
path: "gateway.auth.password",
});
const password = trimToUndefined(resolved.get(secretRefKey(ref)));
if (!password) {
throw new Error("gateway.auth.password resolved to an empty or non-string value.");
if (resolvedPassword) {
return resolvedPassword;
}
return password;
return normalizeSecretInputString(configured);
}
export async function gatherDaemonStatus(

View File

@@ -1,13 +1,12 @@
import type { Command } from "commander";
import qrcode from "qrcode-terminal";
import { loadConfig } from "../config/config.js";
import { hasConfiguredSecretInput, resolveSecretInputRef } from "../config/types.secrets.js";
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
import { readGatewayPasswordEnv, readGatewayTokenEnv } from "../gateway/credentials.js";
import { resolveRequiredConfiguredSecretRefInputString } from "../gateway/resolve-configured-secret-input-string.js";
import { resolvePairingSetupFromConfig, encodePairingSetupCode } from "../pairing/setup-code.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime } from "../runtime.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
import { resolveCommandSecretRefsViaGateway } from "./command-secret-gateway.js";
@@ -66,26 +65,19 @@ function shouldResolveLocalGatewayPasswordSecret(
async function resolveLocalGatewayPasswordSecretIfNeeded(
cfg: ReturnType<typeof loadConfig>,
): Promise<void> {
const authPassword = cfg.gateway?.auth?.password;
const { ref } = resolveSecretInputRef({
value: authPassword,
defaults: cfg.secrets?.defaults,
});
if (!ref) {
return;
}
const resolved = await resolveSecretRefValues([ref], {
const resolvedPassword = await resolveRequiredConfiguredSecretRefInputString({
config: cfg,
env: process.env,
value: cfg.gateway?.auth?.password,
path: "gateway.auth.password",
});
const value = resolved.get(secretRefKey(ref));
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("gateway.auth.password resolved to an empty or non-string value.");
if (!resolvedPassword) {
return;
}
if (!cfg.gateway?.auth) {
return;
}
cfg.gateway.auth.password = value.trim();
cfg.gateway.auth.password = resolvedPassword;
}
function emitQrSecretResolveDiagnostics(diagnostics: string[], opts: QrCliOptions): void {

View File

@@ -1,12 +1,10 @@
import { readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { readGatewayTokenEnv } from "../gateway/credentials.js";
import { resolveConfiguredSecretInputWithFallback } from "../gateway/resolve-configured-secret-input-string.js";
import { copyToClipboard } from "../infra/clipboard.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import {
detectBrowserOpenSupport,
formatControlUiSshHint,
@@ -27,49 +25,26 @@ async function resolveDashboardToken(
unresolvedRefReason?: string;
tokenSecretRefConfigured: boolean;
}> {
const { ref } = resolveSecretInputRef({
const resolved = await resolveConfiguredSecretInputWithFallback({
config: cfg,
env,
value: cfg.gateway?.auth?.token,
defaults: cfg.secrets?.defaults,
path: "gateway.auth.token",
readFallback: () => readGatewayTokenEnv(env),
});
const configToken =
ref || typeof cfg.gateway?.auth?.token !== "string"
? undefined
: cfg.gateway.auth.token.trim() || undefined;
if (configToken) {
return { token: configToken, source: "config", tokenSecretRefConfigured: false };
}
if (!ref) {
const envToken = readGatewayTokenEnv(env);
return envToken
? { token: envToken, source: "env", tokenSecretRefConfigured: false }
: { tokenSecretRefConfigured: false };
}
const refLabel = `${ref.source}:${ref.provider}:${ref.id}`;
try {
const resolved = await resolveSecretRefValues([ref], {
config: cfg,
env,
});
const value = resolved.get(secretRefKey(ref));
if (typeof value === "string" && value.trim().length > 0) {
return { token: value.trim(), source: "secretRef", tokenSecretRefConfigured: true };
}
const envToken = readGatewayTokenEnv(env);
return envToken
? { token: envToken, source: "env", tokenSecretRefConfigured: true }
: {
unresolvedRefReason: `gateway.auth.token SecretRef is unresolved (${refLabel}).`,
tokenSecretRefConfigured: true,
};
} catch {
const envToken = readGatewayTokenEnv(env);
return envToken
? { token: envToken, source: "env", tokenSecretRefConfigured: true }
: {
unresolvedRefReason: `gateway.auth.token SecretRef is unresolved (${refLabel}).`,
tokenSecretRefConfigured: true,
};
}
return {
token: resolved.value,
source:
resolved.source === "config"
? "config"
: resolved.source === "secretRef"
? "secretRef"
: resolved.source === "fallback"
? "env"
: undefined,
unresolvedRefReason: resolved.unresolvedRefReason,
tokenSecretRefConfigured: resolved.secretRefConfigured,
};
}
export async function dashboardCommand(

View File

@@ -1,49 +1,30 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
export { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
import { readGatewayTokenEnv } from "../gateway/credentials.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { resolveConfiguredSecretInputWithFallback } from "../gateway/resolve-configured-secret-input-string.js";
export async function resolveGatewayAuthTokenForService(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): Promise<{ token?: string; unavailableReason?: string }> {
const { ref } = resolveSecretInputRef({
const resolved = await resolveConfiguredSecretInputWithFallback({
config: cfg,
env,
value: cfg.gateway?.auth?.token,
defaults: cfg.secrets?.defaults,
path: "gateway.auth.token",
unresolvedReasonStyle: "detailed",
readFallback: () => readGatewayTokenEnv(env),
});
const configToken =
ref || typeof cfg.gateway?.auth?.token !== "string"
? undefined
: cfg.gateway.auth.token.trim() || undefined;
if (configToken) {
return { token: configToken };
if (resolved.value) {
return { token: resolved.value };
}
if (ref) {
try {
const resolved = await resolveSecretRefValues([ref], {
config: cfg,
env,
});
const value = resolved.get(secretRefKey(ref));
if (typeof value === "string" && value.trim().length > 0) {
return { token: value.trim() };
}
const envToken = readGatewayTokenEnv(env);
if (envToken) {
return { token: envToken };
}
return { unavailableReason: "gateway.auth.token SecretRef resolved to an empty value." };
} catch (err) {
const envToken = readGatewayTokenEnv(env);
if (envToken) {
return { token: envToken };
}
return {
unavailableReason: `gateway.auth.token SecretRef is configured but unresolved (${String(err)}).`,
};
}
if (!resolved.secretRefConfigured) {
return {};
}
return { token: readGatewayTokenEnv(env) };
if (resolved.unresolvedRefReason?.includes("resolved to an empty value")) {
return { unavailableReason: resolved.unresolvedRefReason };
}
return {
unavailableReason: `gateway.auth.token SecretRef is configured but unresolved (${resolved.unresolvedRefReason ?? "unknown reason"}).`,
};
}

View File

@@ -1,7 +1,6 @@
import type { GatewayAuthConfig, OpenClawConfig } from "../config/config.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { resolveRequiredConfiguredSecretRefInputString } from "./resolve-configured-secret-input-string.js";
export function withGatewayAuthPassword(cfg: OpenClawConfig, password: string): OpenClawConfig {
return {
@@ -57,13 +56,14 @@ export async function resolveGatewayPasswordSecretRef(params: {
) {
return params.cfg;
}
const resolved = await resolveSecretRefValues([ref], {
const value = await resolveRequiredConfiguredSecretRefInputString({
config: params.cfg,
env: params.env,
value: authPassword,
path: "gateway.auth.password",
});
const value = resolved.get(secretRefKey(ref));
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("gateway.auth.password resolved to an empty or non-string value.");
if (!value) {
return params.cfg;
}
return withGatewayAuthPassword(params.cfg, value.trim());
return withGatewayAuthPassword(params.cfg, value);
}

View File

@@ -0,0 +1,137 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.js";
import {
resolveConfiguredSecretInputWithFallback,
resolveRequiredConfiguredSecretRefInputString,
} from "./resolve-configured-secret-input-string.js";
function createConfig(value: unknown): OpenClawConfig {
return {
gateway: {
auth: {
token: value,
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
} as OpenClawConfig;
}
describe("resolveConfiguredSecretInputWithFallback", () => {
it("returns plaintext config value when present", async () => {
const resolved = await resolveConfiguredSecretInputWithFallback({
config: createConfig("config-token"),
env: {} as NodeJS.ProcessEnv,
value: "config-token",
path: "gateway.auth.token",
readFallback: () => "env-token",
});
expect(resolved).toEqual({
value: "config-token",
source: "config",
secretRefConfigured: false,
});
});
it("returns fallback value when config is empty and no SecretRef is configured", async () => {
const resolved = await resolveConfiguredSecretInputWithFallback({
config: createConfig(""),
env: {} as NodeJS.ProcessEnv,
value: "",
path: "gateway.auth.token",
readFallback: () => "env-token",
});
expect(resolved).toEqual({
value: "env-token",
source: "fallback",
secretRefConfigured: false,
});
});
it("returns resolved SecretRef value", async () => {
const resolved = await resolveConfiguredSecretInputWithFallback({
config: createConfig("${CUSTOM_GATEWAY_TOKEN}"),
env: { CUSTOM_GATEWAY_TOKEN: "resolved-token" } as NodeJS.ProcessEnv,
value: "${CUSTOM_GATEWAY_TOKEN}",
path: "gateway.auth.token",
readFallback: () => undefined,
});
expect(resolved).toEqual({
value: "resolved-token",
source: "secretRef",
secretRefConfigured: true,
});
});
it("falls back when SecretRef cannot be resolved", async () => {
const resolved = await resolveConfiguredSecretInputWithFallback({
config: createConfig("${MISSING_GATEWAY_TOKEN}"),
env: {} as NodeJS.ProcessEnv,
value: "${MISSING_GATEWAY_TOKEN}",
path: "gateway.auth.token",
readFallback: () => "env-fallback-token",
});
expect(resolved).toEqual({
value: "env-fallback-token",
source: "fallback",
secretRefConfigured: true,
});
});
it("returns unresolved reason when SecretRef cannot be resolved and no fallback exists", async () => {
const resolved = await resolveConfiguredSecretInputWithFallback({
config: createConfig("${MISSING_GATEWAY_TOKEN}"),
env: {} as NodeJS.ProcessEnv,
value: "${MISSING_GATEWAY_TOKEN}",
path: "gateway.auth.token",
});
expect(resolved.value).toBeUndefined();
expect(resolved.source).toBeUndefined();
expect(resolved.secretRefConfigured).toBe(true);
expect(resolved.unresolvedRefReason).toContain("gateway.auth.token SecretRef is unresolved");
expect(resolved.unresolvedRefReason).toContain("MISSING_GATEWAY_TOKEN");
});
});
describe("resolveRequiredConfiguredSecretRefInputString", () => {
it("returns undefined when no SecretRef is configured", async () => {
const value = await resolveRequiredConfiguredSecretRefInputString({
config: createConfig("plain-token"),
env: {} as NodeJS.ProcessEnv,
value: "plain-token",
path: "gateway.auth.token",
});
expect(value).toBeUndefined();
});
it("returns resolved SecretRef value", async () => {
const value = await resolveRequiredConfiguredSecretRefInputString({
config: createConfig("${CUSTOM_GATEWAY_TOKEN}"),
env: { CUSTOM_GATEWAY_TOKEN: "resolved-token" } as NodeJS.ProcessEnv,
value: "${CUSTOM_GATEWAY_TOKEN}",
path: "gateway.auth.token",
});
expect(value).toBe("resolved-token");
});
it("throws when SecretRef cannot be resolved", async () => {
await expect(
resolveRequiredConfiguredSecretRefInputString({
config: createConfig("${MISSING_GATEWAY_TOKEN}"),
env: {} as NodeJS.ProcessEnv,
value: "${MISSING_GATEWAY_TOKEN}",
path: "gateway.auth.token",
}),
).rejects.toThrow(/MISSING_GATEWAY_TOKEN/i);
});
});

View File

@@ -4,6 +4,7 @@ import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
export type SecretInputUnresolvedReasonStyle = "generic" | "detailed"; // pragma: allowlist secret
export type ConfiguredSecretInputSource = "config" | "secretRef" | "fallback";
function trimToUndefined(value: unknown): string | undefined {
if (typeof value !== "string") {
@@ -87,3 +88,98 @@ export async function resolveConfiguredSecretInputString(params: {
};
}
}
export async function resolveConfiguredSecretInputWithFallback(params: {
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
value: unknown;
path: string;
unresolvedReasonStyle?: SecretInputUnresolvedReasonStyle;
readFallback?: () => string | undefined;
}): Promise<{
value?: string;
source?: ConfiguredSecretInputSource;
unresolvedRefReason?: string;
secretRefConfigured: boolean;
}> {
const { ref } = resolveSecretInputRef({
value: params.value,
defaults: params.config.secrets?.defaults,
});
const configValue = !ref ? trimToUndefined(params.value) : undefined;
if (configValue) {
return {
value: configValue,
source: "config",
secretRefConfigured: false,
};
}
if (!ref) {
const fallback = params.readFallback?.();
if (fallback) {
return {
value: fallback,
source: "fallback",
secretRefConfigured: false,
};
}
return { secretRefConfigured: false };
}
const resolved = await resolveConfiguredSecretInputString({
config: params.config,
env: params.env,
value: params.value,
path: params.path,
unresolvedReasonStyle: params.unresolvedReasonStyle,
});
if (resolved.value) {
return {
value: resolved.value,
source: "secretRef",
secretRefConfigured: true,
};
}
const fallback = params.readFallback?.();
if (fallback) {
return {
value: fallback,
source: "fallback",
secretRefConfigured: true,
};
}
return {
unresolvedRefReason: resolved.unresolvedRefReason,
secretRefConfigured: true,
};
}
export async function resolveRequiredConfiguredSecretRefInputString(params: {
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
value: unknown;
path: string;
unresolvedReasonStyle?: SecretInputUnresolvedReasonStyle;
}): Promise<string | undefined> {
const { ref } = resolveSecretInputRef({
value: params.value,
defaults: params.config.secrets?.defaults,
});
if (!ref) {
return undefined;
}
const resolved = await resolveConfiguredSecretInputString({
config: params.config,
env: params.env,
value: params.value,
path: params.path,
unresolvedReasonStyle: params.unresolvedReasonStyle,
});
if (resolved.value) {
return resolved.value;
}
throw new Error(resolved.unresolvedRefReason ?? `${params.path} resolved to an empty value.`);
}

View File

@@ -5,9 +5,7 @@ import type {
OpenClawConfig,
} from "../config/config.js";
import { writeConfigFile } from "../config/config.js";
import { hasConfiguredSecretInput, resolveSecretInputRef } from "../config/types.secrets.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "./auth-mode-policy.js";
import { resolveGatewayAuth, type ResolvedGatewayAuth } from "./auth.js";
import {
@@ -15,6 +13,7 @@ import {
hasGatewayTokenEnvCandidate,
readGatewayTokenEnv,
} from "./credentials.js";
import { resolveRequiredConfiguredSecretRefInputString } from "./resolve-configured-secret-input-string.js";
export function mergeGatewayAuthConfig(
base?: GatewayAuthConfig,
@@ -167,26 +166,15 @@ async function resolveGatewayTokenSecretRef(
env: NodeJS.ProcessEnv,
authOverride?: GatewayAuthConfig,
): Promise<string | undefined> {
const authToken = cfg.gateway?.auth?.token;
const { ref } = resolveSecretInputRef({
value: authToken,
defaults: cfg.secrets?.defaults,
});
if (!ref) {
return undefined;
}
if (!shouldResolveGatewayTokenSecretRef({ cfg, env, authOverride })) {
return undefined;
}
const resolved = await resolveSecretRefValues([ref], {
return await resolveRequiredConfiguredSecretRefInputString({
config: cfg,
env,
value: cfg.gateway?.auth?.token,
path: "gateway.auth.token",
});
const value = resolved.get(secretRefKey(ref));
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("gateway.auth.token resolved to an empty or non-string value.");
}
return value.trim();
}
function shouldResolveGatewayPasswordSecretRef(params: {
@@ -216,26 +204,15 @@ async function resolveGatewayPasswordSecretRef(
env: NodeJS.ProcessEnv,
authOverride?: GatewayAuthConfig,
): Promise<string | undefined> {
const authPassword = cfg.gateway?.auth?.password;
const { ref } = resolveSecretInputRef({
value: authPassword,
defaults: cfg.secrets?.defaults,
});
if (!ref) {
return undefined;
}
if (!shouldResolveGatewayPasswordSecretRef({ cfg, env, authOverride })) {
return undefined;
}
const resolved = await resolveSecretRefValues([ref], {
return await resolveRequiredConfiguredSecretRefInputString({
config: cfg,
env,
value: cfg.gateway?.auth?.password,
path: "gateway.auth.password",
});
const value = resolved.get(secretRefKey(ref));
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("gateway.auth.password resolved to an empty or non-string value.");
}
return value.trim();
}
export async function ensureGatewayStartupAuth(params: {

View File

@@ -7,8 +7,7 @@ import {
resolveSecretInputRef,
} from "../config/types.secrets.js";
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "../gateway/auth-mode-policy.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { resolveRequiredConfiguredSecretRefInputString } from "../gateway/resolve-configured-secret-input-string.js";
import { resolveGatewayBindUrl } from "../shared/gateway-bind-url.js";
import { isCarrierGradeNatIpv4Address, isRfc1918Ipv4Address } from "../shared/net/ip.js";
import { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
@@ -209,14 +208,6 @@ async function resolveGatewayTokenSecretRef(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): Promise<OpenClawConfig> {
const authToken = cfg.gateway?.auth?.token;
const { ref } = resolveSecretInputRef({
value: authToken,
defaults: cfg.secrets?.defaults,
});
if (!ref) {
return cfg;
}
const hasTokenEnvCandidate = Boolean(resolveGatewayTokenFromEnv(env));
if (hasTokenEnvCandidate) {
return cfg;
@@ -233,13 +224,14 @@ async function resolveGatewayTokenSecretRef(
return cfg;
}
}
const resolved = await resolveSecretRefValues([ref], {
const token = await resolveRequiredConfiguredSecretRefInputString({
config: cfg,
env,
value: cfg.gateway?.auth?.token,
path: "gateway.auth.token",
});
const value = resolved.get(secretRefKey(ref));
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("gateway.auth.token resolved to an empty or non-string value.");
if (!token) {
return cfg;
}
return {
...cfg,
@@ -247,7 +239,7 @@ async function resolveGatewayTokenSecretRef(
...cfg.gateway,
auth: {
...cfg.gateway?.auth,
token: value.trim(),
token,
},
},
};
@@ -257,14 +249,6 @@ async function resolveGatewayPasswordSecretRef(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): Promise<OpenClawConfig> {
const authPassword = cfg.gateway?.auth?.password;
const { ref } = resolveSecretInputRef({
value: authPassword,
defaults: cfg.secrets?.defaults,
});
if (!ref) {
return cfg;
}
const hasPasswordEnvCandidate = Boolean(resolveGatewayPasswordFromEnv(env));
if (hasPasswordEnvCandidate) {
return cfg;
@@ -281,13 +265,14 @@ async function resolveGatewayPasswordSecretRef(
return cfg;
}
}
const resolved = await resolveSecretRefValues([ref], {
const password = await resolveRequiredConfiguredSecretRefInputString({
config: cfg,
env,
value: cfg.gateway?.auth?.password,
path: "gateway.auth.password",
});
const value = resolved.get(secretRefKey(ref));
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("gateway.auth.password resolved to an empty or non-string value.");
if (!password) {
return cfg;
}
return {
...cfg,
@@ -295,7 +280,7 @@ async function resolveGatewayPasswordSecretRef(
...cfg.gateway,
auth: {
...cfg.gateway?.auth,
password: value.trim(),
password,
},
},
};