dms_tailscalectl/tailscalectl/lib.js
vybe 202896f8e1 style: strictly enforce semicolon termination + curly braces on all JS statements
- lib.js, test/lib.test.js, and all executable JS in TailscaleWidget.qml (onExited handlers, custom methods, onClicked blocks) now terminate every statement with ;
- All if/for/while blocks use {} even for single statements (per repo code style rules)
- No behavior change; all tests continue to pass
- This was the missing follow-up to the previous refactor (the changes existed in the working tree but were never committed on the feature branch)

Written by AI agent working for @jtmorris. Model: grok-build-0.1.
2026-05-23 04:57:46 +00:00

139 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function parsePeers(peerMap) {
if (!peerMap) { return []; }
return Object.keys(peerMap).map(function (key) {
if (!Object.prototype.hasOwnProperty.call(peerMap, key)) { return null; }
var p = peerMap[key];
return {
hostname: p.HostName || key,
ip: (p.TailscaleIPs && p.TailscaleIPs.length) ? p.TailscaleIPs[0] : "",
online: p.Online || false,
exitNode: p.ExitNodeOption || false
};
}).filter(function (peer) { return peer !== null; });
}
function makeExitNodeCommand(hostname) {
if (!isValidExitNodeHostname(hostname)) {
return null;
}
if (hostname === "") {
return ["tailscale", "set", "--exit-node="];
}
return ["tailscale", "set", "--exit-node=" + hostname];
}
function findActiveExitNode(peerMap) {
if (!peerMap) { return ""; }
for (const key in peerMap) {
if (!Object.prototype.hasOwnProperty.call(peerMap, key)) { continue; }
const p = peerMap[key];
if (p.ExitNode) {
return p.HostName || key;
}
}
return "";
}
const clipboardTools = [
{ argv: ["dms", "cl", "copy"] },
{ argv: ["wl-copy"] }
];
function getClipboardCommands(text) {
return clipboardTools.map(function (tool) {
return tool.argv.concat([text]);
});
}
function getStrings() {
return {
header: "Tailscale",
connected: "Connected",
disconnected: "Disconnected",
exitNodePrefix: "Exit node: ",
none: "None",
copied: function (text) { return "Copied " + text + " to clipboard"; },
clearExitNode: "×",
setExitNode: "↗",
invalidExitNodeHostname: "Invalid exit node hostname"
};
}
// Light UI predicates — keep the view thin.
function shouldShowClearExitNode(currentExitNode) {
return currentExitNode !== "";
}
function isActiveExitNode(currentExitNode, hostname) {
return currentExitNode === hostname;
}
// Security: validate hostnames coming from tailscale status JSON.
// Fail closed on obviously malicious input.
function isValidExitNodeHostname(hostname) {
if (typeof hostname !== "string") { return false; }
if (hostname === "") { return true; }
return /^[a-zA-Z0-9]([a-zA-Z0-9-_.]{0,62}[a-zA-Z0-9])?$/.test(hostname);
}
function parseStatusResult(jsonText) {
try {
const data = JSON.parse(jsonText);
return {
isConnected: data.BackendState === "Running",
tailscaleIP: (data.Self && data.Self.TailscaleIPs && data.Self.TailscaleIPs[0]) || "",
currentExitNode: findActiveExitNode(data.Peer || {}),
peers: parsePeers(data.Peer || {})
};
} catch (e) {
return { isConnected: false, tailscaleIP: "", currentExitNode: "", peers: [] };
}
}
function buildToggleCommand(isConnected) {
return isConnected ? ["tailscale", "down"] : ["tailscale", "up"];
}
// Single source of truth for the status command used for on-demand and post-action verification.
function getStatusCommand() {
return ["tailscale", "status", "--json"];
}
function errorMessage(cmd) {
var messages = {
"up": "Failed to connect to Tailscale",
"connect": "Failed to connect to Tailscale",
"down": "Failed to disconnect from Tailscale",
"disconnect": "Failed to disconnect from Tailscale",
"set": "Failed to set exit node",
"status": "Failed to read Tailscale status",
"clipboard": "Error copying to clipboard"
};
return messages[cmd] || "Tailscale command failed";
}
// Central error formatting for the widget. Used by both success and error paths.
// detail is optional truncated stderr or extra context.
function formatError(action, detail) {
var base = errorMessage(action);
if (detail && detail.length > 0) {
var truncated = detail.length > 120 ? detail.slice(0, 120) : detail;
return base + " — " + truncated;
}
return base;
}
const PendingAction = Object.freeze({
TOGGLE: "toggle"
});
function commandForPendingAction(pending, freshIsConnected) {
if (pending === PendingAction.TOGGLE) {
return buildToggleCommand(freshIsConnected);
}
return null;
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, formatError, getStatusCommand, isValidExitNodeHostname, getClipboardCommands, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode, PendingAction, commandForPendingAction };
}