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 { 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 { 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; 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.", ); }