Files
AgentHub/server/lib/network-config.js
T
nntrivi2001 4a665da79e fix: a token-gated or LAN-bound dashboard works end to end (WSL mirrored)
- /ws-pty carries the dashboard token like /ws (the terminal never attached
  with DASHBOARD_TOKEN set), and so does everything that cannot send the
  header: images, downloads, exports, drops (withToken/tokenHeader). A
  refused upgrade answers 401/403 and is logged and counted by reason.
- The browser asks for a missing or wrong token (401 EUNAUTHORIZED) and takes
  ?token= from the link agenthub start prints, dropping it from the URL.
- The CLI sends the token (DASHBOARD_TOKEN, else the one the running server
  publishes 0600 in <data dir>/runtime/dashboard-token); MCP and statusline
  read the same file.
- DASHBOARD_HOST takes a list, and 127.0.0.1 is always listened on too, so a
  LAN-only bind keeps hooks and the CLI working.
- agenthub start --host/--allowed-hosts/--token are saved in
  <data dir>/network.json for every later start; a wide bind gets a token;
  every reachable URL is printed.
- tmux < 3.7 is an amber, once-per-version notice that says only the pane
  scrollbar is missing, with a ~/.local build instead of sudo make install.
- INSTALL: PATH belongs in ~/.profile for sudo -iu/ssh; WSL mirrored notes.
2026-11-06 14:59:48 +07:00

83 lines
2.7 KiB
JavaScript

/**
* @file network-config.js
* @description The dashboard's saved network settings — bind address(es),
* allowed Host names and the dashboard token — in `<data dir>/network.json`
* (0600). `agenthub start --host/--allowed-hosts/--token` writes it; every
* server start (a restart after an update, the autostart unit, the plugin
* bootstrap) applies it with `applyNetworkConfig()`, so a wider bind no longer
* falls back to loopback silently the next time something else starts the
* server. An environment variable that is set always wins over the file.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");
/** File key → the environment variable it stands in for. */
const KEYS = {
host: "DASHBOARD_HOST",
allowedHosts: "DASHBOARD_ALLOWED_HOSTS",
token: "DASHBOARD_TOKEN",
};
function configPath() {
const { getDataDir } = require("./claude-home");
return path.join(getDataDir(), "network.json");
}
/** The saved settings, `{}` when there are none (or the file is unreadable). */
function readNetworkConfig() {
try {
const c = JSON.parse(fs.readFileSync(configPath(), "utf8"));
if (!c || typeof c !== "object" || Array.isArray(c)) return {};
const out = {};
for (const k of Object.keys(KEYS)) {
if (typeof c[k] === "string" && c[k].trim()) out[k] = c[k].trim();
}
return out;
} catch {
return {};
}
}
/**
* Merge `patch` into the saved settings: a string sets a key, `null` removes
* it. Written 0600 through a temp file. Returns the new settings.
*/
function writeNetworkConfig(patch) {
const next = { ...readNetworkConfig() };
for (const [k, v] of Object.entries(patch)) {
if (!(k in KEYS)) continue;
if (v === null || v === "") delete next[k];
else next[k] = String(v).trim();
}
const file = configPath();
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.${process.pid}.tmp`;
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
fs.chmodSync(tmp, 0o600);
fs.renameSync(tmp, file);
return next;
}
/** Put every saved setting whose environment variable is unset into `env`. */
function applyNetworkConfig(env = process.env) {
const saved = readNetworkConfig();
for (const [k, name] of Object.entries(KEYS)) {
if (saved[k] && !(typeof env[name] === "string" && env[name].trim())) env[name] = saved[k];
}
return saved;
}
/** A fresh random token for `--token auto`. */
const newToken = () => crypto.randomBytes(24).toString("base64url");
module.exports = {
configPath,
readNetworkConfig,
writeNetworkConfig,
applyNetworkConfig,
newToken,
KEYS,
};