317 lines
8.9 KiB
TypeScript
317 lines
8.9 KiB
TypeScript
import crypto from "node:crypto";
|
|
import type {
|
|
GatewayAuthConfig,
|
|
GatewayTailscaleConfig,
|
|
OpenClawConfig,
|
|
} from "../config/config.js";
|
|
import { writeConfigFile } from "../config/config.js";
|
|
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
|
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "./auth-mode-policy.js";
|
|
import { resolveGatewayAuth, type ResolvedGatewayAuth } from "./auth.js";
|
|
import {
|
|
hasGatewayPasswordEnvCandidate,
|
|
hasGatewayTokenEnvCandidate,
|
|
readGatewayTokenEnv,
|
|
} from "./credentials.js";
|
|
import { resolveRequiredConfiguredSecretRefInputString } from "./resolve-configured-secret-input-string.js";
|
|
|
|
export function mergeGatewayAuthConfig(
|
|
base?: GatewayAuthConfig,
|
|
override?: GatewayAuthConfig,
|
|
): GatewayAuthConfig {
|
|
const merged: GatewayAuthConfig = { ...base };
|
|
if (!override) {
|
|
return merged;
|
|
}
|
|
if (override.mode !== undefined) {
|
|
merged.mode = override.mode;
|
|
}
|
|
if (override.token !== undefined) {
|
|
merged.token = override.token;
|
|
}
|
|
if (override.password !== undefined) {
|
|
merged.password = override.password;
|
|
}
|
|
if (override.allowTailscale !== undefined) {
|
|
merged.allowTailscale = override.allowTailscale;
|
|
}
|
|
if (override.rateLimit !== undefined) {
|
|
merged.rateLimit = override.rateLimit;
|
|
}
|
|
if (override.trustedProxy !== undefined) {
|
|
merged.trustedProxy = override.trustedProxy;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
export function mergeGatewayTailscaleConfig(
|
|
base?: GatewayTailscaleConfig,
|
|
override?: GatewayTailscaleConfig,
|
|
): GatewayTailscaleConfig {
|
|
const merged: GatewayTailscaleConfig = { ...base };
|
|
if (!override) {
|
|
return merged;
|
|
}
|
|
if (override.mode !== undefined) {
|
|
merged.mode = override.mode;
|
|
}
|
|
if (override.resetOnExit !== undefined) {
|
|
merged.resetOnExit = override.resetOnExit;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
function resolveGatewayAuthFromConfig(params: {
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
authOverride?: GatewayAuthConfig;
|
|
tailscaleOverride?: GatewayTailscaleConfig;
|
|
}) {
|
|
const tailscaleConfig = mergeGatewayTailscaleConfig(
|
|
params.cfg.gateway?.tailscale,
|
|
params.tailscaleOverride,
|
|
);
|
|
return resolveGatewayAuth({
|
|
authConfig: params.cfg.gateway?.auth,
|
|
authOverride: params.authOverride,
|
|
env: params.env,
|
|
tailscaleMode: tailscaleConfig.mode ?? "off",
|
|
});
|
|
}
|
|
|
|
function shouldPersistGeneratedToken(params: {
|
|
persistRequested: boolean;
|
|
resolvedAuth: ResolvedGatewayAuth;
|
|
}): boolean {
|
|
if (!params.persistRequested) {
|
|
return false;
|
|
}
|
|
|
|
// Keep CLI/runtime mode overrides ephemeral: startup should not silently
|
|
// mutate durable auth policy when mode was chosen by an override flag.
|
|
if (params.resolvedAuth.modeSource === "override") {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function hasGatewayTokenCandidate(params: {
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
authOverride?: GatewayAuthConfig;
|
|
}): boolean {
|
|
const envToken = readGatewayTokenEnv(params.env);
|
|
if (envToken) {
|
|
return true;
|
|
}
|
|
if (
|
|
typeof params.authOverride?.token === "string" &&
|
|
params.authOverride.token.trim().length > 0
|
|
) {
|
|
return true;
|
|
}
|
|
return hasConfiguredSecretInput(params.cfg.gateway?.auth?.token, params.cfg.secrets?.defaults);
|
|
}
|
|
|
|
function hasGatewayTokenOverrideCandidate(params: { authOverride?: GatewayAuthConfig }): boolean {
|
|
return Boolean(
|
|
typeof params.authOverride?.token === "string" && params.authOverride.token.trim().length > 0,
|
|
);
|
|
}
|
|
|
|
function hasGatewayPasswordOverrideCandidate(params: {
|
|
env: NodeJS.ProcessEnv;
|
|
authOverride?: GatewayAuthConfig;
|
|
}): boolean {
|
|
if (hasGatewayPasswordEnvCandidate(params.env)) {
|
|
return true;
|
|
}
|
|
return Boolean(
|
|
typeof params.authOverride?.password === "string" &&
|
|
params.authOverride.password.trim().length > 0,
|
|
);
|
|
}
|
|
|
|
function shouldResolveGatewayTokenSecretRef(params: {
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
authOverride?: GatewayAuthConfig;
|
|
}): boolean {
|
|
if (hasGatewayTokenOverrideCandidate({ authOverride: params.authOverride })) {
|
|
return false;
|
|
}
|
|
if (hasGatewayTokenEnvCandidate(params.env)) {
|
|
return false;
|
|
}
|
|
const explicitMode = params.authOverride?.mode ?? params.cfg.gateway?.auth?.mode;
|
|
if (explicitMode === "token") {
|
|
return true;
|
|
}
|
|
if (explicitMode === "password" || explicitMode === "none" || explicitMode === "trusted-proxy") {
|
|
return false;
|
|
}
|
|
|
|
if (hasGatewayPasswordOverrideCandidate(params)) {
|
|
return false;
|
|
}
|
|
return !hasConfiguredSecretInput(
|
|
params.cfg.gateway?.auth?.password,
|
|
params.cfg.secrets?.defaults,
|
|
);
|
|
}
|
|
|
|
async function resolveGatewayTokenSecretRef(
|
|
cfg: OpenClawConfig,
|
|
env: NodeJS.ProcessEnv,
|
|
authOverride?: GatewayAuthConfig,
|
|
): Promise<string | undefined> {
|
|
if (!shouldResolveGatewayTokenSecretRef({ cfg, env, authOverride })) {
|
|
return undefined;
|
|
}
|
|
return await resolveRequiredConfiguredSecretRefInputString({
|
|
config: cfg,
|
|
env,
|
|
value: cfg.gateway?.auth?.token,
|
|
path: "gateway.auth.token",
|
|
});
|
|
}
|
|
|
|
function shouldResolveGatewayPasswordSecretRef(params: {
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
authOverride?: GatewayAuthConfig;
|
|
}): boolean {
|
|
if (hasGatewayPasswordOverrideCandidate(params)) {
|
|
return false;
|
|
}
|
|
const explicitMode = params.authOverride?.mode ?? params.cfg.gateway?.auth?.mode;
|
|
if (explicitMode === "password") {
|
|
return true;
|
|
}
|
|
if (explicitMode === "token" || explicitMode === "none" || explicitMode === "trusted-proxy") {
|
|
return false;
|
|
}
|
|
|
|
if (hasGatewayTokenCandidate(params)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function resolveGatewayPasswordSecretRef(
|
|
cfg: OpenClawConfig,
|
|
env: NodeJS.ProcessEnv,
|
|
authOverride?: GatewayAuthConfig,
|
|
): Promise<string | undefined> {
|
|
if (!shouldResolveGatewayPasswordSecretRef({ cfg, env, authOverride })) {
|
|
return undefined;
|
|
}
|
|
return await resolveRequiredConfiguredSecretRefInputString({
|
|
config: cfg,
|
|
env,
|
|
value: cfg.gateway?.auth?.password,
|
|
path: "gateway.auth.password",
|
|
});
|
|
}
|
|
|
|
export async function ensureGatewayStartupAuth(params: {
|
|
cfg: OpenClawConfig;
|
|
env?: NodeJS.ProcessEnv;
|
|
authOverride?: GatewayAuthConfig;
|
|
tailscaleOverride?: GatewayTailscaleConfig;
|
|
persist?: boolean;
|
|
}): Promise<{
|
|
cfg: OpenClawConfig;
|
|
auth: ReturnType<typeof resolveGatewayAuth>;
|
|
generatedToken?: string;
|
|
persistedGeneratedToken: boolean;
|
|
}> {
|
|
assertExplicitGatewayAuthModeWhenBothConfigured(params.cfg);
|
|
const env = params.env ?? process.env;
|
|
const persistRequested = params.persist === true;
|
|
const [resolvedTokenRefValue, resolvedPasswordRefValue] = await Promise.all([
|
|
resolveGatewayTokenSecretRef(params.cfg, env, params.authOverride),
|
|
resolveGatewayPasswordSecretRef(params.cfg, env, params.authOverride),
|
|
]);
|
|
const authOverride: GatewayAuthConfig | undefined =
|
|
params.authOverride || resolvedTokenRefValue || resolvedPasswordRefValue
|
|
? {
|
|
...params.authOverride,
|
|
...(resolvedTokenRefValue ? { token: resolvedTokenRefValue } : {}),
|
|
...(resolvedPasswordRefValue ? { password: resolvedPasswordRefValue } : {}),
|
|
}
|
|
: undefined;
|
|
const resolved = resolveGatewayAuthFromConfig({
|
|
cfg: params.cfg,
|
|
env,
|
|
authOverride,
|
|
tailscaleOverride: params.tailscaleOverride,
|
|
});
|
|
if (resolved.mode !== "token" || (resolved.token?.trim().length ?? 0) > 0) {
|
|
assertHooksTokenSeparateFromGatewayAuth({ cfg: params.cfg, auth: resolved });
|
|
return { cfg: params.cfg, auth: resolved, persistedGeneratedToken: false };
|
|
}
|
|
|
|
const generatedToken = crypto.randomBytes(24).toString("hex");
|
|
const nextCfg: OpenClawConfig = {
|
|
...params.cfg,
|
|
gateway: {
|
|
...params.cfg.gateway,
|
|
auth: {
|
|
...params.cfg.gateway?.auth,
|
|
mode: "token",
|
|
token: generatedToken,
|
|
},
|
|
},
|
|
};
|
|
const persist = shouldPersistGeneratedToken({
|
|
persistRequested,
|
|
resolvedAuth: resolved,
|
|
});
|
|
if (persist) {
|
|
await writeConfigFile(nextCfg);
|
|
}
|
|
|
|
const nextAuth = resolveGatewayAuthFromConfig({
|
|
cfg: nextCfg,
|
|
env,
|
|
authOverride: params.authOverride,
|
|
tailscaleOverride: params.tailscaleOverride,
|
|
});
|
|
assertHooksTokenSeparateFromGatewayAuth({ cfg: nextCfg, auth: nextAuth });
|
|
return {
|
|
cfg: nextCfg,
|
|
auth: nextAuth,
|
|
generatedToken,
|
|
persistedGeneratedToken: persist,
|
|
};
|
|
}
|
|
|
|
export function assertHooksTokenSeparateFromGatewayAuth(params: {
|
|
cfg: OpenClawConfig;
|
|
auth: ResolvedGatewayAuth;
|
|
}): void {
|
|
if (params.cfg.hooks?.enabled !== true) {
|
|
return;
|
|
}
|
|
const hooksToken =
|
|
typeof params.cfg.hooks.token === "string" ? params.cfg.hooks.token.trim() : "";
|
|
if (!hooksToken) {
|
|
return;
|
|
}
|
|
const gatewayToken =
|
|
params.auth.mode === "token" && typeof params.auth.token === "string"
|
|
? params.auth.token.trim()
|
|
: "";
|
|
if (!gatewayToken) {
|
|
return;
|
|
}
|
|
if (hooksToken !== gatewayToken) {
|
|
return;
|
|
}
|
|
throw new Error(
|
|
"Invalid config: hooks.token must not match gateway auth token. Set a distinct hooks.token for hook ingress.",
|
|
);
|
|
}
|