openclaw/src/commands/dashboard.ts

151 lines
5.1 KiB
TypeScript

import { readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.js";
import { resolveSecretInputRef } from "../config/types.secrets.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,
openUrl,
resolveControlUiLinks,
} from "./onboard-helpers.js";
type DashboardOptions = {
noOpen?: boolean;
};
function readGatewayTokenEnv(env: NodeJS.ProcessEnv): string | undefined {
const primary = env.OPENCLAW_GATEWAY_TOKEN?.trim();
if (primary) {
return primary;
}
const legacy = env.CLAWDBOT_GATEWAY_TOKEN?.trim();
return legacy || undefined;
}
async function resolveDashboardToken(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv = process.env,
): Promise<{
token?: string;
source?: "config" | "env" | "secretRef";
unresolvedRefReason?: string;
tokenSecretRefConfigured: boolean;
}> {
const { ref } = resolveSecretInputRef({
value: cfg.gateway?.auth?.token,
defaults: cfg.secrets?.defaults,
});
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,
};
}
}
export async function dashboardCommand(
runtime: RuntimeEnv = defaultRuntime,
options: DashboardOptions = {},
) {
const snapshot = await readConfigFileSnapshot();
const cfg = snapshot.valid ? snapshot.config : {};
const port = resolveGatewayPort(cfg);
const bind = cfg.gateway?.bind ?? "loopback";
const basePath = cfg.gateway?.controlUi?.basePath;
const customBindHost = cfg.gateway?.customBindHost;
const resolvedToken = await resolveDashboardToken(cfg, process.env);
const token = resolvedToken.token ?? "";
// LAN URLs fail secure-context checks in browsers.
// Coerce only lan->loopback and preserve other bind modes.
const links = resolveControlUiLinks({
port,
bind: bind === "lan" ? "loopback" : bind,
customBindHost,
basePath,
});
// Avoid embedding externally managed SecretRef tokens in terminal/clipboard/browser args.
const includeTokenInUrl = token.length > 0 && !resolvedToken.tokenSecretRefConfigured;
// Prefer URL fragment to avoid leaking auth tokens via query params.
const dashboardUrl = includeTokenInUrl
? `${links.httpUrl}#token=${encodeURIComponent(token)}`
: links.httpUrl;
runtime.log(`Dashboard URL: ${dashboardUrl}`);
if (resolvedToken.tokenSecretRefConfigured && token) {
runtime.log(
"Token auto-auth is disabled for SecretRef-managed gateway.auth.token; use your external token source if prompted.",
);
}
if (resolvedToken.unresolvedRefReason) {
runtime.log(`Token auto-auth unavailable: ${resolvedToken.unresolvedRefReason}`);
runtime.log(
"Set OPENCLAW_GATEWAY_TOKEN in this shell or resolve your secret provider, then rerun `openclaw dashboard`.",
);
}
const copied = await copyToClipboard(dashboardUrl).catch(() => false);
runtime.log(copied ? "Copied to clipboard." : "Copy to clipboard unavailable.");
let opened = false;
let hint: string | undefined;
if (!options.noOpen) {
const browserSupport = await detectBrowserOpenSupport();
if (browserSupport.ok) {
opened = await openUrl(dashboardUrl);
}
if (!opened) {
hint = formatControlUiSshHint({
port,
basePath,
token: includeTokenInUrl ? token || undefined : undefined,
});
}
} else {
hint = "Browser launch disabled (--no-open). Use the URL above.";
}
if (opened) {
runtime.log("Opened in your browser. Keep that tab to control OpenClaw.");
} else if (hint) {
runtime.log(hint);
}
}