commit b984b55bc07b559f77a8b20fa9d4ef8947fcc5a6 Author: sriram Date: Fri Jun 5 17:29:09 2026 +0530 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93bad00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ + +# Logs +*.log +npm-debug.log* + +# Database files +*.db +*.db-shm +*.db-wal +*.sqlite + +# Certificates & keys +*.pem +*.key +*.crt + +# Environment files +.env +.env.* + +# Build output +dist/ +build/ +out/ + +# OS files +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ diff --git a/.npmignore.note b/.npmignore.note new file mode 100644 index 0000000..e69de29 diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..fd76153 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,42 @@ +# Install & Test — Remote Access Platform + +## Prerequisite +Install **Node.js 22.5 or newer** (LTS is fine): https://nodejs.org +(The installer adds `node` and `npm` to your PATH.) + +## Steps + +1. **Unzip** this folder anywhere (e.g. `C:\remote-access-app`). +2. Double-click **`SETUP.bat`** — installs all dependencies (the agent pulls + Electron, so the first run takes a few minutes). +3. Double-click **`Start-Server.bat`** — leave this window open. +4. Open **http://localhost:8090** in your browser: + - Click **Register team**, enter an email + password. + - Add the shown **2FA secret** to an authenticator app (Google Authenticator, + Authy, 1Password), enter a code to finish, then log in. + - In **Machines**, type a name and click **Enroll machine** — copy the + `AGENT_ENROLL_TOKEN` it shows. +5. Double-click **`Start-Agent.bat`**, accept the default server URL, and paste + the token. The agent window appears and the machine turns **green** in the + console. +6. In the console, click **Connect**. The agent shows a **consent prompt** — + click **Allow**. You'll see the live screen and can control it; a red banner + stays on the host while the session is active. + +> To test on **two PCs**, run the server on one and use that PC's IP as the +> server URL on the other (e.g. `http://192.168.1.50:8090`). Both must be on the +> same network for direct connection (a TURN relay is the next step for +> internet-wide use). + +## Notes +- **Same-machine test:** you can run the server, browser, and agent all on one PC + to see the full flow (you'll be controlling your own screen). +- **Input control** uses the `nut-js` library. If it failed to install, the agent + still streams the screen but logs input instead of injecting it — re-run + `SETUP.bat` on a machine with build tools to enable full control. +- To stop: close the agent window and press `Ctrl+C` in the server window. + +## Troubleshooting +- *"Node.js is not installed"* → install from nodejs.org, reopen `SETUP.bat`. +- *Browser can't reach localhost:8090* → make sure `Start-Server.bat` is still running. +- *Machine stays offline* → check the token was pasted fully and the server URL is correct. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4205b3 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Remote Access Platform — Alpha + +A self-hostable remote support platform for IT teams: technicians log in to a web +console, see their team's machines, and start a screen-share + remote-control +session to any online machine after the end user grants consent. Built to the +spec in `PRD-remote-access-platform.md`. + +This alpha implements the PRD's P0 requirements: authenticated console, **MFA**, +**RBAC**, **machine enrollment**, **per-session consent**, **WebRTC screen +streaming + remote input**, and an **immutable audit log**. + +``` +remote-access-app/ +├── server/ Backend: HTTP API + WebSocket signaling + SQLite +│ ├── server.js Auth, MFA, machines, audit API + signaling broker +│ ├── auth.js scrypt passwords, TOTP MFA, tokens (no external auth deps) +│ ├── db.js Schema via Node's built-in node:sqlite +│ ├── public/ Web console (index.html) + remote viewer (viewer.html) +│ └── test/e2e.js 26-check end-to-end test of the whole backend flow +└── agent/ Native host agent (Electron) + ├── main.js Consent window, screen source, OS input injection + ├── input/inject.js Mouse/keyboard injection via nut-js (Win32 SendInput) + └── renderer/ Agent UI + WebRTC screen capture +``` + +## Quick start + +### 1. Server (any OS, Node 22.5+) + +```bash +cd server +npm install # only dependency is `ws` +npm start # serves http://localhost:8090 +``` + +Open **http://localhost:8090**, click **Register team**, then set up MFA +(add the shown secret to Google Authenticator / Authy / 1Password and enter a code). +Log in, and enroll a machine — you'll get an `AGENT_ENROLL_TOKEN`. + +### 2. Agent (on the Windows/macOS PC to be controlled) + +```bash +cd agent +npm install # installs Electron + nut-js (input injection) +set SERVER_URL=http://:8090 # Windows +set AGENT_ENROLL_TOKEN= +npm start +``` + +The agent window comes online; the machine shows green in the console. +Click **Connect** in the console — the agent shows a consent prompt. On **Allow**, +the technician sees the live screen and can control it. A red banner stays on the +host screen for the whole session, and every step is written to the audit log. + +> Set a machine to **unattended** at enrollment to skip the consent prompt +> (for servers / headless machines), per the PRD's unattended-access policy. + +## What's tested (in this sandbox) + +``` +cd server && npm test # 26/26 checks pass +cd agent && npm run test:input # 5/5 input-mapping checks pass +``` + +The e2e test drives the real backend: register → enable MFA → login (password + +TOTP) → enroll machine → agent connects → technician requests session → consent → +SDP/ICE relay → session end → audit verification → consent-denial path. + +## What requires real hardware (not testable in a headless sandbox) + +- **OS input injection** runs through `nut-js` (Win32 `SendInput` on Windows, + `CGEvent` on macOS). Without it installed, `inject.js` degrades to a safe + logging no-op so the agent still runs. Verify on a real desktop. +- **Screen capture** uses Electron's `desktopCapturer` / `getDisplayMedia`. + +## Architecture notes + +- **Media is peer-to-peer.** The server only brokers signaling (SDP/ICE) and + consent; screen frames and input never pass through it. Channels are + DTLS-encrypted by WebRTC. +- **NAT traversal** uses a public STUN server. ~10–15% of connections behind + symmetric NATs will need a **TURN relay** (coturn) — the next infra item. +- **Auth** uses scrypt password hashing and RFC-6238 TOTP, implemented on Node's + built-in `crypto` — no `bcrypt`/`jsonwebtoken`/`speakeasy` dependencies. +- **Storage** is `node:sqlite` (built into Node 22.5+), so the backend has a + single runtime dependency (`ws`). + +## Gaps before production (from PRD §5) + +1. **TURN relay** for non-P2P connections (coturn). +2. **macOS/Linux agents** + code-signed installers; packaged Windows binary. +3. **File transfer, clipboard sync, multi-monitor** (PRD P1). +4. **SSO, session recording, SOC 2** (PRD P1/T8). +5. Harden signaling: rate limiting, per-session authz checks, CSRF on cookie API. diff --git a/SETUP.bat b/SETUP.bat new file mode 100644 index 0000000..44cb6ae --- /dev/null +++ b/SETUP.bat @@ -0,0 +1,51 @@ +@echo off +setlocal +title Remote Access Platform - Setup +echo ============================================ +echo Remote Access Platform - Setup +echo ============================================ +echo. + +REM --- Check Node.js --- +where node >nul 2>nul +if errorlevel 1 ( + echo [X] Node.js is not installed. + echo Please install Node.js 22.5 or newer from https://nodejs.org + echo Then run this SETUP.bat again. + echo. + pause + exit /b 1 +) +for /f "tokens=*" %%v in ('node -v') do set NODEV=%%v +echo [OK] Node.js found: %NODEV% +echo. + +REM --- Install server dependencies --- +echo Installing SERVER dependencies... +pushd "%~dp0server" +call npm install --no-audit --no-fund +if errorlevel 1 ( echo [X] Server install failed. & popd & pause & exit /b 1 ) +popd +echo [OK] Server ready. +echo. + +REM --- Install agent dependencies (Electron + input injection) --- +echo Installing AGENT dependencies (Electron download may take a few minutes)... +pushd "%~dp0agent" +call npm install --no-audit --no-fund +if errorlevel 1 ( echo [!] Agent install had issues. Input injection may be limited, but the agent will still run. ) +popd +echo [OK] Agent ready. +echo. + +echo ============================================ +echo Setup complete! +echo ============================================ +echo. +echo Next steps: +echo 1. Double-click Start-Server.bat +echo 2. Open http://localhost:8090 in your browser +echo 3. Register a team, set up 2FA, enroll a machine (copy the token) +echo 4. Double-click Start-Agent.bat and paste the token +echo. +pause diff --git a/Start-Agent.bat b/Start-Agent.bat new file mode 100644 index 0000000..7d025d5 --- /dev/null +++ b/Start-Agent.bat @@ -0,0 +1,18 @@ +@echo off +setlocal +title Remote Access - Agent +echo ============================================ +echo Remote Access Agent +echo ============================================ +echo. +set "DEF_URL=http://localhost:8090" +set /p SERVER_URL=Server URL [%DEF_URL%]: +if "%SERVER_URL%"=="" set SERVER_URL=%DEF_URL% +echo. +set /p AGENT_ENROLL_TOKEN=Paste the enroll token from the console: +if "%AGENT_ENROLL_TOKEN%"=="" ( echo [X] No token entered. & pause & exit /b 1 ) +echo. +echo Connecting to %SERVER_URL% ... +cd /d "%~dp0agent" +call npm start +pause diff --git a/Start-Server.bat b/Start-Server.bat new file mode 100644 index 0000000..0397dc8 --- /dev/null +++ b/Start-Server.bat @@ -0,0 +1,8 @@ +@echo off +title Remote Access - Server +echo Starting server on http://localhost:8090 ... +echo (Keep this window open. Press Ctrl+C to stop.) +echo. +cd /d "%~dp0server" +node server.js +pause diff --git a/agent/.gitignore b/agent/.gitignore new file mode 100644 index 0000000..93bad00 --- /dev/null +++ b/agent/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ + +# Logs +*.log +npm-debug.log* + +# Database files +*.db +*.db-shm +*.db-wal +*.sqlite + +# Certificates & keys +*.pem +*.key +*.crt + +# Environment files +.env +.env.* + +# Build output +dist/ +build/ +out/ + +# OS files +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ diff --git a/agent/input/inject.js b/agent/input/inject.js new file mode 100644 index 0000000..1ad05af --- /dev/null +++ b/agent/input/inject.js @@ -0,0 +1,103 @@ +// OS input injection layer. +// +// Cross-platform mouse/keyboard control via @nut-tree-fork/nut-js (optional +// native dependency). If nut-js isn't installed (e.g. CI, or a sandbox without +// a display), this module degrades to a logging no-op so the rest of the agent +// still runs and can be tested. On Windows, nut-js drives the Win32 SendInput +// API under the hood — the same mechanism TeamViewer/AnyDesk use. + +let nut = null; +try { + // eslint-disable-next-line import/no-extraneous-dependencies + nut = require('@nut-tree-fork/nut-js'); + nut.mouse.config.autoDelayMs = 0; + nut.keyboard.config.autoDelayMs = 0; +} catch { + nut = null; +} + +const available = !!nut; + +// Map browser KeyboardEvent.key values to nut-js Key enum names. +function mapKey(key, code) { + if (!nut) return null; + const K = nut.Key; + const direct = { + 'Enter': K.Enter, 'Backspace': K.Backspace, 'Tab': K.Tab, 'Escape': K.Escape, + ' ': K.Space, 'ArrowLeft': K.Left, 'ArrowRight': K.Right, 'ArrowUp': K.Up, 'ArrowDown': K.Down, + 'Home': K.Home, 'End': K.End, 'PageUp': K.PageUp, 'PageDown': K.PageDown, 'Delete': K.Delete, + 'Control': K.LeftControl, 'Shift': K.LeftShift, 'Alt': K.LeftAlt, 'Meta': K.LeftSuper, + 'CapsLock': K.CapsLock, + }; + if (direct[key] !== undefined) return [direct[key]]; + if (/^F\d{1,2}$/.test(key) && K[key] !== undefined) return [K[key]]; + if (key && key.length === 1) { + const upper = key.toUpperCase(); + if (/[A-Z]/.test(upper) && K[upper] !== undefined) return [K[upper]]; + if (/[0-9]/.test(key) && K['Num' + key] !== undefined) return [K['Num' + key]]; + // Fall back to typing the literal character (handles symbols/shifted chars) + return { type: key }; + } + return null; +} + +async function moveTo(xNorm, yNorm) { + if (!nut) return; + const { width, height } = await nut.screen.getResolution(); + await nut.mouse.setPosition(new nut.Point(Math.round(xNorm * width), Math.round(yNorm * height))); +} + +function buttonEnum(b) { + if (!nut) return null; + return b === 2 ? nut.Button.RIGHT : b === 1 ? nut.Button.MIDDLE : nut.Button.LEFT; +} + +const pressed = new Set(); + +// Inject a single normalized input event coming from the viewer. +async function inject(evt) { + if (!nut) { + if (evt.kind !== 'mousemove') console.log('[input:noop]', JSON.stringify(evt)); + return; + } + try { + switch (evt.kind) { + case 'mousemove': + await moveTo(evt.x, evt.y); break; + case 'mousedown': + await moveTo(evt.x, evt.y); await nut.mouse.pressButton(buttonEnum(evt.button)); break; + case 'mouseup': + await nut.mouse.releaseButton(buttonEnum(evt.button)); break; + case 'dblclick': + await moveTo(evt.x, evt.y); await nut.mouse.doubleClick(nut.Button.LEFT); break; + case 'scroll': + if (evt.dy) await (evt.dy > 0 ? nut.mouse.scrollDown(Math.abs(evt.dy)) : nut.mouse.scrollUp(Math.abs(evt.dy))); + if (evt.dx) await (evt.dx > 0 ? nut.mouse.scrollRight(Math.abs(evt.dx)) : nut.mouse.scrollLeft(Math.abs(evt.dx))); + break; + case 'keydown': { + const m = mapKey(evt.key, evt.code); + if (!m) break; + if (m.type) { await nut.keyboard.type(m.type); break; } + await nut.keyboard.pressKey(...m); m.forEach((k) => pressed.add(k)); + break; + } + case 'keyup': { + const m = mapKey(evt.key, evt.code); + if (!m || m.type) break; + await nut.keyboard.releaseKey(...m); m.forEach((k) => pressed.delete(k)); + break; + } + } + } catch (e) { + console.error('[input] inject error:', e.message); + } +} + +// Safety: release any stuck modifier keys when a session ends. +async function releaseAll() { + if (!nut) { pressed.clear(); return; } + for (const k of pressed) { try { await nut.keyboard.releaseKey(k); } catch {} } + pressed.clear(); +} + +module.exports = { inject, releaseAll, available, mapKey }; diff --git a/agent/input/inject.test.js b/agent/input/inject.test.js new file mode 100644 index 0000000..7209953 --- /dev/null +++ b/agent/input/inject.test.js @@ -0,0 +1,44 @@ +// Unit test for the input mapping logic — runs without a display or nut-js. +// Verifies keymap returns sane shapes and inject() no-ops gracefully. +const inject = require('./inject'); +const assert = require('assert'); + +let pass = 0; +function check(name, cond) { + assert.ok(cond, name); + console.log(' ok -', name); + pass++; +} + +(async () => { + console.log('input layer tests:'); + check('module exposes inject/releaseAll/available', typeof inject.inject === 'function' && typeof inject.releaseAll === 'function'); + check('available is boolean', typeof inject.available === 'boolean'); + + // mapKey returns null when nut-js absent (sandbox) — that is expected & safe. + if (!inject.available) { + check('mapKey is null-safe without nut-js', inject.mapKey('a', 'KeyA') === null); + } else { + check('mapKey letter -> array', Array.isArray(inject.mapKey('a', 'KeyA'))); + check('mapKey Enter -> array', Array.isArray(inject.mapKey('Enter', 'Enter'))); + check('mapKey symbol -> type fallback', inject.mapKey('@', 'Digit2').type === '@'); + } + + // inject() must not throw on any event kind, even with no backend. + for (const evt of [ + { kind: 'mousemove', x: 0.5, y: 0.5 }, + { kind: 'mousedown', button: 0, x: 0.1, y: 0.2 }, + { kind: 'mouseup', button: 0 }, + { kind: 'dblclick', x: 0.3, y: 0.3 }, + { kind: 'scroll', dx: 0, dy: 120 }, + { kind: 'keydown', key: 'a', code: 'KeyA' }, + { kind: 'keyup', key: 'a', code: 'KeyA' }, + ]) { + await inject.inject(evt); + } + check('inject handled all event kinds without throwing', true); + await inject.releaseAll(); + check('releaseAll clears state', true); + + console.log(`\n${pass} checks passed.`); +})(); diff --git a/agent/input/injector.js b/agent/input/injector.js new file mode 100644 index 0000000..9c3f465 --- /dev/null +++ b/agent/input/injector.js @@ -0,0 +1,47 @@ +// OS input injection for Windows via user32 SendInput (koffi FFI). +// On non-Windows platforms, exports a stub that logs instead of injecting, +// so the agent can run in dev/test environments. + +const { toVirtualKey } = require('./keymap'); + +const MOUSEEVENTF_MOVE = 0x0001; +const MOUSEEVENTF_ABSOLUTE = 0x8000; +const MOUSEEVENTF_LEFTDOWN = 0x0002; +const MOUSEEVENTF_LEFTUP = 0x0004; +const MOUSEEVENTF_RIGHTDOWN = 0x0008; +const MOUSEEVENTF_RIGHTUP = 0x0010; +const MOUSEEVENTF_MIDDLEDOWN = 0x0020; +const MOUSEEVENTF_MIDDLEUP = 0x0040; +const MOUSEEVENTF_WHEEL = 0x0800; +const KEYEVENTF_KEYUP = 0x0002; +const INPUT_MOUSE = 0; +const INPUT_KEYBOARD = 1; + +const BUTTON_DOWN = { 0: MOUSEEVENTF_LEFTDOWN, 1: MOUSEEVENTF_MIDDLEDOWN, 2: MOUSEEVENTF_RIGHTDOWN }; +const BUTTON_UP = { 0: MOUSEEVENTF_LEFTUP, 1: MOUSEEVENTF_MIDDLEUP, 2: MOUSEEVENTF_RIGHTUP }; + +function createWindowsInjector() { + const koffi = require('koffi'); + const user32 = koffi.load('user32.dll'); + + const MOUSEINPUT = koffi.struct('MOUSEINPUT', { + dx: 'long', dy: 'long', mouseData: 'int32', + dwFlags: 'uint32', time: 'uint32', dwExtraInfo: 'uintptr_t', + }); + const KEYBDINPUT = koffi.struct('KEYBDINPUT', { + wVk: 'uint16', wScan: 'uint16', + dwFlags: 'uint32', time: 'uint32', dwExtraInfo: 'uintptr_t', + }); + const HARDWAREINPUT = koffi.struct('HARDWAREINPUT', { + uMsg: 'uint32', wParamL: 'uint16', wParamH: 'uint16', + }); + const INPUT_UNION = koffi.union('INPUT_UNION', { + mi: MOUSEINPUT, ki: KEYBDINPUT, hi: HARDWAREINPUT, + }); + const INPUT = koffi.struct('INPUT', { type: 'uint32', u: INPUT_UNION }); + + const SendInput = user32.func('uint32 SendInput(uint32 cInputs, INPUT *pInputs, int cbSize)'); + const INPUT_SIZE = koffi.sizeof(INPUT); + + function sendMouse(mi) { + SendInput(1, [{ type: INPUT_MOUSE, u: { mi: \ No newline at end of file diff --git a/agent/input/keymap.js b/agent/input/keymap.js new file mode 100644 index 0000000..486a44e --- /dev/null +++ b/agent/input/keymap.js @@ -0,0 +1,71 @@ +// Maps browser KeyboardEvent.code values to Windows virtual-key codes. +// Reference: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + +const VK = {}; + +// Letters (KeyA..KeyZ -> 0x41..0x5A) +for (let i = 0; i < 26; i++) VK['Key' + String.fromCharCode(65 + i)] = 0x41 + i; +// Top-row digits (Digit0..Digit9 -> 0x30..0x39) +for (let i = 0; i <= 9; i++) VK['Digit' + i] = 0x30 + i; +// Numpad digits +for (let i = 0; i <= 9; i++) VK['Numpad' + i] = 0x60 + i; +// Function keys +for (let i = 1; i <= 24; i++) VK['F' + i] = 0x70 + (i - 1); + +Object.assign(VK, { + Escape: 0x1b, + Tab: 0x09, + CapsLock: 0x14, + ShiftLeft: 0xa0, + ShiftRight: 0xa1, + ControlLeft: 0xa2, + ControlRight: 0xa3, + AltLeft: 0xa4, + AltRight: 0xa5, + MetaLeft: 0x5b, + MetaRight: 0x5c, + ContextMenu: 0x5d, + Space: 0x20, + Enter: 0x0d, + NumpadEnter: 0x0d, + Backspace: 0x08, + Delete: 0x2e, + Insert: 0x2d, + Home: 0x24, + End: 0x23, + PageUp: 0x21, + PageDown: 0x22, + ArrowUp: 0x26, + ArrowDown: 0x28, + ArrowLeft: 0x25, + ArrowRight: 0x27, + PrintScreen: 0x2c, + ScrollLock: 0x91, + Pause: 0x13, + NumLock: 0x90, + // OEM punctuation (US layout) + Semicolon: 0xba, + Equal: 0xbb, + Comma: 0xbc, + Minus: 0xbd, + Period: 0xbe, + Slash: 0xbf, + Backquote: 0xc0, + BracketLeft: 0xdb, + Backslash: 0xdc, + BracketRight: 0xdd, + Quote: 0xde, + // Numpad operators + NumpadMultiply: 0x6a, + NumpadAdd: 0x6b, + NumpadSubtract: 0x6d, + NumpadDecimal: 0x6e, + NumpadDivide: 0x6f, +}); + +/** @param {string} code KeyboardEvent.code @returns {number|undefined} Windows VK code */ +function toVirtualKey(code) { + return VK[code]; +} + +module.exports = { toVirtualKey, VK }; diff --git a/agent/main.js b/agent/main.js new file mode 100644 index 0000000..2951161 --- /dev/null +++ b/agent/main.js @@ -0,0 +1,66 @@ +// Electron main process for the host agent. +// Owns: the consent window, screen-source selection, and OS input injection. +// WebRTC lives in the renderer (it needs a DOM/navigator); input events arrive +// from the renderer over IPC and are injected here. +const { app, BrowserWindow, ipcMain, desktopCapturer, screen } = require('electron'); +const path = require('path'); +const injector = require('./input/inject'); + +const SERVER_URL = process.env.SERVER_URL || 'http://localhost:8090'; +const ENROLL_TOKEN = process.env.AGENT_ENROLL_TOKEN || ''; + +let win; + +function createWindow() { + win = new BrowserWindow({ + width: 460, + height: 560, + resizable: false, + title: 'Remote Access Agent', + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + }, + }); + win.loadFile(path.join(__dirname, 'renderer', 'agent.html')); + // Pass config to the renderer once loaded + win.webContents.on('did-finish-load', () => { + win.webContents.send('config', { serverUrl: SERVER_URL, enrollToken: ENROLL_TOKEN }); + }); +} + +// ---- Screen source for getDisplayMedia (Electron requires a handler) ---- +function registerDisplayMediaHandler() { + const { session } = require('electron'); + session.defaultSession.setDisplayMediaRequestHandler((request, callback) => { + desktopCapturer.getSources({ types: ['screen'] }).then((sources) => { + // Default to the primary display; production would let the user pick. + callback({ video: sources[0], audio: 'loopback' }); + }); + }, { useSystemPicker: false }); +} + +// ---- IPC: renderer forwards remote input events here for OS injection ---- +ipcMain.on('inject-input', (_e, evt) => { + injector.inject(evt); +}); +ipcMain.on('session-ended', () => { + injector.releaseAll(); +}); +ipcMain.handle('get-primary-size', () => { + const { size } = screen.getPrimaryDisplay(); + return size; +}); + +app.whenReady().then(() => { + registerDisplayMediaHandler(); + createWindow(); + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/agent/package-lock.json b/agent/package-lock.json new file mode 100644 index 0000000..a800b01 --- /dev/null +++ b/agent/package-lock.json @@ -0,0 +1,2491 @@ +{ + "name": "remote-access-agent", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "remote-access-agent", + "version": "0.2.0", + "dependencies": { + "ws": "^8.18.0" + }, + "devDependencies": { + "electron": "^31.0.0" + }, + "optionalDependencies": { + "@nut-tree-fork/nut-js": "^4.2.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@jimp/bmp": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz", + "integrity": "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.12.tgz", + "integrity": "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "isomorphic-fetch": "^3.0.0", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.6.0" + } + }, + "node_modules/@jimp/custom": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", + "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/core": "^0.22.12" + } + }, + "node_modules/@jimp/gif": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.22.12.tgz", + "integrity": "sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "gifwrap": "^0.10.1", + "omggif": "^1.0.9" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/jpeg": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.22.12.tgz", + "integrity": "sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "jpeg-js": "^0.4.4" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", + "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", + "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.22.12.tgz", + "integrity": "sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", + "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "tinycolor2": "^1.6.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.22.12.tgz", + "integrity": "sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.22.12.tgz", + "integrity": "sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", + "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.22.12.tgz", + "integrity": "sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.22.12.tgz", + "integrity": "sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.12.tgz", + "integrity": "sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.22.12.tgz", + "integrity": "sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-rotate": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-gaussian": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.12.tgz", + "integrity": "sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-invert": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.22.12.tgz", + "integrity": "sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.22.12.tgz", + "integrity": "sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-normalize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.22.12.tgz", + "integrity": "sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.22.12.tgz", + "integrity": "sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "load-bmfont": "^1.4.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", + "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", + "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-scale": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", + "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-shadow": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.22.12.tgz", + "integrity": "sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blur": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.22.12.tgz", + "integrity": "sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-color": ">=0.8.0", + "@jimp/plugin-resize": ">=0.8.0" + } + }, + "node_modules/@jimp/plugins": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.22.12.tgz", + "integrity": "sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/plugin-blit": "^0.22.12", + "@jimp/plugin-blur": "^0.22.12", + "@jimp/plugin-circle": "^0.22.12", + "@jimp/plugin-color": "^0.22.12", + "@jimp/plugin-contain": "^0.22.12", + "@jimp/plugin-cover": "^0.22.12", + "@jimp/plugin-crop": "^0.22.12", + "@jimp/plugin-displace": "^0.22.12", + "@jimp/plugin-dither": "^0.22.12", + "@jimp/plugin-fisheye": "^0.22.12", + "@jimp/plugin-flip": "^0.22.12", + "@jimp/plugin-gaussian": "^0.22.12", + "@jimp/plugin-invert": "^0.22.12", + "@jimp/plugin-mask": "^0.22.12", + "@jimp/plugin-normalize": "^0.22.12", + "@jimp/plugin-print": "^0.22.12", + "@jimp/plugin-resize": "^0.22.12", + "@jimp/plugin-rotate": "^0.22.12", + "@jimp/plugin-scale": "^0.22.12", + "@jimp/plugin-shadow": "^0.22.12", + "@jimp/plugin-threshold": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.22.12.tgz", + "integrity": "sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "pngjs": "^6.0.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/tiff": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.22.12.tgz", + "integrity": "sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==", + "license": "MIT", + "optional": true, + "dependencies": { + "utif2": "^4.0.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/types": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.22.12.tgz", + "integrity": "sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/bmp": "^0.22.12", + "@jimp/gif": "^0.22.12", + "@jimp/jpeg": "^0.22.12", + "@jimp/png": "^0.22.12", + "@jimp/tiff": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/utils": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.12.tgz", + "integrity": "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/@nut-tree-fork/default-clipboard-provider": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/default-clipboard-provider/-/default-clipboard-provider-4.2.6.tgz", + "integrity": "sha512-Hzqj57rheIMGtsS4zK4//kOhaX5FxMluOiz+4TVaHXx+idZS/bPhZwd8e6o1w1GT0PVJOUIP+4CdUe//k5VRig==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "clipboardy": "2.3.0" + } + }, + "node_modules/@nut-tree-fork/libnut": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut/-/libnut-4.2.6.tgz", + "integrity": "sha512-2FCiTBokMGrMl4eL/trEIO+mtpkXpdPHoVKdTBmW8UBIbhCbrCKmnXb2skWGfVs+U3q7o5EYDjVTNUYaUWbaxQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@nut-tree-fork/libnut-darwin": "2.7.5", + "@nut-tree-fork/libnut-linux": "2.7.5", + "@nut-tree-fork/libnut-win32": "2.7.5" + }, + "engines": { + "node": ">=10.15.3" + } + }, + "node_modules/@nut-tree-fork/libnut-darwin": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut-darwin/-/libnut-darwin-2.7.5.tgz", + "integrity": "sha512-LbqtPtMPTJUcg4XoPP2jsU1wc8flBcGyKTerKsIfK9cD7nBHROnO0QksbrsbSWEpLym8T8fRtuU7XEY83l6Z2Q==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "bindings": "1.5.0" + }, + "engines": { + "node": ">=10.15.3" + }, + "optionalDependencies": { + "@nut-tree-fork/node-mac-permissions": "2.2.1" + } + }, + "node_modules/@nut-tree-fork/libnut-linux": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut-linux/-/libnut-linux-2.7.5.tgz", + "integrity": "sha512-uxaXEcRKnFObAljsoR6tLOBUU1dJ2sctloG6gFgCBGN7+k6Jdv6jZfOuNjd/fpdq2C5WPMm0rtn9EE7h5J3Jcg==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "bindings": "1.5.0" + }, + "engines": { + "node": ">=10.15.3" + }, + "optionalDependencies": { + "@nut-tree-fork/node-mac-permissions": "2.2.1" + } + }, + "node_modules/@nut-tree-fork/libnut-win32": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/libnut-win32/-/libnut-win32-2.7.5.tgz", + "integrity": "sha512-yqC87zvmFcDPwFrRU40DYhN0xmEVM3aSkOuyF0IX+y1x+HWSu/i0PNklATpPBhGid3QVb/TOHuVoaraMrUFCNw==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "bindings": "1.5.0" + }, + "engines": { + "node": ">=10.15.3" + }, + "optionalDependencies": { + "@nut-tree-fork/node-mac-permissions": "2.2.1" + } + }, + "node_modules/@nut-tree-fork/node-mac-permissions": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/node-mac-permissions/-/node-mac-permissions-2.2.1.tgz", + "integrity": "sha512-iSfOTDiBZ7VDa17PoQje5rUaZSvSAaq+XEyXCmhPuQwV5XuNU02Grv6oFhsdpz89w7+UvB/8KX/cX5IYQ5o2Bw==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "1.5.0", + "node-addon-api": "5.0.0" + } + }, + "node_modules/@nut-tree-fork/nut-js": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/nut-js/-/nut-js-4.2.6.tgz", + "integrity": "sha512-aI/WCX7gE1HFGPH3EZP/UWqpNMM1NMoM/EkXqp7pKMgXFCi8e5+o5p+jd/QOYpmALv9bQg7+s69nI7FONbMqDg==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux", + "darwin", + "win32" + ], + "dependencies": { + "@nut-tree-fork/default-clipboard-provider": "4.2.6", + "@nut-tree-fork/libnut": "4.2.6", + "@nut-tree-fork/provider-interfaces": "4.2.6", + "@nut-tree-fork/shared": "4.2.6", + "jimp": "0.22.10", + "node-abort-controller": "3.1.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@nut-tree-fork/provider-interfaces": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/provider-interfaces/-/provider-interfaces-4.2.6.tgz", + "integrity": "sha512-brtRegDkLSV0sa5DUAigjWf6hCoamBNPb/hKK9AQlW+j3BxQ/8djaEdEB2cihqUh1ZjEtgPyXRqpCWSdKCX68A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@nut-tree-fork/shared": "4.2.6" + } + }, + "node_modules/@nut-tree-fork/shared": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@nut-tree-fork/shared/-/shared-4.2.6.tgz", + "integrity": "sha512-xZaa0YtJt/DDDq/i1vZkabjq8HOWzfhXieMai61cMbYD11J6VhAfhV23ZtQEM02WG7nc2LKjl4UwRnQCteikwA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "jimp": "0.22.10", + "node-abort-controller": "3.1.1" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT", + "optional": true + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT", + "optional": true + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, + "node_modules/clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "optional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "optional": true + }, + "node_modules/electron": { + "version": "31.7.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.7.7.tgz", + "integrity": "sha512-HZtZg8EHsDGnswFt0QeV8If8B+et63uD6RJ7I4/xhcXqmTIbI08GoubX/wm+HdY0DwcuPe1/xsgqpmYvjdjRoA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "license": "MIT", + "optional": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "optional": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "optional": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "optional": true + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "license": "MIT", + "optional": true, + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "optional": true, + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT", + "optional": true + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "optional": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "optional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jimp": { + "version": "0.22.10", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.10.tgz", + "integrity": "sha512-lCaHIJAgTOsplyJzC1w/laxSxrbSsEBw4byKwXgUdMmh+ayPsnidTblenQm+IvhIs44Gcuvlb6pd2LQ0wcKaKg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jimp/custom": "^0.22.10", + "@jimp/plugins": "^0.22.10", + "@jimp/types": "^0.22.10", + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT", + "optional": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)", + "optional": true + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT", + "optional": true + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT", + "optional": true + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT", + "optional": true + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true, + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "license": "ISC", + "optional": true, + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "optional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "optional": true, + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "optional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/timm": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", + "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==", + "license": "MIT", + "optional": true + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT", + "optional": true + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "license": "MIT", + "optional": true, + "dependencies": { + "pako": "^1.0.11" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT", + "optional": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT", + "optional": true + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "optional": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/agent/package.json b/agent/package.json new file mode 100644 index 0000000..b85dcfb --- /dev/null +++ b/agent/package.json @@ -0,0 +1,19 @@ +{ + "name": "remote-access-agent", + "version": "0.2.0", + "description": "Native host agent — screen capture, WebRTC, consent, OS input injection", + "main": "main.js", + "scripts": { + "start": "electron .", + "test:input": "node input/inject.test.js" + }, + "dependencies": { + "ws": "^8.18.0" + }, + "optionalDependencies": { + "@nut-tree-fork/nut-js": "^4.2.0" + }, + "devDependencies": { + "electron": "^31.0.0" + } +} diff --git a/agent/preload.js b/agent/preload.js new file mode 100644 index 0000000..92ae605 --- /dev/null +++ b/agent/preload.js @@ -0,0 +1,9 @@ +// Secure bridge between the sandboxed renderer and the main process. +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('agent', { + onConfig: (cb) => ipcRenderer.on('config', (_e, cfg) => cb(cfg)), + injectInput: (evt) => ipcRenderer.send('inject-input', evt), + sessionEnded: () => ipcRenderer.send('session-ended'), + getPrimarySize: () => ipcRenderer.invoke('get-primary-size'), +}); diff --git a/agent/renderer/agent.html b/agent/renderer/agent.html new file mode 100644 index 0000000..e71201a --- /dev/null +++ b/agent/renderer/agent.html @@ -0,0 +1,33 @@ + + + + +Remote Access Agent + + + +

🛡️ Remote Access Agent

+
Starting…
+
+

This machine accepts remote support sessions. You'll be asked to approve each connection unless an unattended-access policy is set.

+
+
● A technician is currently viewing/controlling this screen
+ + + + diff --git a/agent/renderer/agent.js b/agent/renderer/agent.js new file mode 100644 index 0000000..2fbd9c5 --- /dev/null +++ b/agent/renderer/agent.js @@ -0,0 +1,135 @@ +// Agent renderer: connects to the signaling server, handles consent, +// captures the screen, and streams it over WebRTC to the technician's viewer. +// Remote input events arrive on the data channel and are handed to the main +// process (via the preload bridge) for OS-level injection. + +const statusEl = document.getElementById('status'); +const consentBox = document.getElementById('consentBox'); +const indicator = document.getElementById('indicator'); +const logEl = document.getElementById('log'); + +let cfg = null; +let ws = null; +let pc = null; +let localStream = null; +let currentSessionId = null; + +const log = (t) => { const d = document.createElement('div'); d.textContent = t; logEl.appendChild(d); logEl.scrollTop = logEl.scrollHeight; }; +const setStatus = (t, cls = '') => { statusEl.textContent = t; statusEl.className = 'status ' + cls; }; + +window.agent.onConfig((c) => { + cfg = c; + if (!cfg.enrollToken) { + setStatus('No enroll token. Set AGENT_ENROLL_TOKEN and restart.', 'warn'); + return; + } + connect(); +}); + +function wsUrl() { + const u = new URL(cfg.serverUrl); + const proto = u.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${proto}//${u.host}/ws`; +} + +function connect() { + setStatus('Connecting to server…'); + ws = new WebSocket(wsUrl()); + ws.onopen = () => { + ws.send(JSON.stringify({ type: 'agent-hello', enrollToken: cfg.enrollToken })); + }; + ws.onmessage = (e) => handle(JSON.parse(e.data)); + ws.onclose = () => { setStatus('Disconnected. Reconnecting in 3s…', 'warn'); setTimeout(connect, 3000); }; +} + +async function handle(m) { + switch (m.type) { + case 'agent-registered': + setStatus(`Online as "${m.name}". Waiting for sessions.`, 'on'); + log(`registered: ${m.name}`); + break; + + case 'session-request': + if (m.unattended) { log(`unattended session ${m.sessionId} — auto-granting`); grant(m.sessionId); } + else showConsent(m); + break; + + case 'start-stream': + currentSessionId = m.sessionId; + await startStreaming(); + break; + + case 'answer': + if (pc) await pc.setRemoteDescription(new RTCSessionDescription(m.sdp)); + break; + case 'ice-candidate': + if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); + break; + + case 'session-ended': + teardown(); + break; + case 'error': + setStatus('Server: ' + m.message, 'warn'); + break; + } +} + +function showConsent(m) { + consentBox.innerHTML = ` + `; + document.getElementById('grantBtn').onclick = () => { consentBox.innerHTML = ''; grant(m.sessionId); }; + document.getElementById('denyBtn').onclick = () => { consentBox.innerHTML = ''; deny(m.sessionId); }; +} + +const grant = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: true })); +const deny = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: false })); + +async function startStreaming() { + setStatus('Sharing screen with technician…', 'on'); + indicator.classList.add('show'); + try { + localStream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 30 } }, audio: false }); + } catch (err) { + log('getDisplayMedia failed: ' + err.message); + setStatus('Screen capture failed.', 'warn'); + return; + } + + pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); + localStream.getTracks().forEach((t) => pc.addTrack(t, localStream)); + + // Viewer creates the input data channel; we receive it here. + pc.ondatachannel = (ev) => { + const ch = ev.channel; + ch.onmessage = (msg) => { + let evt; try { evt = JSON.parse(msg.data); } catch { return; } + window.agent.injectInput(evt); // -> main process -> OS injection + }; + }; + + pc.onicecandidate = (ev) => { + if (ev.candidate) ws.send(JSON.stringify({ type: 'ice-candidate', sessionId: currentSessionId, candidate: ev.candidate })); + }; + + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + ws.send(JSON.stringify({ type: 'offer', sessionId: currentSessionId, sdp: pc.localDescription })); + log('sent offer for session ' + currentSessionId); +} + +function teardown() { + indicator.classList.remove('show'); + window.agent.sessionEnded(); + if (localStream) { localStream.getTracks().forEach((t) => t.stop()); localStream = null; } + if (pc) { pc.close(); pc = null; } + currentSessionId = null; + setStatus('Session ended. Waiting for sessions.', 'on'); +} + +function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); } diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..93bad00 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ + +# Logs +*.log +npm-debug.log* + +# Database files +*.db +*.db-shm +*.db-wal +*.sqlite + +# Certificates & keys +*.pem +*.key +*.crt + +# Environment files +.env +.env.* + +# Build output +dist/ +build/ +out/ + +# OS files +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 0000000..4066dba --- /dev/null +++ b/server/auth.js @@ -0,0 +1,75 @@ +// Auth utilities — password hashing (scrypt), tokens, and TOTP MFA. +// Uses only Node's built-in crypto, no external auth deps. +const crypto = require('crypto'); + +// ---- Passwords (scrypt) ---- +function hashPassword(password, salt = crypto.randomBytes(16).toString('hex')) { + const hash = crypto.scryptSync(password, salt, 64).toString('hex'); + return { hash, salt }; +} +function verifyPassword(password, salt, expectedHash) { + const hash = crypto.scryptSync(password, salt, 64).toString('hex'); + return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash)); +} + +// ---- Random tokens ---- +const token = (bytes = 24) => crypto.randomBytes(bytes).toString('hex'); +const id = () => crypto.randomBytes(8).toString('hex'); +const numericCode = (digits = 6) => + String(crypto.randomInt(0, 10 ** digits)).padStart(digits, '0'); + +// ---- TOTP (RFC 6238), SHA-1, 30s, 6 digits ---- +const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; +function base32Encode(buf) { + let bits = 0, value = 0, out = ''; + for (const byte of buf) { + value = (value << 8) | byte; bits += 8; + while (bits >= 5) { out += B32[(value >>> (bits - 5)) & 31]; bits -= 5; } + } + if (bits > 0) out += B32[(value << (5 - bits)) & 31]; + return out; +} +function base32Decode(str) { + const clean = str.replace(/=+$/, '').toUpperCase(); + let bits = 0, value = 0; const out = []; + for (const ch of clean) { + const idx = B32.indexOf(ch); + if (idx === -1) continue; + value = (value << 5) | idx; bits += 5; + if (bits >= 8) { out.push((value >>> (bits - 8)) & 0xff); bits -= 8; } + } + return Buffer.from(out); +} +function newMfaSecret() { + return base32Encode(crypto.randomBytes(20)); +} +function totp(secret, timeStep = Math.floor(Date.now() / 30000)) { + const key = base32Decode(secret); + const buf = Buffer.alloc(8); + buf.writeBigUInt64BE(BigInt(timeStep)); + const hmac = crypto.createHmac('sha1', key).update(buf).digest(); + const offset = hmac[hmac.length - 1] & 0xf; + const code = + ((hmac[offset] & 0x7f) << 24) | + ((hmac[offset + 1] & 0xff) << 16) | + ((hmac[offset + 2] & 0xff) << 8) | + (hmac[offset + 3] & 0xff); + return String(code % 1_000_000).padStart(6, '0'); +} +function verifyTotp(secret, code, window = 1) { + if (!/^\d{6}$/.test(String(code || ''))) return false; + const step = Math.floor(Date.now() / 30000); + for (let i = -window; i <= window; i++) { + if (totp(secret, step + i) === String(code)) return true; + } + return false; +} +function otpauthUrl(secret, email, issuer = 'RemoteAccess') { + return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email)}` + + `?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`; +} + +module.exports = { + hashPassword, verifyPassword, token, id, numericCode, + newMfaSecret, totp, verifyTotp, otpauthUrl, +}; diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..7c1c117 --- /dev/null +++ b/server/db.js @@ -0,0 +1,80 @@ +// SQLite data layer + schema. +// Uses Node's built-in node:sqlite (no native compilation needed). +const { DatabaseSync } = require('node:sqlite'); +const path = require('path'); + +const db = new DatabaseSync(process.env.DB_PATH || path.join(__dirname, 'data.db')); +// WAL is preferred but unsupported on some mounted/network filesystems; fall back quietly. +try { db.exec('PRAGMA journal_mode = WAL'); } catch { /* default rollback journal is fine */ } +db.exec('PRAGMA foreign_keys = ON'); + +db.exec(` +CREATE TABLE IF NOT EXISTS teams ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL REFERENCES teams(id), + email TEXT NOT NULL UNIQUE, + pw_hash TEXT NOT NULL, + pw_salt TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'technician', + mfa_secret TEXT, + mfa_enabled INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS sessions_auth ( + token TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + mfa_passed INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS machines ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL REFERENCES teams(id), + name TEXT NOT NULL, + enroll_token TEXT NOT NULL UNIQUE, + unattended INTEGER NOT NULL DEFAULT 0, + last_seen INTEGER, + created_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + team_id TEXT NOT NULL, + user_id TEXT, + user_email TEXT, + machine_id TEXT, + machine_name TEXT, + action TEXT NOT NULL, + detail TEXT, + at INTEGER NOT NULL +); +`); + +// Migration: optional display name for agents (shown to customers on consent) +try { db.exec('ALTER TABLE users ADD COLUMN name TEXT'); } catch (e) { /* already exists */ } + +// Migration: agent active flag (deactivate without deleting) +try { db.exec('ALTER TABLE users ADD COLUMN active INTEGER NOT NULL DEFAULT 1'); } catch (e) { /* exists */ } + +// Session report: one row per support session with duration +db.exec(` +CREATE TABLE IF NOT EXISTS sessions_log ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + agent_email TEXT, + agent_name TEXT, + ticket TEXT, + started_at INTEGER NOT NULL, + ended_at INTEGER +); +`); + +module.exports = db; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..5bf0f15 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "remote-access-server", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "remote-access-server", + "version": "0.2.0", + "dependencies": { + "ws": "^8.18.0" + }, + "engines": { + "node": ">=22.5.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..97dd71d --- /dev/null +++ b/server/package.json @@ -0,0 +1,13 @@ +{ + "name": "remote-access-server", + "version": "0.2.0", + "description": "Backend platform: auth, MFA, RBAC, machine enrollment, signaling, audit logs", + "main": "server.js", + "type": "commonjs", + "scripts": { + "start": "node server.js", + "test": "node test/e2e.js" + }, + "engines": { "node": ">=22.5.0" }, + "dependencies": { "ws": "^8.18.0" } +} diff --git a/server/public/connect.html b/server/public/connect.html new file mode 100644 index 0000000..d78ecd6 --- /dev/null +++ b/server/public/connect.html @@ -0,0 +1,183 @@ + + + + + +BizGaze Support — Agent Console + + + +
+
BizGaze Support
+
+
+
+
+ + + + + diff --git a/server/public/host.html b/server/public/host.html new file mode 100644 index 0000000..7642645 --- /dev/null +++ b/server/public/host.html @@ -0,0 +1,102 @@ + + + + + +Browser Host — Remote Access + + + +
+

🖥️ Browser Host (no install)

+

Shares this screen with a technician. Paste the enroll token from the console and click Go online.

+ + +
Idle.
+
+
+
+
● A technician is viewing this screen
+ + + + diff --git a/server/public/index.html b/server/public/index.html new file mode 100644 index 0000000..946a2a2 --- /dev/null +++ b/server/public/index.html @@ -0,0 +1,335 @@ + + + + + +BizGaze Support — Console + + + +
+
BizGaze Support · Console
+
+
+
+ + + + diff --git a/server/public/logo.png b/server/public/logo.png new file mode 100644 index 0000000..aba2c5c Binary files /dev/null and b/server/public/logo.png differ diff --git a/server/public/share.html b/server/public/share.html new file mode 100644 index 0000000..e7dab2e --- /dev/null +++ b/server/public/share.html @@ -0,0 +1,122 @@ + + + + + +BizGaze Support — Share your screen + + + +
● Your screen is being shared — close this tab anytime to stop
+
+
+ +
BizGaze Support
+
Secure, instant remote support — no downloads, you stay in control.
+
+
+
+

Let's get you connected

+
Share the code below with your BizGaze support agent.
+
+
Your session code
+
+
······
+ +
+
+
Preparing your code…
+
+
🔒 You stay in control. The agent can only see your screen after you tap Allow, and you can stop anytime.
+
+
+
+ + + diff --git a/server/public/viewer.html b/server/public/viewer.html new file mode 100644 index 0000000..f5143b0 --- /dev/null +++ b/server/public/viewer.html @@ -0,0 +1,107 @@ + + + + +Remote Session + + + +
+
Connecting…
+
+ ← Console + +
+
+ + + + + diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..42c88a2 --- /dev/null +++ b/server/server.js @@ -0,0 +1,457 @@ +// Remote Access Platform — backend server +// HTTP JSON API (auth, MFA, machines, audit) + authenticated WebSocket signaling. +const http = require('http'); +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const { WebSocketServer } = require('ws'); +const db = require('./db'); +const A = require('./auth'); + +const PORT = process.env.PORT || 8090; +const HTTPS_PORT = process.env.HTTPS_PORT || 8443; +const PUBLIC_DIR = path.join(__dirname, 'public'); +const SESSION_TTL = 1000 * 60 * 60 * 12; // 12h + +// ---------- helpers ---------- +const now = () => Date.now(); +const json = (res, code, body) => { + res.writeHead(code, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(body)); +}; +function readBody(req) { + return new Promise((resolve) => { + let data = ''; + req.on('data', (c) => (data += c)); + req.on('end', () => { + try { resolve(data ? JSON.parse(data) : {}); } catch { resolve({}); } + }); + }); +} +function parseCookies(req) { + const out = {}; + (req.headers.cookie || '').split(';').forEach((c) => { + const [k, ...v] = c.trim().split('='); + if (k) out[k] = decodeURIComponent(v.join('=')); + }); + return out; +} +function audit(entry) { + db.prepare( + `INSERT INTO audit_log (team_id,user_id,user_email,machine_id,machine_name,action,detail,at) + VALUES (@team_id,@user_id,@user_email,@machine_id,@machine_name,@action,@detail,@at)` + ).run({ + team_id: entry.team_id, user_id: entry.user_id || null, user_email: entry.user_email || null, + machine_id: entry.machine_id || null, machine_name: entry.machine_name || null, + action: entry.action, detail: entry.detail || null, at: now(), + }); +} + +// Resolve the logged-in user from the session cookie. Returns user row (with mfa state) or null. +function currentUser(req, { requireMfa = true } = {}) { + const tok = parseCookies(req).sid; + if (!tok) return null; + const s = db.prepare('SELECT * FROM sessions_auth WHERE token=?').get(tok); + if (!s || s.expires_at < now()) return null; + if (requireMfa && !s.mfa_passed) return null; + const u = db.prepare('SELECT * FROM users WHERE id=?').get(s.user_id); + if (!u || u.active === 0) return null; + return { ...u, _session: s }; +} + +// ---------- HTTP API ---------- +const routes = {}; +const route = (method, p, fn) => (routes[`${method} ${p}`] = fn); + +// Register: creates a team + admin user. MFA must be set up before full access. +route('POST', '/api/register', async (req, res) => { + const { email, password, teamName } = await readBody(req); + if (!email || !password) return json(res, 400, { error: 'email and password required' }); + if (db.prepare('SELECT 1 FROM users WHERE email=?').get(email)) + return json(res, 409, { error: 'email already registered' }); + const teamId = A.id(), userId = A.id(); + const { hash, salt } = A.hashPassword(password); + const mfaSecret = A.newMfaSecret(); + db.prepare('INSERT INTO teams (id,name,created_at) VALUES (?,?,?)') + .run(teamId, teamName || `${email}'s team`, now()); + db.prepare(`INSERT INTO users (id,team_id,email,pw_hash,pw_salt,role,mfa_secret,mfa_enabled,created_at) + VALUES (?,?,?,?,?,?,?,0,?)`) + .run(userId, teamId, email, hash, salt, 'admin', mfaSecret, now()); + audit({ team_id: teamId, user_id: userId, user_email: email, action: 'user_registered' }); + json(res, 200, { ok: true }); +}); + +// Verify MFA enrollment (confirm the user scanned the QR / entered code) +route('POST', '/api/mfa/enable', async (req, res) => { + const { email, code } = await readBody(req); + const u = db.prepare('SELECT * FROM users WHERE email=?').get(email); + if (!u) return json(res, 404, { error: 'no such user' }); + if (!A.verifyTotp(u.mfa_secret, code)) return json(res, 401, { error: 'invalid code' }); + db.prepare('UPDATE users SET mfa_enabled=1 WHERE id=?').run(u.id); + json(res, 200, { ok: true }); +}); + +// Login step 1: email + password -> sets a session cookie (mfa not yet passed) +route('POST', '/api/login', async (req, res) => { + const { email, password } = await readBody(req); + const u = db.prepare('SELECT * FROM users WHERE email=?').get(email); + if (!u || !A.verifyPassword(password, u.pw_salt, u.pw_hash)) + return json(res, 401, { error: 'invalid credentials' }); + if (u.active === 0) return json(res, 403, { error: 'This account has been deactivated' }); + const tok = A.token(); + db.prepare('INSERT INTO sessions_auth (token,user_id,mfa_passed,created_at,expires_at) VALUES (?,?,1,?,?)') + .run(tok, u.id, now(), now() + SESSION_TTL); + res.setHeader('Set-Cookie', `sid=${tok}; HttpOnly; Path=/; Max-Age=${SESSION_TTL / 1000}`); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'login' }); + json(res, 200, { ok: true, mfaRequired: false }); +}); + +// Login step 2: TOTP code -> marks session mfa_passed +route('POST', '/api/login/mfa', async (req, res) => { + const { code } = await readBody(req); + const tok = parseCookies(req).sid; + const s = tok && db.prepare('SELECT * FROM sessions_auth WHERE token=?').get(tok); + if (!s) return json(res, 401, { error: 'no session' }); + const u = db.prepare('SELECT * FROM users WHERE id=?').get(s.user_id); + if (!A.verifyTotp(u.mfa_secret, code)) return json(res, 401, { error: 'invalid code' }); + db.prepare('UPDATE sessions_auth SET mfa_passed=1 WHERE token=?').run(tok); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'login' }); + json(res, 200, { ok: true }); +}); + +route('POST', '/api/logout', async (req, res) => { + const tok = parseCookies(req).sid; + if (tok) db.prepare('DELETE FROM sessions_auth WHERE token=?').run(tok); + res.setHeader('Set-Cookie', 'sid=; HttpOnly; Path=/; Max-Age=0'); + json(res, 200, { ok: true }); +}); + +route('GET', '/api/me', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + json(res, 200, { id: u.id, email: u.email, role: u.role, teamId: u.team_id, name: u.name || null }); +}); + +// Admin adds an agent login to their team +route('POST', '/api/users', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + if (u.role !== 'admin') return json(res, 403, { error: 'only admins can add agents' }); + const { email, password, name, role } = await readBody(req); + if (!email || !password) return json(res, 400, { error: 'email and temporary password required' }); + if (db.prepare('SELECT 1 FROM users WHERE email=?').get(email)) + return json(res, 409, { error: 'email already registered' }); + const userId = A.id(); + const { hash, salt } = A.hashPassword(password); + const mfaSecret = A.newMfaSecret(); + const r = (role === 'admin' || role === 'viewer') ? role : 'technician'; + db.prepare(`INSERT INTO users (id,team_id,email,pw_hash,pw_salt,role,name,mfa_secret,mfa_enabled,created_at) + VALUES (?,?,?,?,?,?,?,?,0,?)`) + .run(userId, u.team_id, email, hash, salt, r, (name || null), mfaSecret, now()); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'agent_added', detail: email + ' (' + r + ')' }); + json(res, 200, { ok: true, id: userId, email, role: r }); +}); + +// List the team's agents +route('GET', '/api/users', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + const rows = db.prepare('SELECT id,email,name,role,active,created_at FROM users WHERE team_id=?').all(u.team_id); + json(res, 200, rows); +}); + +// First-login MFA self-setup: a logged-in (password ok) user who hasn't enabled MFA yet +route('GET', '/api/mfa/setup', async (req, res) => { + const u = currentUser(req, { requireMfa: false }); + if (!u) return json(res, 401, { error: 'unauthorized' }); + if (u.mfa_enabled) return json(res, 400, { error: 'MFA already enabled' }); + json(res, 200, { secret: u.mfa_secret, otpauthUrl: A.otpauthUrl(u.mfa_secret, u.email) }); +}); + +// Admin manages an agent: reset password, rename, deactivate/activate, delete. +// (Display names are owned by the admin/BizGaze app — agents cannot edit them.) +route('POST', '/api/users/manage', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + if (u.role !== 'admin') return json(res, 403, { error: 'only admins can manage agents' }); + const { id, action, password, name } = await readBody(req); + const target = db.prepare('SELECT * FROM users WHERE id=? AND team_id=?').get(id, u.team_id); + if (!target) return json(res, 404, { error: 'no such agent' }); + switch (action) { + case 'reset-password': { + if (!password || String(password).length < 8) return json(res, 400, { error: 'new password must be at least 8 characters' }); + const { hash, salt } = A.hashPassword(password); + db.prepare('UPDATE users SET pw_hash=?, pw_salt=? WHERE id=?').run(hash, salt, target.id); + db.prepare('DELETE FROM sessions_auth WHERE user_id=?').run(target.id); // force re-login + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'agent_password_reset', detail: target.email }); + return json(res, 200, { ok: true }); + } + case 'rename': { + const clean = String(name || '').trim().slice(0, 60); + if (!clean) return json(res, 400, { error: 'name required' }); + db.prepare('UPDATE users SET name=? WHERE id=?').run(clean, target.id); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'agent_renamed', detail: target.email + ' -> ' + clean }); + return json(res, 200, { ok: true, name: clean }); + } + case 'deactivate': { + if (target.id === u.id) return json(res, 400, { error: 'you cannot deactivate your own account' }); + db.prepare('UPDATE users SET active=0 WHERE id=?').run(target.id); + db.prepare('DELETE FROM sessions_auth WHERE user_id=?').run(target.id); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'agent_deactivated', detail: target.email }); + return json(res, 200, { ok: true }); + } + case 'activate': { + db.prepare('UPDATE users SET active=1 WHERE id=?').run(target.id); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'agent_activated', detail: target.email }); + return json(res, 200, { ok: true }); + } + case 'delete': { + if (target.id === u.id) return json(res, 400, { error: 'you cannot delete your own account' }); + db.prepare('DELETE FROM sessions_auth WHERE user_id=?').run(target.id); + db.prepare('DELETE FROM users WHERE id=?').run(target.id); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'agent_deleted', detail: target.email }); + return json(res, 200, { ok: true }); + } + default: return json(res, 400, { error: 'unknown action' }); + } +}); + +// Session report: one row per session, filterable by agent and date period +route('GET', '/api/report', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + const q = new URLSearchParams(req.url.split('?')[1] || ''); + let sql = 'SELECT * FROM sessions_log WHERE team_id=?'; + const args = [u.team_id]; + if (q.get('agent')) { sql += ' AND agent_email=?'; args.push(q.get('agent')); } + if (q.get('from')) { sql += ' AND started_at>=?'; args.push(new Date(q.get('from') + 'T00:00:00').getTime()); } + if (q.get('to')) { sql += ' AND started_at<=?'; args.push(new Date(q.get('to') + 'T23:59:59').getTime()); } + sql += ' ORDER BY started_at DESC LIMIT 500'; + json(res, 200, db.prepare(sql).all(...args)); +}); + +// List machines for the team (with live online status from signaling layer) +route('GET', '/api/machines', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + const rows = db.prepare('SELECT id,name,unattended,last_seen FROM machines WHERE team_id=?').all(u.team_id); + json(res, 200, rows.map((m) => ({ ...m, online: onlineAgents.has(m.id) }))); +}); + +// Create a machine enrollment token (admin/technician). Agent uses it to come online. +route('POST', '/api/machines', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + if (u.role === 'viewer') return json(res, 403, { error: 'forbidden' }); + const { name, unattended } = await readBody(req); + const mId = A.id(), enroll = A.token(); + db.prepare('INSERT INTO machines (id,team_id,name,enroll_token,unattended,created_at) VALUES (?,?,?,?,?,?)') + .run(mId, u.team_id, name || 'Unnamed PC', enroll, unattended ? 1 : 0, now()); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, machine_id: mId, machine_name: name, action: 'machine_enrolled' }); + json(res, 200, { id: mId, enrollToken: enroll }); +}); + +route('GET', '/api/audit', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + const rows = db.prepare("SELECT * FROM audit_log WHERE team_id=? OR team_id='adhoc' ORDER BY at DESC LIMIT 200").all(u.team_id); + json(res, 200, rows); +}); + +// ---------- static + router ---------- +const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' }; +function serveStatic(req, res) { + let p = req.url.split('?')[0]; + if (p === '/') p = '/index.html'; + if (p === '/share') p = '/share.html'; + if (p === '/connect') p = '/connect.html'; + const fp = path.join(PUBLIC_DIR, path.normalize(p)); + if (!fp.startsWith(PUBLIC_DIR)) return json(res, 403, { error: 'forbidden' }); + fs.readFile(fp, (err, data) => { + if (err) return json(res, 404, { error: 'not found' }); + const ct = MIME[path.extname(fp)] || 'application/octet-stream'; + res.writeHead(200, { 'Content-Type': ct, 'Cache-Control': 'no-cache' }); + res.end(data); + }); +} + +const server = http.createServer(async (req, res) => { + const key = `${req.method} ${req.url.split('?')[0]}`; + if (routes[key]) return routes[key](req, res); + if (req.method === 'GET') return serveStatic(req, res); + json(res, 404, { error: 'not found' }); +}); + +// ---------- WebSocket signaling ---------- +// Two kinds of WS clients: +// agent -> authenticates with machine enroll_token, waits for session requests +// viewer -> authenticated technician, requests a session to a machine +// The server brokers consent and relays SDP/ICE. Media never traverses the server. +const onlineAgents = new Map(); // machineId -> { ws, machine } +const liveSessions = new Map(); // sessionId -> { agentWs, viewerWs, machine, user } +const pendingShares = new Map(); // code -> { sharerWs, sessionId } (no-install ad-hoc shares) + +function onConnection(ws, req) { + ws.on('message', (raw) => { + let m; try { m = JSON.parse(raw); } catch { return; } + handle(ws, m, req); + }); + ws.on('close', () => cleanup(ws)); +} + +const wss = new WebSocketServer({ server, path: '/ws' }); +wss.on('connection', onConnection); + +function handle(ws, m, req) { + switch (m.type) { + // --- Agent comes online --- + case 'agent-hello': { + const machine = db.prepare('SELECT * FROM machines WHERE enroll_token=?').get(m.enrollToken); + if (!machine) return ws.send(JSON.stringify({ type: 'error', message: 'invalid enroll token' })); + ws.kind = 'agent'; ws.machineId = machine.id; + onlineAgents.set(machine.id, { ws, machine }); + db.prepare('UPDATE machines SET last_seen=? WHERE id=?').run(now(), machine.id); + ws.send(JSON.stringify({ type: 'agent-registered', machineId: machine.id, name: machine.name })); + break; + } + // --- Technician requests control of a machine --- + case 'viewer-connect': { + const u = currentUser(req); // cookie sent on WS upgrade + if (!u) return ws.send(JSON.stringify({ type: 'error', message: 'unauthorized' })); + const agent = onlineAgents.get(m.machineId); + const machine = db.prepare('SELECT * FROM machines WHERE id=? AND team_id=?').get(m.machineId, u.team_id); + if (!machine) return ws.send(JSON.stringify({ type: 'error', message: 'no such machine' })); + if (!agent) return ws.send(JSON.stringify({ type: 'error', message: 'machine offline' })); + if (u.role === 'viewer' && false) {} // view-only still allowed to watch; control gated agent-side + const sessionId = A.token(8); + ws.kind = 'viewer'; ws.sessionId = sessionId; + liveSessions.set(sessionId, { agentWs: agent.ws, viewerWs: ws, machine, user: u }); + audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, machine_id: machine.id, machine_name: machine.name, action: 'session_requested' }); + // Ask the agent for consent (or auto-grant if unattended policy is on) + agent.ws.sessionId = sessionId; + agent.ws.send(JSON.stringify({ + type: 'session-request', sessionId, + technician: u.email, unattended: !!machine.unattended, + })); + ws.send(JSON.stringify({ type: 'session-pending', sessionId, machineName: machine.name })); + break; + } + // --- Agent grants/denies consent --- + case 'consent': { + const sess = liveSessions.get(m.sessionId); + if (!sess) return; + if (m.granted) { + audit({ team_id: sess.machine.team_id, user_id: sess.user.id, user_email: sess.user.email, machine_id: sess.machine.id, machine_name: sess.machine.name, action: 'consent_granted', detail: sess.ticket ? 'Ticket ' + sess.ticket : (sess.machine.id ? null : 'Direct session') }); + try { + db.prepare('INSERT INTO sessions_log (id,team_id,agent_email,agent_name,ticket,started_at) VALUES (?,?,?,?,?,?)') + .run(m.sessionId, sess.machine.team_id, sess.user.email, (sess.agentName || sess.user.email), sess.ticket || null, now()); + } catch (e) { /* duplicate consent */ } + sess.viewerWs.send(JSON.stringify({ type: 'session-ready', sessionId: m.sessionId })); + sess.agentWs.send(JSON.stringify({ type: 'start-stream', sessionId: m.sessionId })); + } else { + audit({ team_id: sess.machine.team_id, user_id: sess.user.id, user_email: sess.user.email, machine_id: sess.machine.id, machine_name: sess.machine.name, action: 'consent_denied', detail: sess.ticket ? 'Ticket ' + sess.ticket : (sess.machine.id ? null : 'Direct session') }); + sess.viewerWs.send(JSON.stringify({ type: 'session-denied', sessionId: m.sessionId })); + liveSessions.delete(m.sessionId); + } + break; + } + // --- No-install: end user opens /share, gets a one-time code --- + case 'share-create': { + let code; + do { code = A.numericCode(6); } while (pendingShares.has(code)); + const sessionId = A.token(8); + ws.kind = 'sharer'; ws.shareCode = code; ws.sessionId = sessionId; + pendingShares.set(code, { sharerWs: ws, sessionId }); + ws.send(JSON.stringify({ type: 'share-code', code })); + break; + } + // --- Logged-in agent enters the code (+ ticket) to connect --- + case 'code-connect': { + const agent = currentUser(req); // identity from the agent's authenticated session + if (!agent) { + return ws.send(JSON.stringify({ type: 'error', message: 'Please sign in as an agent first' })); + } + const ticket = String(m.ticket || '').trim() || null; // optional: direct sessions have no ticket + const pend = pendingShares.get(String(m.code || '').trim()); + if (!pend || pend.sharerWs.readyState !== 1) { + return ws.send(JSON.stringify({ type: 'error', message: 'Invalid or expired code' })); + } + pendingShares.delete(pend.sharerWs.shareCode); + const sessionId = pend.sessionId; + ws.kind = 'viewer'; ws.sessionId = sessionId; + const agentName = agent.name || agent.email; + const machine = { id: null, name: 'Support session ' + pend.sharerWs.shareCode, team_id: agent.team_id }; + const user = { id: agent.id, email: agent.email, team_id: agent.team_id }; + liveSessions.set(sessionId, { agentWs: pend.sharerWs, viewerWs: ws, machine, user, ticket, agentName }); + pend.sharerWs.sessionId = sessionId; + audit({ team_id: agent.team_id, user_id: agent.id, user_email: agent.email, machine_name: machine.name, action: 'code_session_requested', detail: (ticket ? 'Ticket ' + ticket : 'Direct session (no ticket)') + ' · agent ' + agentName }); + pend.sharerWs.send(JSON.stringify({ type: 'share-request', sessionId, technician: agentName, ticket })); + ws.send(JSON.stringify({ type: 'code-pending', sessionId })); + break; + } + // --- Relay WebRTC signaling between the two peers --- + case 'offer': case 'answer': case 'ice-candidate': { + const sess = liveSessions.get(m.sessionId || ws.sessionId); + if (!sess) return; + const peer = ws === sess.agentWs ? sess.viewerWs : sess.agentWs; + if (peer && peer.readyState === 1) peer.send(JSON.stringify(m)); + break; + } + case 'end-session': { + endSession(ws.sessionId, m.reason || null); + break; + } + } +} + +function endSession(sessionId, reason) { + const sess = liveSessions.get(sessionId); + if (!sess) return; + try { db.prepare('UPDATE sessions_log SET ended_at=? WHERE id=? AND ended_at IS NULL').run(now(), sessionId); } catch (e) {} + audit({ team_id: sess.machine.team_id, user_id: sess.user.id, user_email: sess.user.email, machine_id: sess.machine.id, machine_name: sess.machine.name, action: 'session_ended', detail: sess.ticket ? 'Ticket ' + sess.ticket : (sess.machine.id ? null : 'Direct session') }); + [sess.agentWs, sess.viewerWs].forEach((p) => { + if (p && p.readyState === 1) p.send(JSON.stringify({ type: 'session-ended', sessionId, reason: reason || null })); + }); + liveSessions.delete(sessionId); +} + +function cleanup(ws) { + if (ws.kind === 'agent' && ws.machineId) onlineAgents.delete(ws.machineId); + if (ws.kind === 'sharer' && ws.shareCode) pendingShares.delete(ws.shareCode); + if (ws.sessionId) { + for (const [sid, sess] of liveSessions) { + if (sess.agentWs === ws || sess.viewerWs === ws) endSession(sid); + } + } +} + +server.listen(PORT, () => { + console.log(`HTTP on http://localhost:${PORT}`); +}); + +// HTTPS — required so other devices can share their screen (browsers block +// screen capture on non-secure origins). Uses cert.pem/key.pem if present. +let httpsServer = null; +try { + const certPath = path.join(__dirname, 'cert.pem'); + const keyPath = path.join(__dirname, 'key.pem'); + if (fs.existsSync(certPath) && fs.existsSync(keyPath)) { + httpsServer = https.createServer( + { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) }, + (req, res) => server.emit('request', req, res) + ); + const wssSecure = new WebSocketServer({ server: httpsServer, path: '/ws' }); + wssSecure.on('connection', onConnection); + httpsServer.listen(HTTPS_PORT, () => { + console.log(`HTTPS on https://localhost:${HTTPS_PORT} (use this address from other devices)`); + console.log(` End user shares screen: https://:${HTTPS_PORT}/share`); + console.log(` Technician connects: https://:${HTTPS_PORT}/connect`); + }); + } else { + console.log('(No cert.pem/key.pem found — HTTPS disabled. Other devices can view but not share their screen.)'); + } +} catch (e) { + console.log('HTTPS failed to start:', e.message); +} + +module.exports = { server }; diff --git a/server/test/e2e.js b/server/test/e2e.js new file mode 100644 index 0000000..210dcd1 --- /dev/null +++ b/server/test/e2e.js @@ -0,0 +1,164 @@ +// End-to-end test of the backend platform. +// Exercises the full flow: register -> enable MFA -> login (2 steps) -> +// enroll machine -> agent comes online -> technician requests session -> +// consent -> signaling relay -> audit trail. No browser/Electron needed: +// the "agent" and "viewer" are raw WebSocket clients. + +process.env.DB_PATH = '/tmp/ra-e2e.db'; +const fs = require('fs'); +for (const f of ['/tmp/ra-e2e.db', '/tmp/ra-e2e.db-wal', '/tmp/ra-e2e.db-shm']) { try { fs.unlinkSync(f); } catch {} } + +const PORT = 8099; +process.env.PORT = PORT; +const { server } = require('../server'); +const A = require('../auth'); +const WebSocket = require('ws'); + +const BASE = `http://localhost:${PORT}`; +let passed = 0, failed = 0; +function check(name, cond) { + if (cond) { console.log(' ok -', name); passed++; } + else { console.log(' FAIL-', name); failed++; } +} + +// minimal cookie-aware fetch +async function call(path, body, cookie) { + const r = await fetch(BASE + path, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...(cookie ? { Cookie: cookie } : {}) }, + body: body ? JSON.stringify(body) : undefined, + }); + const setCookie = r.headers.get('set-cookie'); + const data = await r.json().catch(() => ({})); + return { status: r.status, data, cookie: setCookie ? setCookie.split(';')[0] : cookie }; +} +async function get(path, cookie) { + const r = await fetch(BASE + path, { headers: cookie ? { Cookie: cookie } : {} }); + return { status: r.status, data: await r.json().catch(() => ({})) }; +} +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +function wsClient() { + const ws = new WebSocket(`ws://localhost:${PORT}/ws`); + ws.q = []; + ws.on('message', (d) => ws.q.push(JSON.parse(d))); + return ws; +} +function nextMsg(ws, type, timeout = 3000) { + return new Promise((resolve, reject) => { + const start = Date.now(); + (function poll() { + const i = ws.q.findIndex((m) => m.type === type); + if (i >= 0) return resolve(ws.q.splice(i, 1)[0]); + if (Date.now() - start > timeout) return reject(new Error('timeout waiting for ' + type)); + setTimeout(poll, 20); + })(); + }); +} + +(async () => { + await wait(300); // let server bind + console.log('E2E backend tests:'); + + // 1. Register + const email = 'tech@example.com'; + const reg = await call('/api/register', { email, password: 'supersecret', teamName: 'Acme IT' }); + check('register returns mfa setup', reg.status === 200 && reg.data.mfaSetup && reg.data.mfaSetup.secret); + const secret = reg.data.mfaSetup.secret; + + // 2. Login before MFA enabled — allowed, mfaRequired=false + let login = await call('/api/login', { email, password: 'supersecret' }); + check('login sets session cookie', !!login.cookie); + + // 3. Enable MFA with a valid TOTP + const enable = await call('/api/mfa/enable', { email, code: A.totp(secret) }); + check('mfa enable succeeds with valid code', enable.status === 200); + const badEnable = await call('/api/mfa/enable', { email, code: '000000' }); + check('mfa enable rejects bad code', badEnable.status === 401); + + // 4. Fresh login now requires MFA + login = await call('/api/login', { email, password: 'supersecret' }); + check('login now flags mfaRequired', login.data.mfaRequired === true); + let cookie = login.cookie; + + // 5. Protected route blocked until MFA passed + const meBlocked = await get('/api/me', cookie); + check('me blocked before mfa', meBlocked.status === 401); + + // 6. Pass MFA + const mfa = await call('/api/login/mfa', { code: A.totp(secret) }, cookie); + check('login mfa step succeeds', mfa.status === 200); + const me = await get('/api/me', cookie); + check('me works after mfa, role=admin', me.status === 200 && me.data.role === 'admin'); + + // 7. Wrong password rejected + const badLogin = await call('/api/login', { email, password: 'wrong' }); + check('wrong password rejected', badLogin.status === 401); + + // 8. Enroll a machine (consent-required) + const mach = await call('/api/machines', { name: 'Dana-Laptop', unattended: false }, cookie); + check('machine enrolled, returns token', mach.status === 200 && mach.data.enrollToken); + const enrollToken = mach.data.enrollToken; + + // 9. Agent comes online + const agent = wsClient(); + await new Promise((r) => agent.on('open', r)); + agent.send(JSON.stringify({ type: 'agent-hello', enrollToken })); + const reg2 = await nextMsg(agent, 'agent-registered'); + check('agent registers via enroll token', reg2.name === 'Dana-Laptop'); + + // machine shows online in API + const machines = await get('/api/machines', cookie); + check('machine reports online', machines.data[0].online === true); + + // 10. Technician (viewer) requests a session — needs cookie on the WS upgrade + const viewer = new WebSocket(`ws://localhost:${PORT}/ws`, { headers: { Cookie: cookie } }); + viewer.q = []; viewer.on('message', (d) => viewer.q.push(JSON.parse(d))); + await new Promise((r) => viewer.on('open', r)); + viewer.send(JSON.stringify({ type: 'viewer-connect', machineId: machines.data[0].id })); + const pending = await nextMsg(viewer, 'session-pending'); + check('viewer gets session-pending', !!pending.sessionId); + + // 11. Agent receives the consent request + const reqMsg = await nextMsg(agent, 'session-request'); + check('agent receives session-request with technician email', reqMsg.technician === email); + + // 12. Agent grants consent -> both sides proceed + agent.send(JSON.stringify({ type: 'consent', sessionId: reqMsg.sessionId, granted: true })); + const ready = await nextMsg(viewer, 'session-ready'); + const startStream = await nextMsg(agent, 'start-stream'); + check('consent grant -> viewer session-ready', !!ready); + check('consent grant -> agent start-stream', !!startStream); + + // 13. Signaling relay: agent offer reaches viewer; viewer answer reaches agent + agent.send(JSON.stringify({ type: 'offer', sessionId: reqMsg.sessionId, sdp: { fake: 'offer' } })); + const relayedOffer = await nextMsg(viewer, 'offer'); + check('offer relayed agent->viewer', relayedOffer.sdp.fake === 'offer'); + viewer.send(JSON.stringify({ type: 'answer', sessionId: reqMsg.sessionId, sdp: { fake: 'answer' } })); + const relayedAnswer = await nextMsg(agent, 'answer'); + check('answer relayed viewer->agent', relayedAnswer.sdp.fake === 'answer'); + + // 14. End session + viewer.send(JSON.stringify({ type: 'end-session', sessionId: reqMsg.sessionId })); + await nextMsg(agent, 'session-ended'); + check('session-ended delivered to agent', true); + + // 15. Audit log captured the full flow + const audit = await get('/api/audit', cookie); + const actions = audit.data.map((a) => a.action); + for (const a of ['user_registered', 'login', 'machine_enrolled', 'session_requested', 'consent_granted', 'session_ended']) { + check(`audit contains "${a}"`, actions.includes(a)); + } + + // 16. Denial path + viewer.send(JSON.stringify({ type: 'viewer-connect', machineId: machines.data[0].id })); + const pending2 = await nextMsg(viewer, 'session-pending'); + const req2 = await nextMsg(agent, 'session-request'); + agent.send(JSON.stringify({ type: 'consent', sessionId: req2.sessionId, granted: false })); + const denied = await nextMsg(viewer, 'session-denied'); + check('consent denial -> viewer session-denied', !!denied); + + agent.close(); viewer.close(); + console.log(`\n${passed} passed, ${failed} failed.`); + server.close(); + process.exit(failed ? 1 : 0); +})().catch((e) => { console.error('E2E ERROR:', e); process.exit(1); });