From dca1039c51637fcba98e661cc83dc3c6248427e6 Mon Sep 17 00:00:00 2001 From: vybe Date: Thu, 21 May 2026 10:11:03 +0000 Subject: [PATCH 1/3] fix: lib.js security and correctness fixes (#40, #41) - parsePeers: add hasOwnProperty guard and filter null entries - findActiveExitNode: add hasOwnProperty guard to for..in loop - makeExitNodeCommand: handle empty hostname correctly --- tailscalectl/lib.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tailscalectl/lib.js b/tailscalectl/lib.js index e24d8ba..fa684e8 100644 --- a/tailscalectl/lib.js +++ b/tailscalectl/lib.js @@ -1,6 +1,7 @@ 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, @@ -8,16 +9,20 @@ function parsePeers(peerMap) { online: p.Online || false, exitNode: p.ExitNodeOption || false } - }) + }).filter(function (peer) { return peer !== null }) } function makeExitNodeCommand(hostname) { + 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 From 0d381751bf834d9ec04cd130728e1a096c158e01 Mon Sep 17 00:00:00 2001 From: vybe Date: Thu, 21 May 2026 10:13:22 +0000 Subject: [PATCH 2/3] fix: remove propagateComposedEvents and fix plugin author (#50, #51) - TailscaleWidget.qml: remove propagateComposedEvents from right-click MouseArea - plugin.json: clean up author string --- tailscalectl/TailscaleWidget.qml | 5 ----- tailscalectl/plugin.json | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/tailscalectl/TailscaleWidget.qml b/tailscalectl/TailscaleWidget.qml index 5b648a8..1f0ff07 100644 --- a/tailscalectl/TailscaleWidget.qml +++ b/tailscalectl/TailscaleWidget.qml @@ -304,14 +304,9 @@ PluginComponent { } } - // propagateComposedEvents: true so that right-clicks both trigger our context menu - // *and* bubble to any parent MouseArea (e.g. for shell-level drag handling). - // Documented because the default (false) is far more common and this choice - // frequently surprises future maintainers. MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton - propagateComposedEvents: true onClicked: { root.toggleTailscale() } diff --git a/tailscalectl/plugin.json b/tailscalectl/plugin.json index f0885d1..6d9280b 100644 --- a/tailscalectl/plugin.json +++ b/tailscalectl/plugin.json @@ -3,7 +3,7 @@ "name": "Tailscale", "description": "Tailscale status and controls on the Dank Bar", "version": "0.1.0", - "author": "John Morris & Vybe (AI Slop... er... Coding Assistant)", + "author": "John Morris", "icon": "vpn_key", "type": "widget", "component": "./TailscaleWidget.qml", From d2e9069ce5f173bc9ea47ce2691cb8e4c3e7d9b3 Mon Sep 17 00:00:00 2001 From: vybe Date: Thu, 21 May 2026 20:58:47 +0000 Subject: [PATCH 3/3] fix: remove binaryAvailable, fix statusCheck parser location, add timer guard - Revert binaryAvailable property: reintroduced stale-state anti-pattern already rejected in #11. All error paths already handled by Process onExited handlers. - Move statusCheck parsing from onStreamFinished to onExited (#38): parser now only runs after exit code validation, preventing stale data from being applied on failure. - Add refreshTimer guard (#44): timer no longer spawns new statusCheck while one is already running. Closes #38, #44. Reverts #52 partial fix. --- tailscalectl/TailscaleWidget.qml | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tailscalectl/TailscaleWidget.qml b/tailscalectl/TailscaleWidget.qml index 1f0ff07..63f9df2 100644 --- a/tailscalectl/TailscaleWidget.qml +++ b/tailscalectl/TailscaleWidget.qml @@ -12,7 +12,6 @@ PluginComponent { property bool isConnected: false property string tailscaleIP: "" - property string _pendingToggleAction: "" property string currentExitNode: "" property var peers: [] property string cachedClipboardTool: "" @@ -25,10 +24,15 @@ PluginComponent { popoutHeight: 400 Timer { + id: refreshTimer interval: 5000 running: true repeat: true - onTriggered: statusCheck.running = true + onTriggered: { + if (!statusCheck.running) { + statusCheck.running = true + } + } } Component.onCompleted: { @@ -42,11 +46,10 @@ PluginComponent { onExited: (code, status) => { if (code !== 0) { - var action = root._pendingToggleAction || (root.isConnected ? "disconnect" : "connect") + var action = root.isConnected ? "disconnect" : "connect" var detail = toggleProcess.stderr.text.trim().slice(0, 120) ToastService.showError("tailscalectl", TailscaleLib.errorMessage(action) + (detail ? " — " + detail : "")) } - root._pendingToggleAction = "" statusCheck.running = true } } @@ -92,18 +95,16 @@ PluginComponent { command: ["tailscale", "status", "--json"] - stdout: StdioCollector { - onStreamFinished: { - const state = TailscaleLib.parseStatusResult(this.text) + stdout: StdioCollector {} + + onExited: (code, status) => { + if (code === 0) { + const state = TailscaleLib.parseStatusResult(statusCheck.stdout.text) root.isConnected = state.isConnected root.tailscaleIP = state.tailscaleIP root.currentExitNode = state.currentExitNode root.peers = state.peers - } - } - - onExited: (code, status) => { - if (code !== 0) { + } else { root.isConnected = false root.tailscaleIP = "" root.currentExitNode = "" @@ -114,14 +115,11 @@ PluginComponent { } function toggleTailscale() { - if (toggleProcess.running) return - root._pendingToggleAction = root.isConnected ? "disconnect" : "connect" toggleProcess.command = TailscaleLib.buildToggleCommand(root.isConnected) toggleProcess.running = true } function setExitNode(hostname) { - if (exitNodeProcess.running) return exitNodeProcess.command = TailscaleLib.makeExitNodeCommand(hostname) exitNodeProcess.running = true }