Files
openclaw/src/gateway/credentials.ts
2026-03-11 01:37:25 +00:00

351 lines
11 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import {
createGatewayCredentialPlan,
type GatewayCredentialPlan,
readGatewayPasswordEnv,
readGatewayTokenEnv,
trimCredentialToUndefined,
trimToUndefined,
} from "./credential-planner.js";
export {
hasGatewayPasswordEnvCandidate,
hasGatewayTokenEnvCandidate,
readGatewayPasswordEnv,
readGatewayTokenEnv,
trimCredentialToUndefined,
trimToUndefined,
} from "./credential-planner.js";
export type ExplicitGatewayAuth = {
token?: string;
password?: string;
};
export type ResolvedGatewayCredentials = {
token?: string;
password?: string;
};
export type GatewayCredentialMode = "local" | "remote";
export type GatewayCredentialPrecedence = "env-first" | "config-first";
export type GatewayRemoteCredentialPrecedence = "remote-first" | "env-first";
export type GatewayRemoteCredentialFallback = "remote-env-local" | "remote-only";
const GATEWAY_SECRET_REF_UNAVAILABLE_ERROR_CODE = "GATEWAY_SECRET_REF_UNAVAILABLE"; // pragma: allowlist secret
export class GatewaySecretRefUnavailableError extends Error {
readonly code = GATEWAY_SECRET_REF_UNAVAILABLE_ERROR_CODE;
readonly path: string;
constructor(path: string) {
super(
[
`${path} is configured as a secret reference but is unavailable in this command path.`,
"Fix: set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD, pass explicit --token/--password,",
"or run a gateway command path that resolves secret references before credential selection.",
].join("\n"),
);
this.name = "GatewaySecretRefUnavailableError";
this.path = path;
}
}
export function isGatewaySecretRefUnavailableError(
error: unknown,
expectedPath?: string,
): error is GatewaySecretRefUnavailableError {
if (!(error instanceof GatewaySecretRefUnavailableError)) {
return false;
}
if (!expectedPath) {
return true;
}
return error.path === expectedPath;
}
function firstDefined(values: Array<string | undefined>): string | undefined {
for (const value of values) {
if (value) {
return value;
}
}
return undefined;
}
function throwUnresolvedGatewaySecretInput(path: string): never {
throw new GatewaySecretRefUnavailableError(path);
}
export function resolveGatewayCredentialsFromValues(params: {
configToken?: unknown;
configPassword?: unknown;
env?: NodeJS.ProcessEnv;
includeLegacyEnv?: boolean;
tokenPrecedence?: GatewayCredentialPrecedence;
passwordPrecedence?: GatewayCredentialPrecedence;
}): ResolvedGatewayCredentials {
const env = params.env ?? process.env;
const includeLegacyEnv = params.includeLegacyEnv ?? true;
const envToken = readGatewayTokenEnv(env, includeLegacyEnv);
const envPassword = readGatewayPasswordEnv(env, includeLegacyEnv);
const configToken = trimCredentialToUndefined(params.configToken);
const configPassword = trimCredentialToUndefined(params.configPassword);
const tokenPrecedence = params.tokenPrecedence ?? "env-first";
const passwordPrecedence = params.passwordPrecedence ?? "env-first";
const token =
tokenPrecedence === "config-first"
? firstDefined([configToken, envToken])
: firstDefined([envToken, configToken]);
const password =
passwordPrecedence === "config-first" // pragma: allowlist secret
? firstDefined([configPassword, envPassword])
: firstDefined([envPassword, configPassword]);
return { token, password };
}
function resolveLocalGatewayCredentials(params: {
plan: GatewayCredentialPlan;
env: NodeJS.ProcessEnv;
includeLegacyEnv: boolean;
localTokenPrecedence: GatewayCredentialPrecedence;
localPasswordPrecedence: GatewayCredentialPrecedence;
}): ResolvedGatewayCredentials {
const fallbackToken = params.plan.localToken.configured
? params.plan.localToken.value
: params.plan.remoteToken.value;
const fallbackPassword = params.plan.localPassword.configured
? params.plan.localPassword.value
: params.plan.remotePassword.value;
const localResolved = resolveGatewayCredentialsFromValues({
configToken: fallbackToken,
configPassword: fallbackPassword,
env: params.env,
includeLegacyEnv: params.includeLegacyEnv,
tokenPrecedence: params.localTokenPrecedence,
passwordPrecedence: params.localPasswordPrecedence,
});
const localPasswordCanWin =
params.plan.authMode === "password" ||
(params.plan.authMode !== "token" &&
params.plan.authMode !== "none" &&
params.plan.authMode !== "trusted-proxy" &&
!localResolved.token);
const localTokenCanWin =
params.plan.authMode === "token" ||
(params.plan.authMode !== "password" &&
params.plan.authMode !== "none" &&
params.plan.authMode !== "trusted-proxy" &&
!localResolved.password);
if (
params.plan.localToken.refPath &&
params.localTokenPrecedence === "config-first" &&
!params.plan.localToken.value &&
Boolean(params.plan.envToken) &&
localTokenCanWin
) {
throwUnresolvedGatewaySecretInput(params.plan.localToken.refPath);
}
if (
params.plan.localPassword.refPath &&
params.localPasswordPrecedence === "config-first" && // pragma: allowlist secret
!params.plan.localPassword.value &&
Boolean(params.plan.envPassword) &&
localPasswordCanWin
) {
throwUnresolvedGatewaySecretInput(params.plan.localPassword.refPath);
}
if (
params.plan.localToken.refPath &&
!localResolved.token &&
!params.plan.envToken &&
localTokenCanWin
) {
throwUnresolvedGatewaySecretInput(params.plan.localToken.refPath);
}
if (
params.plan.localPassword.refPath &&
!localResolved.password &&
!params.plan.envPassword &&
localPasswordCanWin
) {
throwUnresolvedGatewaySecretInput(params.plan.localPassword.refPath);
}
return localResolved;
}
function resolveRemoteGatewayCredentials(params: {
plan: GatewayCredentialPlan;
remoteTokenPrecedence: GatewayRemoteCredentialPrecedence;
remotePasswordPrecedence: GatewayRemoteCredentialPrecedence;
remoteTokenFallback: GatewayRemoteCredentialFallback;
remotePasswordFallback: GatewayRemoteCredentialFallback;
}): ResolvedGatewayCredentials {
const token =
params.remoteTokenFallback === "remote-only"
? params.plan.remoteToken.value
: params.remoteTokenPrecedence === "env-first"
? firstDefined([
params.plan.envToken,
params.plan.remoteToken.value,
params.plan.localToken.value,
])
: firstDefined([
params.plan.remoteToken.value,
params.plan.envToken,
params.plan.localToken.value,
]);
const password =
params.remotePasswordFallback === "remote-only" // pragma: allowlist secret
? params.plan.remotePassword.value
: params.remotePasswordPrecedence === "env-first" // pragma: allowlist secret
? firstDefined([
params.plan.envPassword,
params.plan.remotePassword.value,
params.plan.localPassword.value,
])
: firstDefined([
params.plan.remotePassword.value,
params.plan.envPassword,
params.plan.localPassword.value,
]);
const localTokenFallbackEnabled = params.remoteTokenFallback !== "remote-only";
const localTokenFallback =
params.remoteTokenFallback === "remote-only" ? undefined : params.plan.localToken.value;
const localPasswordFallback =
params.remotePasswordFallback === "remote-only" ? undefined : params.plan.localPassword.value; // pragma: allowlist secret
if (
params.plan.remoteToken.refPath &&
!token &&
!params.plan.envToken &&
!localTokenFallback &&
!password
) {
throwUnresolvedGatewaySecretInput(params.plan.remoteToken.refPath);
}
if (
params.plan.remotePassword.refPath &&
!password &&
!params.plan.envPassword &&
!localPasswordFallback &&
!token
) {
throwUnresolvedGatewaySecretInput(params.plan.remotePassword.refPath);
}
if (
params.plan.localToken.refPath &&
localTokenFallbackEnabled &&
!token &&
!password &&
!params.plan.envToken &&
!params.plan.remoteToken.value &&
params.plan.localTokenCanWin
) {
throwUnresolvedGatewaySecretInput(params.plan.localToken.refPath);
}
return { token, password };
}
export function resolveGatewayCredentialsFromConfig(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
explicitAuth?: ExplicitGatewayAuth;
urlOverride?: string;
urlOverrideSource?: "cli" | "env";
modeOverride?: GatewayCredentialMode;
includeLegacyEnv?: boolean;
localTokenPrecedence?: GatewayCredentialPrecedence;
localPasswordPrecedence?: GatewayCredentialPrecedence;
remoteTokenPrecedence?: GatewayRemoteCredentialPrecedence;
remotePasswordPrecedence?: GatewayRemoteCredentialPrecedence;
remoteTokenFallback?: GatewayRemoteCredentialFallback;
remotePasswordFallback?: GatewayRemoteCredentialFallback;
}): ResolvedGatewayCredentials {
const env = params.env ?? process.env;
const includeLegacyEnv = params.includeLegacyEnv ?? true;
const explicitToken = trimToUndefined(params.explicitAuth?.token);
const explicitPassword = trimToUndefined(params.explicitAuth?.password);
if (explicitToken || explicitPassword) {
return { token: explicitToken, password: explicitPassword };
}
if (trimToUndefined(params.urlOverride) && params.urlOverrideSource !== "env") {
return {};
}
if (trimToUndefined(params.urlOverride) && params.urlOverrideSource === "env") {
return resolveGatewayCredentialsFromValues({
configToken: undefined,
configPassword: undefined,
env,
includeLegacyEnv,
tokenPrecedence: "env-first",
passwordPrecedence: "env-first", // pragma: allowlist secret
});
}
const plan = createGatewayCredentialPlan({
config: params.cfg,
env,
includeLegacyEnv,
});
const mode: GatewayCredentialMode = params.modeOverride ?? plan.configuredMode;
const localTokenPrecedence =
params.localTokenPrecedence ??
(env.OPENCLAW_SERVICE_KIND === "gateway" ? "config-first" : "env-first");
const localPasswordPrecedence = params.localPasswordPrecedence ?? "env-first";
if (mode === "local") {
return resolveLocalGatewayCredentials({
plan,
env,
includeLegacyEnv,
localTokenPrecedence,
localPasswordPrecedence,
});
}
const remoteTokenFallback = params.remoteTokenFallback ?? "remote-env-local";
const remotePasswordFallback = params.remotePasswordFallback ?? "remote-env-local";
const remoteTokenPrecedence = params.remoteTokenPrecedence ?? "remote-first";
const remotePasswordPrecedence = params.remotePasswordPrecedence ?? "env-first";
return resolveRemoteGatewayCredentials({
plan,
remoteTokenPrecedence,
remotePasswordPrecedence,
remoteTokenFallback,
remotePasswordFallback,
});
}
export function resolveGatewayProbeCredentialsFromConfig(params: {
cfg: OpenClawConfig;
mode: GatewayCredentialMode;
env?: NodeJS.ProcessEnv;
explicitAuth?: ExplicitGatewayAuth;
}): ResolvedGatewayCredentials {
return resolveGatewayCredentialsFromConfig({
cfg: params.cfg,
env: params.env,
explicitAuth: params.explicitAuth,
modeOverride: params.mode,
includeLegacyEnv: false,
remoteTokenFallback: "remote-only",
});
}
export function resolveGatewayDriftCheckCredentialsFromConfig(params: {
cfg: OpenClawConfig;
}): ResolvedGatewayCredentials {
return resolveGatewayCredentialsFromConfig({
cfg: params.cfg,
env: {} as NodeJS.ProcessEnv,
modeOverride: "local",
localTokenPrecedence: "config-first",
});
}