9remote 0.1.64 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ import dns from "dns";
2
+
3
+ // Bypass broken macOS system DNS resolver for long hostnames (e.g. trycloudflare).
4
+ // Skip localhost/IP to avoid slow/hanging DNS resolve on Windows.
5
+ const _originalLookup = dns.lookup;
6
+ const IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$|^::1$|^[0-9a-f:]+$/i;
7
+ dns.lookup = (hostname, options, cb) => {
8
+ if (typeof options === "function") { cb = options; options = {}; }
9
+ if (hostname === "localhost" || IP_REGEX.test(hostname)) {
10
+ return _originalLookup(hostname, options, cb);
11
+ }
12
+ dns.resolve4(hostname, (err, addrs) => {
13
+ if (err) return _originalLookup(hostname, options, cb);
14
+ if (options.all) {
15
+ cb(null, addrs.map(a => ({ address: a, family: 4 })));
16
+ } else {
17
+ cb(null, addrs[0], 4);
18
+ }
19
+ });
20
+ };
21
+
22
+ /**
23
+ * Shared constants for agent server + CLI
24
+ */
25
+
26
+ // Connection step states (UI progress tracking)
27
+ export const STEP = {
28
+ STOPPED: 0,
29
+ PREPARING: 1,
30
+ CONNECTING: 2,
31
+ TUNNELING: 3,
32
+ VERIFYING: 4,
33
+ READY: 5,
34
+ };
35
+
36
+ // Debug flags — toggle diagnostic displays without touching logic
37
+ export const DEBUG = {
38
+ showTunnelUrlInMenu: false,
39
+ };
40
+
41
+ // macOS permission poll — TCC has no change event, so we poll periodically.
42
+ // Fast cadence kicks in right after user clicks "Grant" so UI reflects the toggle quickly.
43
+ export const PERMISSION_POLL_MS = 5000;
44
+ export const PERMISSION_POLL_FAST_MS = 1000;
45
+ export const PERMISSION_POLL_FAST_DURATION = 60000;
46
+
47
+ // Browser-like headers to avoid CDN/firewall blocks
48
+ const BROWSER_HEADERS = {
49
+ "Accept": "application/json, text/plain, */*",
50
+ "Accept-Language": "en-US,en;q=0.9",
51
+ "Cache-Control": "no-cache",
52
+ "Pragma": "no-cache",
53
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
54
+ };
55
+
56
+ /**
57
+ * fetch() wrapper with browser-like headers for external requests
58
+ */
59
+ export function browserFetch(url, options = {}) {
60
+ return fetch(url, {
61
+ ...options,
62
+ headers: { ...BROWSER_HEADERS, ...options.headers },
63
+ });
64
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Device approval manager — persists approved deviceIds to ~/.9remote/approvedDevices.json
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+
9
+ const STATE_DIR = join(homedir(), ".9remote");
10
+ const DEVICES_FILE = join(STATE_DIR, "approvedDevices.json");
11
+ const CONFIG_FILE = join(STATE_DIR, "config.json");
12
+
13
+ // deviceId -> { approvedAt }
14
+ let approvedDevices = new Map();
15
+
16
+ // Auto-approve new device connections (default false). Persisted to config.json.
17
+ let autoApprove = false;
18
+
19
+ // Pending approval requests: socketId -> { deviceId, ip }
20
+ const pendingApprovals = new Map();
21
+
22
+ // Rejected devices (RAM only, cleared on restart): deviceId -> { ip, rejectedAt, socketId }
23
+ const rejectedDevices = new Map();
24
+
25
+ function ensureDir() {
26
+ if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
27
+ }
28
+
29
+ export function loadApprovedDevices() {
30
+ try {
31
+ ensureDir();
32
+ if (existsSync(DEVICES_FILE)) {
33
+ const data = JSON.parse(readFileSync(DEVICES_FILE, "utf8"));
34
+ // Migrate from old array format to new map format
35
+ if (Array.isArray(data)) {
36
+ approvedDevices = new Map(data.map((id) => [id, { approvedAt: null }]));
37
+ } else {
38
+ approvedDevices = new Map(Object.entries(data));
39
+ }
40
+ }
41
+ } catch {
42
+ approvedDevices = new Map();
43
+ }
44
+ }
45
+
46
+ function saveApprovedDevices() {
47
+ try {
48
+ ensureDir();
49
+ writeFileSync(DEVICES_FILE, JSON.stringify(Object.fromEntries(approvedDevices), null, 2));
50
+ } catch {}
51
+ }
52
+
53
+ export function isDeviceApproved(deviceId) {
54
+ if (!deviceId) return false;
55
+ return approvedDevices.has(deviceId);
56
+ }
57
+
58
+ export function approveDevice(deviceId) {
59
+ if (!deviceId) return;
60
+ approvedDevices.set(deviceId, { approvedAt: new Date().toISOString() });
61
+ saveApprovedDevices();
62
+ }
63
+
64
+ export function removeDevice(deviceId) {
65
+ approvedDevices.delete(deviceId);
66
+ saveApprovedDevices();
67
+ }
68
+
69
+ export function getApprovedDevices() {
70
+ return [...approvedDevices.entries()].map(([id, meta]) => ({ deviceId: id, ...meta }));
71
+ }
72
+
73
+ // Pending approval queue
74
+ export function addPendingApproval(socketId, data) {
75
+ pendingApprovals.set(socketId, data);
76
+ }
77
+
78
+ export function getPendingApproval(socketId) {
79
+ return pendingApprovals.get(socketId) || null;
80
+ }
81
+
82
+ export function removePendingApproval(socketId) {
83
+ pendingApprovals.delete(socketId);
84
+ }
85
+
86
+ export function isDevicePending(deviceId) {
87
+ if (!deviceId) return false;
88
+ for (const data of pendingApprovals.values()) {
89
+ if (data.deviceId === deviceId) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ export function getAllPendingApprovals() {
95
+ return [...pendingApprovals.entries()].map(([socketId, data]) => ({ socketId, ...data }));
96
+ }
97
+
98
+ // Rejected devices (pending re-approval from Clients list)
99
+ export function markDeviceRejected(deviceId, data) {
100
+ if (!deviceId) return;
101
+ rejectedDevices.set(deviceId, { ...data, rejectedAt: new Date().toISOString() });
102
+ }
103
+
104
+ export function isDeviceRejected(deviceId) {
105
+ if (!deviceId) return false;
106
+ return rejectedDevices.has(deviceId);
107
+ }
108
+
109
+ export function updateRejectedSocket(deviceId, socketId, ip) {
110
+ const entry = rejectedDevices.get(deviceId);
111
+ if (entry) rejectedDevices.set(deviceId, { ...entry, socketId, ip });
112
+ }
113
+
114
+ export function getRejectedDevices() {
115
+ return [...rejectedDevices.entries()].map(([deviceId, meta]) => ({ deviceId, ...meta }));
116
+ }
117
+
118
+ export function clearRejectedDevice(deviceId) {
119
+ rejectedDevices.delete(deviceId);
120
+ }
121
+
122
+ // ── Auto-approve setting (persisted) ────────────────────────────────
123
+ function readConfig() {
124
+ try {
125
+ if (existsSync(CONFIG_FILE)) return JSON.parse(readFileSync(CONFIG_FILE, "utf8")) || {};
126
+ } catch {}
127
+ return {};
128
+ }
129
+
130
+ function writeConfig(cfg) {
131
+ try {
132
+ ensureDir();
133
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
134
+ } catch {}
135
+ }
136
+
137
+ export function loadAutoApprove() {
138
+ autoApprove = !!readConfig().autoApprove;
139
+ return autoApprove;
140
+ }
141
+
142
+ export function isAutoApprove() {
143
+ return autoApprove;
144
+ }
145
+
146
+ export function setAutoApprove(enabled) {
147
+ autoApprove = !!enabled;
148
+ const cfg = readConfig();
149
+ cfg.autoApprove = autoApprove;
150
+ writeConfig(cfg);
151
+ return autoApprove;
152
+ }
package/lib/router.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Lightweight config-driven HTTP router
3
+ * - Route table with path, method, handler
4
+ * - Public route whitelist (bypass localhost check)
5
+ * - Auto body parsing for POST/PUT
6
+ */
7
+
8
+ import { parse } from "url";
9
+
10
+ function readBody(req) {
11
+ return new Promise((resolve) => {
12
+ let body = "";
13
+ req.on("data", (chunk) => body += chunk);
14
+ req.on("end", () => resolve(body));
15
+ });
16
+ }
17
+
18
+ export function jsonOk(res, data = { ok: true }) {
19
+ res.setHeader("Content-Type", "application/json");
20
+ res.writeHead(200);
21
+ res.end(JSON.stringify(data));
22
+ }
23
+
24
+ export function jsonErr(res, code, msg) {
25
+ res.setHeader("Content-Type", "application/json");
26
+ res.writeHead(code);
27
+ res.end(JSON.stringify({ error: msg }));
28
+ }
29
+
30
+ /**
31
+ * Parse JSON body with error handling, returns parsed object or null
32
+ */
33
+ export async function parseJsonBody(req, res) {
34
+ try {
35
+ const raw = await readBody(req);
36
+ return JSON.parse(raw || "{}");
37
+ } catch {
38
+ jsonErr(res, 400, "Invalid JSON");
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Create router from route config
45
+ * @param {Array<{path: string, method: string, handler: Function, public?: boolean}>} routes
46
+ * @param {Object} options
47
+ * @param {Function} options.fallback - Called when no route matches
48
+ * @returns {Function} HTTP request handler (req, res) => void
49
+ */
50
+ export function createRouter(routes, { fallback } = {}) {
51
+ // Pre-build lookup map: "METHOD:/path" → { handler, public }
52
+ const exactMap = new Map();
53
+ const prefixRoutes = [];
54
+
55
+ for (const route of routes) {
56
+ const methods = route.method === "*" ? ["GET", "POST", "PUT", "DELETE"] : [route.method];
57
+ for (const m of methods) {
58
+ if (route.path.endsWith("/*")) {
59
+ prefixRoutes.push({ ...route, prefix: route.path.slice(0, -2), method: m });
60
+ } else {
61
+ exactMap.set(`${m}:${route.path}`, route);
62
+ }
63
+ }
64
+ }
65
+
66
+ // Collect public paths for fast localhost check
67
+ const publicExact = new Set();
68
+ const publicPrefixes = [];
69
+ for (const route of routes) {
70
+ if (!route.public) continue;
71
+ if (route.path.endsWith("/*")) {
72
+ publicPrefixes.push(route.path.slice(0, -2));
73
+ } else {
74
+ publicExact.add(route.path);
75
+ }
76
+ }
77
+
78
+ function isPublic(pathname) {
79
+ if (publicExact.has(pathname)) return true;
80
+ return publicPrefixes.some((p) => pathname.startsWith(p));
81
+ }
82
+
83
+ return async (req, res) => {
84
+ const parsedUrl = parse(req.url, true);
85
+ const { pathname, search } = parsedUrl;
86
+
87
+ // Localhost guard — block non-public routes from remote/tunnel
88
+ const isTunnel = !!req.headers["cf-connecting-ip"];
89
+ const isLocal = req.socket.remoteAddress === "127.0.0.1" || req.socket.remoteAddress === "::1";
90
+ if (!isPublic(pathname) && (isTunnel || !isLocal)) {
91
+ jsonErr(res, 403, "Forbidden");
92
+ return;
93
+ }
94
+
95
+ // Exact match
96
+ const key = `${req.method}:${pathname}`;
97
+ const route = exactMap.get(key);
98
+ if (route) {
99
+ try {
100
+ await route.handler(req, res, { pathname, query: parsedUrl.query, search });
101
+ } catch (err) {
102
+ console.error("Error:", req.url, err);
103
+ jsonErr(res, 500, err.message);
104
+ }
105
+ return;
106
+ }
107
+
108
+ // Prefix match
109
+ for (const pr of prefixRoutes) {
110
+ if ((pr.method === req.method) && pathname.startsWith(pr.prefix)) {
111
+ try {
112
+ await pr.handler(req, res, { pathname, query: parsedUrl.query, search });
113
+ } catch (err) {
114
+ console.error("Error:", req.url, err);
115
+ jsonErr(res, 500, err.message);
116
+ }
117
+ return;
118
+ }
119
+ }
120
+
121
+ // Fallback (static files, etc.)
122
+ if (fallback) {
123
+ try {
124
+ await fallback(req, res, { pathname, search });
125
+ } catch (err) {
126
+ console.error("Error:", req.url, err);
127
+ jsonErr(res, 500, err.message);
128
+ }
129
+ return;
130
+ }
131
+
132
+ jsonErr(res, 404, "Not found");
133
+ };
134
+ }
package/lib/socketio.js CHANGED
@@ -6,7 +6,22 @@ import { homedir } from "os";
6
6
  import { setupTerminalSocket } from "../features/terminal/terminalSocket.js";
7
7
  import { setupRemoteSocket, checkRemoteAvailable } from "../features/remote/remoteSocket.js";
8
8
  import { setupFileExplorerSocket } from "../features/fileExplorer/fileExplorerSocket.js";
9
- import { trackConnection, untrackConnection, pushUiLog, clearOneTimeKey } from "../index.js";
9
+ import { trackConnection, untrackConnection, pushUiLog, clearOneTimeKey, pushUiEvent, setRemoteAvailable } from "../api/ui.js";
10
+ import {
11
+ loadApprovedDevices,
12
+ isDeviceApproved,
13
+ isDevicePending,
14
+ approveDevice,
15
+ addPendingApproval,
16
+ removePendingApproval,
17
+ getPendingApproval,
18
+ markDeviceRejected,
19
+ isDeviceRejected,
20
+ updateRejectedSocket,
21
+ clearRejectedDevice,
22
+ loadAutoApprove,
23
+ isAutoApprove
24
+ } from "./deviceApproval.js";
10
25
 
11
26
  function loadApiKey() {
12
27
  try {
@@ -24,7 +39,107 @@ export function getIO() {
24
39
  return ioInstance;
25
40
  }
26
41
 
42
+ /** Setup features on an approved socket */
43
+ function setupSocketFeatures(socket) {
44
+ // Clear one-time key if used
45
+ if (socket.handshake.auth?.tempKey) {
46
+ pushUiLog("One-time key used \u2014 clearing from UI");
47
+ clearOneTimeKey();
48
+ }
49
+ }
50
+
51
+ /** Approve a pending socket by socketId */
52
+ export function approveSocketDevice(socketId) {
53
+ const io = ioInstance;
54
+ if (!io) return false;
55
+
56
+ const socket = io.sockets.sockets.get(socketId);
57
+ const pending = getPendingApproval(socketId);
58
+ console.log(`[DEBUG-APPROVE] socketId=${socketId}, socketExists=${!!socket}, pendingExists=${!!pending}`);
59
+ if (!socket || !pending) return false;
60
+
61
+ // Save device as approved; clear any prior rejection
62
+ approveDevice(pending.deviceId);
63
+ removePendingApproval(socketId);
64
+ clearRejectedDevice(pending.deviceId);
65
+
66
+ // Unlock socket + notify client
67
+ socket.data.approved = true;
68
+ console.log(`[DEBUG-APPROVE] Emitting device:approved to ${socketId}`);
69
+ socket.emit("device:approved");
70
+
71
+ // Setup features
72
+ setupSocketFeatures(socket);
73
+ pushUiLog(`Device approved: ${pending.deviceId.slice(0, 8)}...`);
74
+
75
+ return true;
76
+ }
77
+
78
+ /** Approve a previously-rejected device by deviceId (from Clients list) */
79
+ export function approveRejectedDevice(deviceId) {
80
+ const io = ioInstance;
81
+ if (!io || !deviceId) return false;
82
+
83
+ approveDevice(deviceId);
84
+ clearRejectedDevice(deviceId);
85
+
86
+ // Notify any active socket for this device
87
+ for (const socket of io.sockets.sockets.values()) {
88
+ if (socket.handshake.auth?.deviceId === deviceId) {
89
+ socket.data.approved = true;
90
+ socket.emit("device:approved");
91
+ setupSocketFeatures(socket);
92
+ }
93
+ }
94
+ pushUiLog(`Device approved from pending: ${deviceId.slice(0, 8)}...`);
95
+ return true;
96
+ }
97
+
98
+ /** Disconnect all active sockets belonging to a deviceId (device stays approved) */
99
+ export function disconnectDeviceSockets(deviceId) {
100
+ const io = ioInstance;
101
+ if (!io || !deviceId) return 0;
102
+ let count = 0;
103
+ for (const socket of io.sockets.sockets.values()) {
104
+ if (socket.handshake.auth?.deviceId === deviceId) {
105
+ socket.disconnect(true);
106
+ count++;
107
+ }
108
+ }
109
+ if (count) pushUiLog(`Disconnected ${count} socket(s) for device ${deviceId.slice(0, 8)}...`);
110
+ return count;
111
+ }
112
+
113
+ /** Reject a pending socket by socketId (remember deviceId in RAM as pending) */
114
+ export function rejectSocketDevice(socketId) {
115
+ const io = ioInstance;
116
+ if (!io) return false;
117
+
118
+ const pending = getPendingApproval(socketId);
119
+ const socket = io.sockets.sockets.get(socketId);
120
+ removePendingApproval(socketId);
121
+
122
+ // Remember rejection in RAM so it shows up in Clients list as pending
123
+ if (pending?.deviceId) {
124
+ markDeviceRejected(pending.deviceId, { ip: pending.ip, socketId });
125
+ }
126
+
127
+ if (socket) {
128
+ socket.emit("device:rejected");
129
+ socket.disconnect(true);
130
+ }
131
+
132
+ // Notify UI to refresh pending/approved list
133
+ pushUiEvent("deviceApproval", { action: "refresh" });
134
+ pushUiLog(`Device rejected: ${pending?.deviceId?.slice(0, 8) || "unknown"}...`);
135
+ return true;
136
+ }
137
+
27
138
  export async function setupSocketIO(server) {
139
+ // Load approved devices + auto-approve setting from disk
140
+ loadApprovedDevices();
141
+ loadAutoApprove();
142
+
28
143
  const io = new Server(server, {
29
144
  cors: {
30
145
  origin: "*",
@@ -36,41 +151,82 @@ export async function setupSocketIO(server) {
36
151
  allowEIO3: true,
37
152
  allowUpgrades: true,
38
153
  pingTimeout: 60000,
39
- pingInterval: 25000
154
+ pingInterval: 25000,
155
+ maxHttpBufferSize: 1e8
40
156
  });
41
157
 
42
- // Verify apiKey on every Socket.IO connection
43
- // io.use((socket, next) => {
44
- // const serverKey = loadApiKey();
45
- // console.log("🚀 ~ setupSocketIO ~ serverKey:", serverKey)
46
- // // If no key configured yet (first run), allow through
47
- // if (!serverKey) return next();
48
- // const clientKey = socket.handshake.auth?.apiKey;
49
- // console.log("🚀 ~ setupSocketIO ~ clientKey:", clientKey)
50
- // if (clientKey === serverKey) return next();
51
- // next(new Error("unauthorized"));
52
- // });
53
-
54
158
  // Check remote availability at startup
55
- await checkRemoteAvailable();
159
+ const hasRemote = await checkRemoteAvailable();
160
+ setRemoteAvailable(hasRemote);
56
161
 
57
- // Track connections for UI display + log
162
+ // Track connections + device approval
58
163
  io.on("connection", (socket) => {
59
164
  const ip = socket.handshake.headers["x-forwarded-for"] || socket.handshake.address || "unknown";
60
- trackConnection(socket.id, ip);
61
- pushUiLog(`Client connected: ${ip}`);
62
-
63
- // If client connected using a one-time key, clear it from UI state
64
- pushUiLog(`Auth received: tempKey=${socket.handshake.auth?.tempKey ?? "null"}`);
65
- if (socket.handshake.auth?.tempKey) {
66
- pushUiLog(`One-time key used — clearing from UI`);
67
- clearOneTimeKey();
68
- }
165
+ const deviceId = socket.handshake.auth?.deviceId || null;
166
+
167
+ // Block all events from unapproved sockets (except device:clientReady)
168
+ socket.data.approved = false;
169
+ socket.use((packet, next) => {
170
+ if (socket.data.approved) return next();
171
+ const event = packet[0];
172
+ if (event === "device:clientReady" || event === "disconnect") return next();
173
+ return next(new Error("Device not approved"));
174
+ });
175
+
176
+ trackConnection(socket.id, ip, deviceId);
177
+ pushUiLog(`Client connected: ${ip} (device: ${deviceId?.slice(0, 8) || "none"})`);
69
178
 
70
179
  socket.on("disconnect", (reason) => {
71
180
  untrackConnection(socket.id);
181
+ removePendingApproval(socket.id);
72
182
  pushUiLog(`Client disconnected: ${ip} (${reason})`);
73
183
  });
184
+
185
+ // Check device approval
186
+ if (deviceId && isDeviceApproved(deviceId)) {
187
+ // Known device — allow immediately
188
+ pushUiLog(`Device recognized: ${deviceId.slice(0, 8)}...`);
189
+ socket.data.approved = true;
190
+ setupSocketFeatures(socket);
191
+ } else if (deviceId && isDeviceRejected(deviceId)) {
192
+ // Previously rejected — keep socket unapproved, no modal, update socketId for later approve
193
+ updateRejectedSocket(deviceId, socket.id, ip);
194
+ pushUiLog(`Rejected device reconnected: ${deviceId.slice(0, 8)} — waiting in Clients list`);
195
+ socket.emit("device:rejected");
196
+ pushUiEvent("deviceApproval", { action: "refresh" });
197
+ } else if (deviceId && isAutoApprove()) {
198
+ // Auto-approve enabled — skip pending flow, approve immediately
199
+ approveDevice(deviceId);
200
+ clearRejectedDevice(deviceId);
201
+ socket.data.approved = true;
202
+ pushUiLog(`Auto-approved device: ${deviceId.slice(0, 8)}...`);
203
+ setupSocketFeatures(socket);
204
+ // Notify client after it signals ready so listeners are attached
205
+ socket.once("device:clientReady", () => socket.emit("device:approved"));
206
+ pushUiEvent("deviceApproval", { action: "refresh" });
207
+ } else {
208
+ // Unknown device — hold and request approval
209
+ pushUiLog(`Unknown device: ${deviceId?.slice(0, 8) || "no-id"} — waiting for approval`);
210
+ // Skip if same deviceId already pending (client reconnected)
211
+ if (isDevicePending(deviceId)) {
212
+ pushUiLog(`Device ${deviceId?.slice(0, 8)} already pending, ignoring duplicate`);
213
+ socket.disconnect(true);
214
+ return;
215
+ }
216
+
217
+ addPendingApproval(socket.id, { deviceId, ip });
218
+
219
+ // Wait for client to signal ready before emitting approval request
220
+ socket.once("device:clientReady", () => {
221
+ socket.emit("device:pendingApproval");
222
+ pushUiEvent("deviceApproval", {
223
+ socketId: socket.id,
224
+ deviceId,
225
+ ip,
226
+ action: "pending"
227
+ });
228
+ });
229
+ }
74
230
  });
75
231
 
76
232
  // Setup Terminal + Remote on same root namespace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "9remote",
3
- "version": "0.1.64",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "description": "Remote terminal access from anywhere",
6
6
  "main": "index.js",
@@ -31,18 +31,22 @@
31
31
  ],
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
+ "@julusian/jpeg-turbo": "^2.3.0",
34
35
  "bufferutil": "^4.1.0",
35
36
  "chalk": "^5.4.1",
37
+ "edge-tts-node": "^1.5.7",
36
38
  "http-proxy": "^1.18.1",
37
39
  "inquirer": "^9.3.8",
38
40
  "node-datachannel": "^0.32.1",
39
41
  "node-machine-id": "^1.1.12",
42
+ "node-screenshots": "^0.2.8",
40
43
  "ora": "^8.1.1",
41
44
  "preact": "^10.26.2",
42
45
  "qrcode": "^1.5.4",
43
46
  "qrcode-terminal": "^0.12.0",
44
47
  "sharp": "^0.33.5",
45
48
  "socket.io": "^4.8.3",
49
+ "systray": "^1.0.5",
46
50
  "utf-8-validate": "^6.0.6",
47
51
  "web-push": "^3.6.7"
48
52
  },
@@ -58,6 +62,7 @@
58
62
  "postcss": "^8.5.3",
59
63
  "tailwindcss": "^3.4.17",
60
64
  "vite": "^6.2.2",
65
+ "vite-plugin-javascript-obfuscator": "^3.1.0",
61
66
  "wait-on": "^9.0.4"
62
67
  }
63
68
  }