Files
openclaw/src/commands/gateway-install-token.ts
2026-03-30 00:39:39 +01:00

201 lines
6.4 KiB
TypeScript

import { formatCliCommand } from "../cli/command-format.js";
import {
readConfigFileSnapshot,
replaceConfigFile,
type OpenClawConfig,
} from "../config/config.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { readGatewayTokenEnv } from "../gateway/credentials.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { randomToken } from "./onboard-helpers.js";
type GatewayInstallTokenOptions = {
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
explicitToken?: string;
autoGenerateWhenMissing?: boolean;
persistGeneratedToken?: boolean;
};
export type GatewayInstallTokenResolution = {
token?: string;
tokenRefConfigured: boolean;
unavailableReason?: string;
warnings: string[];
};
function resolveConfiguredGatewayInstallToken(params: {
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
explicitToken?: string;
tokenRef: unknown;
}): string | undefined {
const configToken =
params.tokenRef || typeof params.config.gateway?.auth?.token !== "string"
? undefined
: params.config.gateway.auth.token.trim() || undefined;
const explicitToken = params.explicitToken?.trim() || undefined;
const envToken = readGatewayTokenEnv(params.env);
return explicitToken || configToken || (params.tokenRef ? undefined : envToken);
}
async function validateGatewayInstallTokenSecretRef(params: {
tokenRef: NonNullable<ReturnType<typeof resolveSecretInputRef>["ref"]>;
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
}): Promise<string | undefined> {
try {
const resolved = await resolveSecretRefValues([params.tokenRef], {
config: params.config,
env: params.env,
});
const value = resolved.get(secretRefKey(params.tokenRef));
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("gateway.auth.token resolved to an empty or non-string value.");
}
return undefined;
} catch (err) {
return `gateway.auth.token SecretRef is configured but unresolved (${String(err)}).`;
}
}
async function maybePersistAutoGeneratedGatewayInstallToken(params: {
token: string;
config: OpenClawConfig;
warnings: string[];
}): Promise<string | undefined> {
try {
const snapshot = await readConfigFileSnapshot();
if (snapshot.exists && !snapshot.valid) {
params.warnings.push(
"Warning: config file exists but is invalid; skipping token persistence.",
);
return params.token;
}
const baseConfig = snapshot.exists ? (snapshot.sourceConfig ?? snapshot.config) : {};
const existingTokenRef = resolveSecretInputRef({
value: baseConfig.gateway?.auth?.token,
defaults: baseConfig.secrets?.defaults,
}).ref;
const baseConfigToken =
existingTokenRef || typeof baseConfig.gateway?.auth?.token !== "string"
? undefined
: baseConfig.gateway.auth.token.trim() || undefined;
if (!existingTokenRef && !baseConfigToken) {
await replaceConfigFile({
baseHash: snapshot.hash,
nextConfig: {
...baseConfig,
gateway: {
...baseConfig.gateway,
auth: {
...baseConfig.gateway?.auth,
mode: baseConfig.gateway?.auth?.mode ?? "token",
token: params.token,
},
},
},
});
return params.token;
}
if (baseConfigToken) {
return baseConfigToken;
}
params.warnings.push(
"Warning: gateway.auth.token is SecretRef-managed; skipping plaintext token persistence.",
);
return undefined;
} catch (err) {
params.warnings.push(`Warning: could not persist token to config: ${String(err)}`);
return params.token;
}
}
function formatAmbiguousGatewayAuthModeReason(): string {
return [
"gateway.auth.token and gateway.auth.password are both configured while gateway.auth.mode is unset.",
`Set ${formatCliCommand("openclaw config set gateway.auth.mode token")} or ${formatCliCommand("openclaw config set gateway.auth.mode password")}.`,
].join(" ");
}
export async function resolveGatewayInstallToken(
options: GatewayInstallTokenOptions,
): Promise<GatewayInstallTokenResolution> {
const cfg = options.config;
const warnings: string[] = [];
const tokenRef = resolveSecretInputRef({
value: cfg.gateway?.auth?.token,
defaults: cfg.secrets?.defaults,
}).ref;
const tokenRefConfigured = Boolean(tokenRef);
if (hasAmbiguousGatewayAuthModeConfig(cfg)) {
return {
token: undefined,
tokenRefConfigured,
unavailableReason: formatAmbiguousGatewayAuthModeReason(),
warnings,
};
}
const resolvedAuth = resolveGatewayAuth({
authConfig: cfg.gateway?.auth,
env: options.env,
tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
});
const needsToken =
shouldRequireGatewayTokenForInstall(cfg, options.env) && !resolvedAuth.allowTailscale;
let token = resolveConfiguredGatewayInstallToken({
config: cfg,
env: options.env,
explicitToken: options.explicitToken,
tokenRef,
});
let unavailableReason: string | undefined;
if (tokenRef && !token && needsToken) {
unavailableReason = await validateGatewayInstallTokenSecretRef({
tokenRef,
config: cfg,
env: options.env,
});
if (!unavailableReason) {
warnings.push(
"gateway.auth.token is SecretRef-managed; install will not persist a resolved token in service environment. Ensure the SecretRef is resolvable in the daemon runtime context.",
);
}
}
const allowAutoGenerate = options.autoGenerateWhenMissing ?? false;
const persistGeneratedToken = options.persistGeneratedToken ?? false;
if (!token && needsToken && !tokenRef && allowAutoGenerate) {
token = randomToken();
warnings.push(
persistGeneratedToken
? "No gateway token found. Auto-generated one and saving to config."
: "No gateway token found. Auto-generated one for this run without saving to config.",
);
if (persistGeneratedToken) {
token = await maybePersistAutoGeneratedGatewayInstallToken({
token,
config: cfg,
warnings,
});
}
}
return {
token,
tokenRefConfigured,
unavailableReason,
warnings,
};
}