function filterIPv4(ips) { var result = [] if (!ips || typeof ips.length !== "number") return result for (var i = 0; i < ips.length; i++) { var ip = String(ips[i] || "") if (/^100\./.test(ip)) result.push(ip) } return result } function filterIPv6(ips) { var result = [] if (!ips || typeof ips.length !== "number") return result for (var i = 0; i < ips.length; i++) { var ip = String(ips[i] || "") if (/^fd7a:115c:a1e0:/i.test(ip)) result.push(ip) } return result } function cleanDnsName(name) { var value = String(name || "") return value.charAt(value.length - 1) === "." ? value.slice(0, -1) : value } function shortDnsName(name) { var clean = cleanDnsName(name) if (clean === "") return "" return clean.split(".")[0] || clean } function displayHostName(hostName, dnsName) { var host = String(hostName || "") if (host !== "" && host.toLowerCase() !== "localhost") return host return shortDnsName(dnsName) || host || "Unknown" } function isMullvadPeer(peer) { var hostName = String((peer && peer.HostName) || "").toLowerCase() var dnsName = cleanDnsName((peer && peer.DNSName) || "").toLowerCase() return dnsName.indexOf(".mullvad.ts.net") !== -1 || hostName.indexOf(".mullvad.ts.net") !== -1 } function osIcon(os) { var value = String(os || "").toLowerCase() if (value === "linux") return "󰌽" if (value === "macos" || value === "ios") return "󰀵" if (value === "windows") return "󰍲" if (value === "android") return "󰀲" if (value === "mullvad") return "󰖂" return "󰟀" } function accountLabel(account) { if (!account) return "Unknown account" if (account.nickname) return String(account.nickname) if (account.tailnet) return String(account.tailnet) if (account.account) return String(account.account) return String(account.id || "Unknown account") } function peerFromStatus(id, peer) { return { id: id, HostName: displayHostName(peer.HostName, peer.DNSName), DNSName: cleanDnsName(peer.DNSName), DisplayName: displayHostName(peer.HostName, peer.DNSName), TailscaleIPs: filterIPv4(peer.TailscaleIPs || []), TailscaleIPv6: filterIPv6(peer.TailscaleIPs || []), Online: peer.Online === true, OS: String(peer.OS || ""), Tags: peer.Tags || [], ExitNodeOption: peer.ExitNodeOption === true, ExitNode: peer.ExitNode === true, Mullvad: isMullvadPeer(peer) } } function sliceTableColumn(line, start, end) { var text = String(line || "") if (start < 0 || start >= text.length) return "" if (end < 0) return text.substring(start).trim() return text.substring(start, Math.min(end, text.length)).trim() } function parseExitNodeList(raw) { var lines = String(raw || "").split(/\r?\n/) var header = "" var headerIndex = -1 for (var i = 0; i < lines.length; i++) { if (/^\s*IP\s+HOSTNAME\s+COUNTRY\s+CITY\s+STATUS\s*$/.test(lines[i])) { header = lines[i] headerIndex = i break } } if (headerIndex === -1) return [] var ipStart = header.indexOf("IP") var hostStart = header.indexOf("HOSTNAME") var countryStart = header.indexOf("COUNTRY") var cityStart = header.indexOf("CITY") var statusStart = header.indexOf("STATUS") var byHost = {} for (var j = headerIndex + 1; j < lines.length; j++) { var line = lines[j] if (/^\s*$/.test(line) || /^\s*#/.test(line)) continue var ip = sliceTableColumn(line, ipStart, hostStart) var host = sliceTableColumn(line, hostStart, countryStart) var country = sliceTableColumn(line, countryStart, cityStart) var city = sliceTableColumn(line, cityStart, statusStart) var status = sliceTableColumn(line, statusStart, -1) if (host.indexOf(".mullvad.ts.net") === -1) continue byHost[host] = { id: "mullvad:" + host, HostName: host, DNSName: host, DisplayName: (city && city !== "Any" ? city + ", " : "") + country, TailscaleIPs: ip ? [ip] : [], TailscaleIPv6: [], Online: true, OS: "mullvad", Tags: [], ExitNodeOption: true, ExitNode: status !== "" && status !== "-", Mullvad: true, Country: country, City: city, Status: status } } var result = [] for (var hostName in byHost) result.push(byHost[hostName]) result.sort(function(a, b) { var countryCompare = String(a.Country).localeCompare(String(b.Country)) if (countryCompare !== 0) return countryCompare return String(a.DisplayName).localeCompare(String(b.DisplayName)) }) return result } function mullvadRegionOptions(nodes) { var byRegion = {} var values = Array.isArray(nodes) ? nodes : [] for (var i = 0; i < values.length; i++) { var node = values[i] || {} if (node.Mullvad !== true) continue var country = String(node.Country || "").trim() var city = String(node.City || "").trim() if (country === "") continue if (city === "" || city === "Any") continue var key = country + "\n" + city if (byRegion[key]) continue var option = {} for (var propertyName in node) option[propertyName] = node[propertyName] option.id = "mullvad-region:" + key option.DisplayName = city + ", " + country option.Country = country option.City = city option.MullvadRegion = true byRegion[key] = option } var result = [] for (var name in byRegion) result.push(byRegion[name]) result.sort(function(a, b) { var countryCompare = String(a.Country).localeCompare(String(b.Country)) if (countryCompare !== 0) return countryCompare return String(a.City).localeCompare(String(b.City)) }) return result } function mullvadCountryOptions(nodes) { return mullvadRegionOptions(nodes) } function parseStatus(raw) { var text = String(raw || "").trim() if (text === "") return { ok: true, unavailable: true, message: "Disconnected" } try { var data = JSON.parse(text) var backendState = String(data.BackendState || "Unknown") var self = data.Self || {} var selfIps = filterIPv4(self.TailscaleIPs || data.TailscaleIPs || []) var peers = [] var exitNodes = [] var rawPeers = data.Peer || {} for (var id in rawPeers) { var peer = rawPeers[id] || {} var normalized = peerFromStatus(id, peer) if (normalized.Mullvad) continue if (normalized.Online) { peers.push(normalized) if (normalized.ExitNodeOption) exitNodes.push(normalized) } } peers.sort(function(a, b) { return String(a.HostName).localeCompare(String(b.HostName)) }) exitNodes.sort(function(a, b) { return String(a.HostName).localeCompare(String(b.HostName)) }) return { ok: true, unavailable: false, backendState: backendState, running: backendState === "Running", needsLogin: backendState === "NeedsLogin", authUrl: String(data.AuthURL || ""), selfName: displayHostName(self.HostName, self.DNSName), selfDnsName: cleanDnsName(self.DNSName), selfIp: selfIps.length > 0 ? selfIps[0] : "", peers: peers, exitNodes: exitNodes } } catch (e) { return { ok: false, unavailable: true, message: "Status error", error: "Failed to parse tailscale status" } } } function parseAccounts(raw) { var text = String(raw || "").trim() if (text === "") return { accounts: [], selectedAccountId: "", selectedAccountLabel: "" } try { var parsed = JSON.parse(text) var next = [] var selected = null if (parsed && typeof parsed.length === "number") { for (var i = 0; i < parsed.length; i++) { var rawAccount = parsed[i] || {} var account = { id: String(rawAccount.id || rawAccount.ID || ""), nickname: String(rawAccount.nickname || rawAccount.Nickname || rawAccount.name || rawAccount.Name || ""), tailnet: String(rawAccount.tailnet || rawAccount.Tailnet || ""), account: String(rawAccount.account || rawAccount.Account || rawAccount.loginName || rawAccount.LoginName || rawAccount.user || rawAccount.User || ""), selected: rawAccount.selected === true || rawAccount.Selected === true } next.push(account) if (account.selected === true) selected = account } } return { accounts: next, selectedAccountId: selected ? String(selected.id || "") : "", selectedAccountLabel: selected ? accountLabel(selected) : "" } } catch (e) { return { accounts: [], selectedAccountId: "", selectedAccountLabel: "" } } } if (typeof module !== "undefined") { module.exports = { filterIPv4: filterIPv4, filterIPv6: filterIPv6, cleanDnsName: cleanDnsName, shortDnsName: shortDnsName, displayHostName: displayHostName, osIcon: osIcon, accountLabel: accountLabel, isMullvadPeer: isMullvadPeer, peerFromStatus: peerFromStatus, parseExitNodeList: parseExitNodeList, mullvadRegionOptions: mullvadRegionOptions, mullvadCountryOptions: mullvadCountryOptions, parseStatus: parseStatus, parseAccounts: parseAccounts } }