herdr-remote 0.2.39 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/353DMT35.js +3 -0
- package/dist/chunks/42KARAL4.js +31 -0
- package/dist/chunks/7AUQHB3O.js +10 -0
- package/dist/chunks/7SMZ43LI.js +9 -0
- package/dist/chunks/AM3OEMFK.js +16 -0
- package/dist/chunks/ANNJESOL.js +10 -0
- package/dist/chunks/AWB4A65P.js +63 -0
- package/dist/chunks/C2UUZY6J.js +2 -0
- package/dist/chunks/DGUZCGQN.js +3 -0
- package/dist/chunks/H24ZP5D5.js +16 -0
- package/dist/chunks/I6NRCYLI.js +3 -0
- package/dist/chunks/JJGEV4EZ.js +3 -0
- package/dist/chunks/KRSURB5L.js +5 -0
- package/dist/chunks/KRVF37OW.js +3 -0
- package/dist/chunks/KUDCC7ZO.js +6 -0
- package/dist/chunks/M3NRPJK2.js +77 -0
- package/dist/chunks/M4AFZHKH.js +3 -0
- package/dist/chunks/M52LJEES.js +6 -0
- package/dist/chunks/O2LNVFUT.js +3 -0
- package/dist/chunks/OJPBI7KB.js +3 -0
- package/dist/chunks/OMN6C7J6.js +3 -0
- package/dist/chunks/PBMBFWLQ.js +3 -0
- package/dist/chunks/PBRQ3HEG.js +3 -0
- package/dist/chunks/PFFVGFPL.js +45 -0
- package/dist/chunks/QRGNEKME.js +5 -0
- package/dist/chunks/TNLCNVA2.js +3 -0
- package/dist/chunks/U27JJKJ6.js +3 -0
- package/dist/chunks/U6QAYG3E.js +3 -0
- package/dist/chunks/UEOQE77N.js +4 -0
- package/dist/chunks/WSACSE45.js +4 -0
- package/dist/chunks/WY7WHEJ4.js +3 -0
- package/dist/chunks/XJKWLK43.js +4 -0
- package/dist/chunks/XN6D5S55.js +3 -0
- package/dist/chunks/YD3MIP7B.js +5 -0
- package/dist/chunks/YXDQ62BB.js +4 -0
- package/dist/cli.js +21 -294
- package/dist/connector/admin-broker.js +3 -303
- package/dist/connector/main.js +20 -18
- package/dist/harfbuzz-subset.wasm +0 -0
- package/dist/postinstall.js +17 -0
- package/herdr-plugin.toml +1 -1
- package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
- package/node_modules/herdr-remote-relay/package.json +3 -46
- package/package.json +8 -27
- package/dist/agent-status.js +0 -130
- package/dist/binary-plist.js +0 -157
- package/dist/config.js +0 -278
- package/dist/connector/admin-broker-setup.js +0 -40
- package/dist/connector/admin-shell.js +0 -67
- package/dist/connector/admin-tab.js +0 -90
- package/dist/connector/adminBrokerEndpoint.js +0 -68
- package/dist/connector/agent-watch.js +0 -123
- package/dist/connector/elevatedPty.js +0 -141
- package/dist/connector/font-server.js +0 -165
- package/dist/connector/herdr-prefix-keys.js +0 -28
- package/dist/connector/host-connector.js +0 -433
- package/dist/connector/host-hello.js +0 -32
- package/dist/connector/lock.js +0 -73
- package/dist/connector/sessions.js +0 -381
- package/dist/connector/update-report.js +0 -70
- package/dist/connector/wire.js +0 -42
- package/dist/exit-codes.js +0 -13
- package/dist/font-subset.js +0 -116
- package/dist/herdr-api.js +0 -254
- package/dist/herdr-command.js +0 -245
- package/dist/herdr-plugin.js +0 -120
- package/dist/herdr-server.js +0 -190
- package/dist/i18n/en.js +0 -257
- package/dist/i18n/index.js +0 -48
- package/dist/i18n/zh.js +0 -256
- package/dist/keepalive/detached.js +0 -97
- package/dist/keepalive/environment.js +0 -44
- package/dist/keepalive/index.js +0 -129
- package/dist/keepalive/launchd.js +0 -117
- package/dist/keepalive/systemd.js +0 -112
- package/dist/keepalive/types.js +0 -1
- package/dist/keepalive/windows-service-setup.js +0 -138
- package/dist/keepalive/windows-service.js +0 -272
- package/dist/keepalive/windows-task.js +0 -191
- package/dist/lib/process.js +0 -132
- package/dist/lifecycle.js +0 -79
- package/dist/net-interfaces.js +0 -82
- package/dist/node-pty-loader.js +0 -80
- package/dist/pasted-files.js +0 -115
- package/dist/paths.js +0 -74
- package/dist/pty-session.js +0 -133
- package/dist/relay-client.js +0 -106
- package/dist/relay-urls.js +0 -99
- package/dist/runtime.js +0 -107
- package/dist/service.js +0 -267
- package/dist/settings-model.js +0 -298
- package/dist/socket-discovery.js +0 -70
- package/dist/supervisor.js +0 -323
- package/dist/terminal-font/deps.js +0 -83
- package/dist/terminal-font/files.js +0 -357
- package/dist/terminal-font/formats.js +0 -355
- package/dist/terminal-font/index.js +0 -187
- package/dist/terminal-font/readers.js +0 -373
- package/dist/terminal-font/terminals.js +0 -147
- package/dist/terminal-palette.js +0 -399
- package/dist/tui/App.js +0 -199
- package/dist/tui/api.js +0 -91
- package/dist/tui/components/TextField.js +0 -50
- package/dist/tui/components/common.js +0 -80
- package/dist/tui/index.js +0 -36
- package/dist/tui/mouse/index.js +0 -164
- package/dist/tui/mouse/protocol.js +0 -96
- package/dist/tui/mouse/source.js +0 -79
- package/dist/tui/screens/About.js +0 -172
- package/dist/tui/screens/Herdr.js +0 -75
- package/dist/tui/screens/Keepalive.js +0 -141
- package/dist/tui/screens/Overview.js +0 -36
- package/dist/tui/screens/Pair.js +0 -74
- package/dist/tui/screens/Relay.js +0 -243
- package/dist/tui/screens/Services.js +0 -63
- package/dist/tui/screens/Wizard.js +0 -241
- package/dist/tui/terminal.js +0 -39
- package/dist/tui/theme.js +0 -33
- package/dist/update-command.js +0 -118
- package/dist/updater/in-place.js +0 -83
- package/dist/updater/npm.js +0 -62
- package/dist/updater.js +0 -401
- package/node_modules/herdr-remote-relay/README.md +0 -76
- package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
- package/node_modules/herdr-remote-relay/config.example.json +0 -18
- package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
- package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
- package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
- package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
- package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
- package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
- package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
- package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
- package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
- package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
- package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
- package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
- package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
- package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
- package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
- package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
- package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
- package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
- package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
- package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
- package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
- package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
- package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
- package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
- package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
- package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
- package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
- package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
- package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
- package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
- package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
- package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
- package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
- package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
- package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
- package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
- package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
- package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
- package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
- package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
- package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
- package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
- package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
- package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
- package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
- package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
- package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
- package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
- package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
- package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
- package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
- package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
- package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
- package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
- package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
- package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
- package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
- package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
- package/node_modules/herdr-remote-relay/dist/state.js +0 -47
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -22
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -530
- package/node_modules/ws/lib/receiver.js +0 -743
- package/node_modules/ws/lib/sender.js +0 -607
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -562
- package/node_modules/ws/lib/websocket.js +0 -1407
- package/node_modules/ws/package.json +0 -74
- package/node_modules/ws/wrapper.mjs +0 -21
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { configDir, stateDir } from '../paths.js';
|
|
5
|
-
import { detachedLogsHint } from './detached.js';
|
|
6
|
-
import { cliEntryPoint, serviceEnvironment } from './environment.js';
|
|
7
|
-
import { currentServiceAccount, encodePowerShellCommand, escapePowerShellSingleQuoted, serviceId, windowsService, } from './windows-service.js';
|
|
8
|
-
const TASK_START_TIMEOUT_MS = 15_000;
|
|
9
|
-
const TASK_STOP_TIMEOUT_MS = 15_000;
|
|
10
|
-
const TASK_POLL_INTERVAL_MS = 250;
|
|
11
|
-
export function windowsTaskName(username = currentServiceAccount().user) {
|
|
12
|
-
return serviceId(username);
|
|
13
|
-
}
|
|
14
|
-
export function renderWindowsTaskLaunchScript(options) {
|
|
15
|
-
const environment = {
|
|
16
|
-
...options.environment,
|
|
17
|
-
HERDR_REMOTE_CONFIG_DIR: options.configDirectory,
|
|
18
|
-
HERDR_REMOTE_STATE_DIR: options.stateDirectory,
|
|
19
|
-
HERDR_REMOTE_SERVICE: '1',
|
|
20
|
-
};
|
|
21
|
-
const environmentLines = Object.entries(environment).map(([key, value]) => `$env:${key} = '${escapePowerShellSingleQuoted(value)}'`);
|
|
22
|
-
return `$ErrorActionPreference = 'Stop'
|
|
23
|
-
${environmentLines.join('\n')}
|
|
24
|
-
Set-Location -LiteralPath '${escapePowerShellSingleQuoted(options.workingDirectory)}'
|
|
25
|
-
& '${escapePowerShellSingleQuoted(options.nodePath)}' '${escapePowerShellSingleQuoted(options.entryPoint)}' run --daemon
|
|
26
|
-
exit $LASTEXITCODE`;
|
|
27
|
-
}
|
|
28
|
-
export function renderWindowsTaskRegistrationScript(options) {
|
|
29
|
-
const taskName = escapePowerShellSingleQuoted(options.taskName);
|
|
30
|
-
const account = escapePowerShellSingleQuoted(options.account);
|
|
31
|
-
const powershellPath = escapePowerShellSingleQuoted(options.powershellPath);
|
|
32
|
-
const workingDirectory = escapePowerShellSingleQuoted(options.workingDirectory);
|
|
33
|
-
const launchArguments = `-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encodePowerShellCommand(options.launchScript)}`;
|
|
34
|
-
return `$ErrorActionPreference = 'Stop'
|
|
35
|
-
$action = New-ScheduledTaskAction -Execute '${powershellPath}' -Argument '${launchArguments}' -WorkingDirectory '${workingDirectory}'
|
|
36
|
-
$trigger = New-ScheduledTaskTrigger -AtLogOn -User '${account}'
|
|
37
|
-
# Interactive logon uses the signed-in token and avoids storing account credentials.
|
|
38
|
-
$principal = New-ScheduledTaskPrincipal -UserId '${account}' -LogonType Interactive -RunLevel Limited
|
|
39
|
-
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
|
|
40
|
-
Register-ScheduledTask -TaskName '${taskName}' -Description 'Herdr Remote keep-alive' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
|
41
|
-
Enable-ScheduledTask -TaskName '${taskName}' | Out-Null
|
|
42
|
-
Start-ScheduledTask -TaskName '${taskName}'`;
|
|
43
|
-
}
|
|
44
|
-
export function parseWindowsTaskState(output) {
|
|
45
|
-
try {
|
|
46
|
-
const state = JSON.parse(output.replace(/^\uFEFF/, '').trim()).State;
|
|
47
|
-
return state === 'Ready' || state === 'Queued' || state === 'Running' || state === 'Disabled'
|
|
48
|
-
? state
|
|
49
|
-
: 'Absent';
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// Task status is best-effort; malformed scheduler output means no known task.
|
|
53
|
-
return 'Absent';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function windowsPowerShellPath() {
|
|
57
|
-
const root = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
|
|
58
|
-
return path.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
59
|
-
}
|
|
60
|
-
function taskStateScript(taskName) {
|
|
61
|
-
const safeName = escapePowerShellSingleQuoted(taskName);
|
|
62
|
-
return `$ErrorActionPreference = 'Stop'
|
|
63
|
-
$task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
|
|
64
|
-
if ($null -eq $task) { '{"State":"Absent"}'; exit 0 }
|
|
65
|
-
[pscustomobject]@{ State = [string]$task.State } | ConvertTo-Json -Compress`;
|
|
66
|
-
}
|
|
67
|
-
function invokePowerShell(script, label) {
|
|
68
|
-
const result = spawnSync(windowsPowerShellPath(), [
|
|
69
|
-
'-NoLogo',
|
|
70
|
-
'-NoProfile',
|
|
71
|
-
'-NonInteractive',
|
|
72
|
-
'-EncodedCommand',
|
|
73
|
-
encodePowerShellCommand(script),
|
|
74
|
-
], { encoding: 'utf8', windowsHide: true });
|
|
75
|
-
if (result.error || result.status !== 0) {
|
|
76
|
-
const output = String(result.stderr || result.stdout || '').trim();
|
|
77
|
-
throw new Error(`${label} failed (${String(result.status)}): ${output}`);
|
|
78
|
-
}
|
|
79
|
-
return String(result.stdout || '').trim();
|
|
80
|
-
}
|
|
81
|
-
function taskState() {
|
|
82
|
-
const result = spawnSync(windowsPowerShellPath(), [
|
|
83
|
-
'-NoProfile',
|
|
84
|
-
'-NoLogo',
|
|
85
|
-
'-NonInteractive',
|
|
86
|
-
'-EncodedCommand',
|
|
87
|
-
encodePowerShellCommand(taskStateScript(windowsTaskName())),
|
|
88
|
-
], { encoding: 'utf8', windowsHide: true });
|
|
89
|
-
if (result.error || result.status !== 0) {
|
|
90
|
-
// A status query must not make unrelated CLI commands fail on an unavailable scheduler.
|
|
91
|
-
return 'Absent';
|
|
92
|
-
}
|
|
93
|
-
return parseWindowsTaskState(String(result.stdout || ''));
|
|
94
|
-
}
|
|
95
|
-
function accountName() {
|
|
96
|
-
const { domain, user } = currentServiceAccount();
|
|
97
|
-
return domain ? `${domain}\\${user}` : user;
|
|
98
|
-
}
|
|
99
|
-
function stopTaskScript(taskName) {
|
|
100
|
-
const safeName = escapePowerShellSingleQuoted(taskName);
|
|
101
|
-
return `$ErrorActionPreference = 'Stop'
|
|
102
|
-
$task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
|
|
103
|
-
if ($null -eq $task -or ($task.State -ne 'Running' -and $task.State -ne 'Queued')) { exit 0 }
|
|
104
|
-
Stop-ScheduledTask -TaskName '${safeName}'
|
|
105
|
-
$deadline = [DateTime]::UtcNow.AddMilliseconds(${TASK_STOP_TIMEOUT_MS})
|
|
106
|
-
do {
|
|
107
|
-
Start-Sleep -Milliseconds ${TASK_POLL_INTERVAL_MS}
|
|
108
|
-
$task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
|
|
109
|
-
} while ($task -and ($task.State -eq 'Running' -or $task.State -eq 'Queued') -and [DateTime]::UtcNow -lt $deadline)
|
|
110
|
-
if ($task -and ($task.State -eq 'Running' -or $task.State -eq 'Queued')) { throw 'Scheduled task did not stop in time.' }`;
|
|
111
|
-
}
|
|
112
|
-
function stopTask() {
|
|
113
|
-
const state = taskState();
|
|
114
|
-
if (state === 'Running' || state === 'Queued')
|
|
115
|
-
invokePowerShell(stopTaskScript(windowsTaskName()), 'Stopping the Windows scheduled task');
|
|
116
|
-
}
|
|
117
|
-
function launchOptions() {
|
|
118
|
-
const workingDirectory = stateDir();
|
|
119
|
-
return {
|
|
120
|
-
nodePath: process.execPath,
|
|
121
|
-
entryPoint: cliEntryPoint(),
|
|
122
|
-
workingDirectory,
|
|
123
|
-
configDirectory: configDir(),
|
|
124
|
-
stateDirectory: workingDirectory,
|
|
125
|
-
environment: serviceEnvironment(),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function status() {
|
|
129
|
-
const state = taskState();
|
|
130
|
-
const installed = state !== 'Absent';
|
|
131
|
-
return {
|
|
132
|
-
manager: 'windows-task',
|
|
133
|
-
installed,
|
|
134
|
-
active: state === 'Running',
|
|
135
|
-
enabled: installed && state !== 'Disabled',
|
|
136
|
-
state: state.toLowerCase(),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
async function install() {
|
|
140
|
-
if (taskState() !== 'Absent')
|
|
141
|
-
stopTask();
|
|
142
|
-
// Remove the earlier service before starting a second supervisor for this user.
|
|
143
|
-
if (windowsService.status().installed)
|
|
144
|
-
await windowsService.uninstall();
|
|
145
|
-
const options = launchOptions();
|
|
146
|
-
await fs.promises.mkdir(options.workingDirectory, { recursive: true });
|
|
147
|
-
const launchScript = renderWindowsTaskLaunchScript(options);
|
|
148
|
-
const script = renderWindowsTaskRegistrationScript({
|
|
149
|
-
taskName: windowsTaskName(),
|
|
150
|
-
account: accountName(),
|
|
151
|
-
powershellPath: windowsPowerShellPath(),
|
|
152
|
-
workingDirectory: options.workingDirectory,
|
|
153
|
-
launchScript,
|
|
154
|
-
});
|
|
155
|
-
invokePowerShell(script, 'Installing the Windows scheduled task');
|
|
156
|
-
const deadline = Date.now() + TASK_START_TIMEOUT_MS;
|
|
157
|
-
let state = taskState();
|
|
158
|
-
while (state !== 'Running' && state !== 'Absent' && Date.now() < deadline) {
|
|
159
|
-
await new Promise((resolve) => setTimeout(resolve, TASK_POLL_INTERVAL_MS));
|
|
160
|
-
state = taskState();
|
|
161
|
-
}
|
|
162
|
-
if (state !== 'Running')
|
|
163
|
-
throw new Error(`Windows scheduled task did not start (state: ${state.toLowerCase()}).`);
|
|
164
|
-
return { ok: true, taskName: windowsTaskName(), state };
|
|
165
|
-
}
|
|
166
|
-
async function uninstall() {
|
|
167
|
-
const name = windowsTaskName();
|
|
168
|
-
if (taskState() !== 'Absent') {
|
|
169
|
-
stopTask();
|
|
170
|
-
const safeName = escapePowerShellSingleQuoted(name);
|
|
171
|
-
invokePowerShell(`Unregister-ScheduledTask -TaskName '${safeName}' -Confirm:$false -ErrorAction Stop`, 'Removing the Windows scheduled task');
|
|
172
|
-
}
|
|
173
|
-
if (windowsService.status().installed)
|
|
174
|
-
await windowsService.uninstall();
|
|
175
|
-
return { ok: true, taskName: name };
|
|
176
|
-
}
|
|
177
|
-
function restart() {
|
|
178
|
-
stopTask();
|
|
179
|
-
const safeName = escapePowerShellSingleQuoted(windowsTaskName());
|
|
180
|
-
invokePowerShell(`Start-ScheduledTask -TaskName '${safeName}'`, 'Starting the Windows scheduled task');
|
|
181
|
-
return { ok: true };
|
|
182
|
-
}
|
|
183
|
-
export const windowsTask = {
|
|
184
|
-
name: 'windows-task',
|
|
185
|
-
status,
|
|
186
|
-
install,
|
|
187
|
-
uninstall,
|
|
188
|
-
restart,
|
|
189
|
-
stop: stopTask,
|
|
190
|
-
logsHint: () => detachedLogsHint('win32'),
|
|
191
|
-
};
|
package/dist/lib/process.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
const PROCESS_QUERY_TIMEOUT_MS = 10_000;
|
|
4
|
-
// Records written before start tokens existed carry only a pid. Until none are
|
|
5
|
-
// left, such a record is trusted only while its process runs one of our entry
|
|
6
|
-
// points: the relay, the host connector or `herdr-remote run`.
|
|
7
|
-
const OUR_ENTRY_POINT = /herdr-remote-relay\.js|connector[\\/]main\.js|herdr-remote\.js"?\s+run\b/;
|
|
8
|
-
/** Whether `pid` names a running process, including one owned by another user. */
|
|
9
|
-
export function pidAlive(pid) {
|
|
10
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
11
|
-
return false;
|
|
12
|
-
try {
|
|
13
|
-
process.kill(pid, 0);
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
return error.code === 'EPERM';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function runCommand(command, args) {
|
|
21
|
-
const result = spawnSync(command, args, {
|
|
22
|
-
encoding: 'utf8',
|
|
23
|
-
// The same instant must print the same text whenever and wherever it is asked.
|
|
24
|
-
env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' },
|
|
25
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
-
timeout: PROCESS_QUERY_TIMEOUT_MS,
|
|
27
|
-
windowsHide: true,
|
|
28
|
-
});
|
|
29
|
-
// The exit code is not trusted: ps and PowerShell fail when any one pid is
|
|
30
|
-
// gone, yet still print the rest. A command that could not run prints nothing.
|
|
31
|
-
return typeof result.stdout === 'string' ? result.stdout : '';
|
|
32
|
-
}
|
|
33
|
-
/** `<pid> <value>` lines, as ps and our PowerShell queries print them. */
|
|
34
|
-
function parsePidLines(output) {
|
|
35
|
-
const details = new Map();
|
|
36
|
-
for (const line of output.split(/\r?\n/)) {
|
|
37
|
-
const match = /^\s*(\d+)\s+(\S.*?)\s*$/.exec(line);
|
|
38
|
-
if (match)
|
|
39
|
-
details.set(Number(match[1]), match[2]);
|
|
40
|
-
}
|
|
41
|
-
return details;
|
|
42
|
-
}
|
|
43
|
-
function linuxDetails(pids, detail, run) {
|
|
44
|
-
let bootId = '';
|
|
45
|
-
if (detail === 'start') {
|
|
46
|
-
try {
|
|
47
|
-
// Start times count clock ticks since boot, so they only identify a
|
|
48
|
-
// process together with the boot they were counted in.
|
|
49
|
-
bootId = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// A /proc without boot ids (some sandboxes) still has ps.
|
|
53
|
-
return psDetails(pids, detail, run);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const details = new Map();
|
|
57
|
-
for (const pid of pids) {
|
|
58
|
-
let text;
|
|
59
|
-
try {
|
|
60
|
-
text = fs.readFileSync(`/proc/${pid}/${detail === 'start' ? 'stat' : 'cmdline'}`, 'utf8');
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// Not running, or hidden from us: either way it is not a process we can vouch for.
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (detail === 'command') {
|
|
67
|
-
details.set(pid, text.replaceAll('\0', ' ').trim());
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
// The command name before this may itself contain spaces and parentheses;
|
|
71
|
-
// after it, field 3 (state) comes first and field 22 is the start time.
|
|
72
|
-
const fields = text.slice(text.lastIndexOf(')') + 2).split(' ');
|
|
73
|
-
if (fields[19])
|
|
74
|
-
details.set(pid, `${bootId}:${fields[19]}`);
|
|
75
|
-
}
|
|
76
|
-
return details;
|
|
77
|
-
}
|
|
78
|
-
function psDetails(pids, detail, run) {
|
|
79
|
-
const column = detail === 'start' ? 'lstart' : 'command';
|
|
80
|
-
return parsePidLines(run('ps', ['-o', `pid=,${column}=`, '-p', pids.join(',')]));
|
|
81
|
-
}
|
|
82
|
-
function windowsDetails(pids, detail, run) {
|
|
83
|
-
const script = detail === 'start'
|
|
84
|
-
? // StartTime is empty for processes we may not inspect; those are not ours anyway.
|
|
85
|
-
`Get-Process -Id ${pids.join(',')} -ErrorAction SilentlyContinue | Where-Object StartTime | ForEach-Object { "$($_.Id) $($_.StartTime.ToFileTimeUtc())" }`
|
|
86
|
-
: `Get-CimInstance Win32_Process -Filter "${pids.map((pid) => `ProcessId=${pid}`).join(' OR ')}" | ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }`;
|
|
87
|
-
return parsePidLines(run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]));
|
|
88
|
-
}
|
|
89
|
-
function processDetails(pids, detail, platform, run) {
|
|
90
|
-
// Dead pids are the common case after a clean stop, and ruling them out here
|
|
91
|
-
// saves spawning ps or PowerShell just to hear so.
|
|
92
|
-
const candidates = [...new Set(pids)].filter(pidAlive);
|
|
93
|
-
if (candidates.length === 0)
|
|
94
|
-
return new Map();
|
|
95
|
-
if (platform === 'linux')
|
|
96
|
-
return linuxDetails(candidates, detail, run);
|
|
97
|
-
if (platform === 'win32')
|
|
98
|
-
return windowsDetails(candidates, detail, run);
|
|
99
|
-
return psDetails(candidates, detail, run);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* When each running process in `pids` started, as opaque tokens; pids that are
|
|
103
|
-
* not running, or that the system will not describe, are left out.
|
|
104
|
-
*
|
|
105
|
-
* A pid alone does not identify a process: the number goes to another process
|
|
106
|
-
* once this one exits, and after a reboot the same boot-time services land on
|
|
107
|
-
* the same numbers again. On Linux the number of a thread in another process
|
|
108
|
-
* even passes `pidAlive`. Recorded next to its pid, this token only ever
|
|
109
|
-
* matches the process it was read from.
|
|
110
|
-
*/
|
|
111
|
-
export function processStarts(pids, platform = process.platform, run = runCommand) {
|
|
112
|
-
return processDetails(pids, 'start', platform, run);
|
|
113
|
-
}
|
|
114
|
-
/** The command line of each running process in `pids`, which the system is willing to show. */
|
|
115
|
-
export function processCommands(pids, platform = process.platform, run = runCommand) {
|
|
116
|
-
return processDetails(pids, 'command', platform, run);
|
|
117
|
-
}
|
|
118
|
-
export function processStart(pid, platform = process.platform) {
|
|
119
|
-
return processStarts([pid], platform).get(pid) ?? null;
|
|
120
|
-
}
|
|
121
|
-
/** The records that still name the very process they were taken from. */
|
|
122
|
-
export function stillRunning(records) {
|
|
123
|
-
const pidsOf = (list) => list.map((record) => record.pid).filter((pid) => Number.isInteger(pid));
|
|
124
|
-
const legacy = records.filter((record) => typeof record.processStart !== 'string');
|
|
125
|
-
const starts = processStarts(pidsOf(records.filter((record) => !legacy.includes(record))));
|
|
126
|
-
const commands = legacy.length > 0 ? processCommands(pidsOf(legacy)) : new Map();
|
|
127
|
-
return records.filter((record) => legacy.includes(record)
|
|
128
|
-
? // We only ever write start tokens, so a legacy record naming our own pid
|
|
129
|
-
// was left by an earlier process that happened to have it.
|
|
130
|
-
record.pid !== process.pid && OUR_ENTRY_POINT.test(commands.get(record.pid) ?? '')
|
|
131
|
-
: starts.get(record.pid) === record.processStart);
|
|
132
|
-
}
|
package/dist/lifecycle.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// Start/stop/restart that respect the keep-alive manager.
|
|
2
|
-
//
|
|
3
|
-
// When systemd or launchd supervises the services, killing the processes just
|
|
4
|
-
// makes the manager start them again — and starting them by hand produces a
|
|
5
|
-
// second, unmanaged copy fighting for the port. Every entry point goes through
|
|
6
|
-
// here so the managed and unmanaged paths behave the same from the outside.
|
|
7
|
-
import { loadConfig } from './config.js';
|
|
8
|
-
import * as keepalive from './keepalive/index.js';
|
|
9
|
-
import { managedPids, readRuntime } from './runtime.js';
|
|
10
|
-
import { restartServices, startServices, statusServices, stopServices } from './service.js';
|
|
11
|
-
function managerInUse(config) {
|
|
12
|
-
const status = keepalive.status(config);
|
|
13
|
-
return status.installed || status.active ? status : null;
|
|
14
|
-
}
|
|
15
|
-
function startAll(config = loadConfig()) {
|
|
16
|
-
const managed = managerInUse(config);
|
|
17
|
-
if (managed) {
|
|
18
|
-
// Start means "make it run", not "restart it". Herdr runs this through the
|
|
19
|
-
// plugin's startup hook every time a Herdr server starts, and a restart
|
|
20
|
-
// there dropped every browser attached to the workstation — including the
|
|
21
|
-
// one that had just asked for that Herdr to be started.
|
|
22
|
-
if (!managed.active)
|
|
23
|
-
keepalive.restart(config);
|
|
24
|
-
return {
|
|
25
|
-
ok: true,
|
|
26
|
-
managed: true,
|
|
27
|
-
manager: managed.manager,
|
|
28
|
-
alreadyRunning: Boolean(managed.active),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
return { ...startServices(), managed: false };
|
|
32
|
-
}
|
|
33
|
-
function stopAll(config = loadConfig()) {
|
|
34
|
-
const result = keepalive.stopManaged(config);
|
|
35
|
-
if (result.managed) {
|
|
36
|
-
// The manager stops the supervisor, which stops its children; clear the
|
|
37
|
-
// recorded pids so status does not report ghosts.
|
|
38
|
-
stopServices();
|
|
39
|
-
return { ok: true, managed: true, manager: result.manager };
|
|
40
|
-
}
|
|
41
|
-
return { ...stopServices(), managed: false };
|
|
42
|
-
}
|
|
43
|
-
function restartAll(config = loadConfig()) {
|
|
44
|
-
const managed = managerInUse(config);
|
|
45
|
-
if (managed) {
|
|
46
|
-
keepalive.restart(config);
|
|
47
|
-
return { ok: true, managed: true, manager: managed.manager };
|
|
48
|
-
}
|
|
49
|
-
return { ...restartServices(), managed: false };
|
|
50
|
-
}
|
|
51
|
-
/** Whether the host connector is up, under a keep-alive manager or detached. */
|
|
52
|
-
function servicesRunning(config = loadConfig()) {
|
|
53
|
-
if (managerInUse(config)?.active)
|
|
54
|
-
return true;
|
|
55
|
-
const state = readRuntime();
|
|
56
|
-
return managedPids(state).some(({ pid }) => pid === state.hostPid);
|
|
57
|
-
}
|
|
58
|
-
/** Stop Windows keep-alive processes while npm replaces their package files. */
|
|
59
|
-
async function whileServicesStopped(config, task, { platform = process.platform, isRunning = () => servicesRunning(config), stop = stopAll, start = startAll, } = {}) {
|
|
60
|
-
if (platform !== 'win32' || !isRunning())
|
|
61
|
-
return task();
|
|
62
|
-
await stop(config);
|
|
63
|
-
try {
|
|
64
|
-
return await task();
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
await start(config);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/** Service status plus the keep-alive picture, which is what the TUI shows. */
|
|
71
|
-
async function fullStatus(config = loadConfig()) {
|
|
72
|
-
const [services, keepaliveStatus] = [await statusServices(), keepalive.status(config)];
|
|
73
|
-
return {
|
|
74
|
-
...services,
|
|
75
|
-
keepalive: keepaliveStatus,
|
|
76
|
-
logsHint: keepalive.logsHint(config),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
export { startAll, stopAll, restartAll, fullStatus, managerInUse, servicesRunning, whileServicesStopped, };
|
package/dist/net-interfaces.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
// Interface names that are almost always virtual bridges rather than something
|
|
3
|
-
// a phone can reach. They are still listed, just ranked last.
|
|
4
|
-
const VIRTUAL_NAME_PATTERN = /^(docker|br-|virbr|veth|vmnet|vboxnet|lxcbr|cni|flannel|kube|vmware network adapter|virtualbox host-only)/i;
|
|
5
|
-
const TAILSCALE_NAME_PATTERN = /^(tailscale|ts)\d*$/i;
|
|
6
|
-
/**
|
|
7
|
-
* Is this address inside Tailscale's 100.64.0.0/10 CGNAT range? Interface names
|
|
8
|
-
* differ per platform (tailscale0 on Linux, utunN on macOS), so the address
|
|
9
|
-
* range is the portable signal.
|
|
10
|
-
*/
|
|
11
|
-
function isTailscaleAddress(address) {
|
|
12
|
-
const octets = String(address).split('.');
|
|
13
|
-
if (octets.length !== 4)
|
|
14
|
-
return false;
|
|
15
|
-
const first = Number(octets[0]);
|
|
16
|
-
const second = Number(octets[1]);
|
|
17
|
-
if (!Number.isInteger(first) || !Number.isInteger(second))
|
|
18
|
-
return false;
|
|
19
|
-
return first === 100 && second >= 64 && second <= 127;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Windows assigns 169.254 addresses when DHCP fails, and other devices cannot
|
|
23
|
-
* reach them.
|
|
24
|
-
*/
|
|
25
|
-
function isLinkLocalAddress(address) {
|
|
26
|
-
const octets = String(address).split('.');
|
|
27
|
-
if (octets.length !== 4)
|
|
28
|
-
return false;
|
|
29
|
-
return Number(octets[0]) === 169 && Number(octets[1]) === 254;
|
|
30
|
-
}
|
|
31
|
-
function classify(name, info) {
|
|
32
|
-
if (info.internal)
|
|
33
|
-
return 'loopback';
|
|
34
|
-
if (TAILSCALE_NAME_PATTERN.test(name) || isTailscaleAddress(info.address))
|
|
35
|
-
return 'tailscale';
|
|
36
|
-
if (isLinkLocalAddress(info.address) || VIRTUAL_NAME_PATTERN.test(name))
|
|
37
|
-
return 'virtual';
|
|
38
|
-
return 'lan';
|
|
39
|
-
}
|
|
40
|
-
const KIND_ORDER = { tailscale: 0, lan: 1, virtual: 2, loopback: 3 };
|
|
41
|
-
/**
|
|
42
|
-
* Addresses this machine can be reached at, best candidate first.
|
|
43
|
-
*
|
|
44
|
-
* Tailscale addresses come first because a device on the tailnet reaches them
|
|
45
|
-
* from anywhere, which is exactly the "works away from home without running a
|
|
46
|
-
* relay" case; plain LAN addresses follow, then virtual bridges, then loopback.
|
|
47
|
-
*/
|
|
48
|
-
function listReachableAddresses({ includeLoopback = true, includeIpv6 = false, interfaces = os.networkInterfaces(), } = {}) {
|
|
49
|
-
const results = [];
|
|
50
|
-
for (const [name, entries] of Object.entries(interfaces)) {
|
|
51
|
-
for (const info of entries || []) {
|
|
52
|
-
// Older Node releases reported the family as a number.
|
|
53
|
-
const rawFamily = info.family;
|
|
54
|
-
const family = typeof rawFamily === 'string' ? rawFamily : `IPv${rawFamily}`;
|
|
55
|
-
if (family !== 'IPv4' && !(includeIpv6 && family === 'IPv6'))
|
|
56
|
-
continue;
|
|
57
|
-
const kind = classify(name, info);
|
|
58
|
-
if (kind === 'loopback' && !includeLoopback)
|
|
59
|
-
continue;
|
|
60
|
-
results.push({
|
|
61
|
-
name,
|
|
62
|
-
address: info.address,
|
|
63
|
-
family,
|
|
64
|
-
kind,
|
|
65
|
-
internal: Boolean(info.internal),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
results.sort((a, b) => {
|
|
70
|
-
const byKind = KIND_ORDER[a.kind] - KIND_ORDER[b.kind];
|
|
71
|
-
if (byKind !== 0)
|
|
72
|
-
return byKind;
|
|
73
|
-
return a.address.localeCompare(b.address);
|
|
74
|
-
});
|
|
75
|
-
return results;
|
|
76
|
-
}
|
|
77
|
-
/** Best guess for the address to advertise when binding to every interface. */
|
|
78
|
-
function preferredLanAddress(interfaces = os.networkInterfaces()) {
|
|
79
|
-
const candidates = listReachableAddresses({ includeLoopback: false, interfaces });
|
|
80
|
-
return candidates.length > 0 ? candidates[0].address : null;
|
|
81
|
-
}
|
|
82
|
-
export { isTailscaleAddress, listReachableAddresses, preferredLanAddress };
|
package/dist/node-pty-loader.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// Where node-pty is loaded from.
|
|
2
|
-
//
|
|
3
|
-
// npm updates this package by renaming its directory and deleting the old one,
|
|
4
|
-
// and Windows refuses both while a process has a file in it loaded: node-pty's
|
|
5
|
-
// .node modules, or the OpenConsole.exe that ConPTY starts from beside them.
|
|
6
|
-
// The host connector and the admin broker hold those for as long as they run,
|
|
7
|
-
// so on Windows node-pty is loaded from a copy in the state directory, one per
|
|
8
|
-
// build. Copies are never deleted: a process of an older release may still
|
|
9
|
-
// have one loaded, and a build is a few megabytes.
|
|
10
|
-
import { createHash } from 'node:crypto';
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import { createRequire } from 'node:module';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import { stateDir } from './paths.js';
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
/** What node-pty loads or starts; debug symbols are most of the prebuilds' size. */
|
|
17
|
-
const BINARY_EXTENSIONS = ['.node', '.dll', '.exe'];
|
|
18
|
-
/** The files a running node-pty needs, relative to its package directory. */
|
|
19
|
-
function runtimeFiles(source, platform, arch) {
|
|
20
|
-
const files = ['package.json'];
|
|
21
|
-
const collect = (directory, keep) => {
|
|
22
|
-
const absolute = path.join(source, directory);
|
|
23
|
-
if (!fs.existsSync(absolute))
|
|
24
|
-
return;
|
|
25
|
-
for (const entry of fs.readdirSync(absolute, { recursive: true, withFileTypes: true })) {
|
|
26
|
-
if (entry.isFile() && keep(entry.name))
|
|
27
|
-
files.push(path.relative(source, path.join(entry.parentPath, entry.name)));
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
collect('lib', (name) => name.endsWith('.js') && !name.endsWith('.test.js'));
|
|
31
|
-
const binary = (name) => BINARY_EXTENSIONS.includes(path.extname(name));
|
|
32
|
-
// Where node-pty looks for its modules: a local build, then the prebuilds.
|
|
33
|
-
collect(path.join('build', 'Release'), binary);
|
|
34
|
-
collect(path.join('prebuilds', `${platform}-${arch}`), binary);
|
|
35
|
-
return files;
|
|
36
|
-
}
|
|
37
|
-
/** Copy node-pty's run-time files under `directory`, once per build; returns the copy. */
|
|
38
|
-
export function copyNodePty(source, directory, platform, arch) {
|
|
39
|
-
const files = runtimeFiles(source, platform, arch);
|
|
40
|
-
const hash = createHash('sha256');
|
|
41
|
-
for (const file of files)
|
|
42
|
-
hash.update(`${file}\0`).update(fs.readFileSync(path.join(source, file)));
|
|
43
|
-
const target = path.join(directory, hash.digest('hex').slice(0, 16));
|
|
44
|
-
if (fs.existsSync(target))
|
|
45
|
-
return target;
|
|
46
|
-
// Renamed into place whole, so a copy that exists is complete.
|
|
47
|
-
const staging = `${target}.${process.pid}.tmp`;
|
|
48
|
-
for (const file of files) {
|
|
49
|
-
fs.mkdirSync(path.dirname(path.join(staging, file)), { recursive: true });
|
|
50
|
-
fs.copyFileSync(path.join(source, file), path.join(staging, file));
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
fs.renameSync(staging, target);
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
57
|
-
// Another process put the same build in place first.
|
|
58
|
-
if (!fs.existsSync(target))
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
return target;
|
|
62
|
-
}
|
|
63
|
-
export function loadNodePty({ platform = process.platform, arch = process.arch, source = path.dirname(require.resolve('node-pty/package.json')), directory = path.join(stateDir(), 'node-pty'), warn = (message) => process.stderr.write(`${message}\n`), } = {}) {
|
|
64
|
-
if (platform !== 'win32')
|
|
65
|
-
return require(source);
|
|
66
|
-
try {
|
|
67
|
-
return require(copyNodePty(source, directory, platform, arch));
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
// Terminals still work from the package; only an update has to wait for them to end.
|
|
71
|
-
warn(`herdr-remote: could not copy node-pty out of the package: ${error.message}`);
|
|
72
|
-
return require(source);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
let loaded = null;
|
|
76
|
-
/** node-pty, loaded on first use: only processes that start terminals pay for it. */
|
|
77
|
-
export function nodePty() {
|
|
78
|
-
loaded ??= loadNodePty();
|
|
79
|
-
return loaded;
|
|
80
|
-
}
|
package/dist/pasted-files.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Secure file storage and validation for pasted clipboard images.
|
|
3
|
-
*
|
|
4
|
-
* Security requirements:
|
|
5
|
-
* 1. Pinned directory: writes strictly to path.join(stateDir(), 'pasted') (directory mode 0o700).
|
|
6
|
-
* 2. Unpredictable filename: generated via crypto.randomUUID(), never using client-provided strings.
|
|
7
|
-
* 3. Whitelisted extension: derived exclusively from verified MIME type.
|
|
8
|
-
* 4. Host-side size check: re-validated to enforce <= 3 MB independently of relay checks.
|
|
9
|
-
* 5. Magic bytes sniffing: inspects raw binary headers (PNG, JPEG, GIF, WebP) to prevent file-type spoofing.
|
|
10
|
-
* 6. File mode: files are created with mode 0o600.
|
|
11
|
-
* 7. Automatic pruning: caps at 20 files and 50 MB (oldest deleted first), removes >24h stale files at startup.
|
|
12
|
-
*/
|
|
13
|
-
import crypto from 'node:crypto';
|
|
14
|
-
import fs from 'node:fs';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
import { stateDir } from './paths.js';
|
|
17
|
-
import { ensureDir } from 'herdr-remote-relay/state';
|
|
18
|
-
import { PASTE_MAX_BYTES, PASTE_IMAGE_EXTENSIONS, isPasteImageMime, hasImageSignature, } from 'herdr-remote-relay/protocol';
|
|
19
|
-
const MAX_SAVED_FILES = 20;
|
|
20
|
-
const MAX_TOTAL_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
21
|
-
const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
22
|
-
function getPastedDir() {
|
|
23
|
-
const dir = path.join(stateDir(), 'pasted');
|
|
24
|
-
ensureDir(dir);
|
|
25
|
-
return dir;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Prunes the pasted directory:
|
|
29
|
-
* - Removes files older than maxAgeMs (default 24 hours).
|
|
30
|
-
* - Caps total file count (default 20) and total disk footprint (default 50 MB),
|
|
31
|
-
* deleting oldest files first.
|
|
32
|
-
*/
|
|
33
|
-
function cleanPastedDir({ dir = getPastedDir(), maxFiles = MAX_SAVED_FILES, maxBytes = MAX_TOTAL_BYTES, maxAgeMs = MAX_AGE_MS, now = Date.now(), } = {}) {
|
|
34
|
-
let entries;
|
|
35
|
-
try {
|
|
36
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const fileInfos = [];
|
|
42
|
-
for (const entry of entries) {
|
|
43
|
-
if (!entry.isFile())
|
|
44
|
-
continue;
|
|
45
|
-
const fullPath = path.join(dir, entry.name);
|
|
46
|
-
try {
|
|
47
|
-
const stat = fs.statSync(fullPath);
|
|
48
|
-
if (now - stat.mtimeMs > maxAgeMs) {
|
|
49
|
-
try {
|
|
50
|
-
fs.unlinkSync(fullPath);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// Removed by another process meanwhile, or not ours to remove: skip it.
|
|
54
|
-
}
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
fileInfos.push({ path: fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// The file vanished between listing and stat; nothing to account for.
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Sort oldest first
|
|
64
|
-
fileInfos.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
65
|
-
let totalSize = fileInfos.reduce((sum, f) => sum + f.size, 0);
|
|
66
|
-
while (fileInfos.length > maxFiles || totalSize > maxBytes) {
|
|
67
|
-
const oldest = fileInfos.shift();
|
|
68
|
-
if (!oldest)
|
|
69
|
-
break;
|
|
70
|
-
try {
|
|
71
|
-
fs.unlinkSync(oldest.path);
|
|
72
|
-
totalSize -= oldest.size;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// Pruning is best effort; the next save prunes again.
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Validates base64 data, checks payload limits, sniffs magic bytes against declared MIME,
|
|
81
|
-
* writes to state storage with mode 0o600, and returns the absolute local path.
|
|
82
|
-
*/
|
|
83
|
-
function savePastedFile({ mime, dataBase64, dir = getPastedDir(), }) {
|
|
84
|
-
if (!isPasteImageMime(mime)) {
|
|
85
|
-
throw new Error(`unsupported MIME type: ${mime}`);
|
|
86
|
-
}
|
|
87
|
-
if (typeof dataBase64 !== 'string') {
|
|
88
|
-
throw new Error('missing or invalid dataBase64 payload');
|
|
89
|
-
}
|
|
90
|
-
const buf = Buffer.from(dataBase64, 'base64');
|
|
91
|
-
if (buf.length > PASTE_MAX_BYTES) {
|
|
92
|
-
throw new Error(`file size (${buf.length} bytes) exceeds maximum limit of 3 MB`);
|
|
93
|
-
}
|
|
94
|
-
if (buf.length === 0) {
|
|
95
|
-
throw new Error('file payload is empty');
|
|
96
|
-
}
|
|
97
|
-
if (!hasImageSignature(mime, buf)) {
|
|
98
|
-
throw new Error(`file magic bytes do not match declared MIME type ${mime}`);
|
|
99
|
-
}
|
|
100
|
-
// Prune before writing new file
|
|
101
|
-
cleanPastedDir({ dir });
|
|
102
|
-
const fileName = `${crypto.randomUUID()}${PASTE_IMAGE_EXTENSIONS[mime]}`;
|
|
103
|
-
const filePath = path.join(dir, fileName);
|
|
104
|
-
fs.writeFileSync(filePath, buf, { mode: 0o600, flag: 'wx' });
|
|
105
|
-
try {
|
|
106
|
-
fs.chmodSync(filePath, 0o600);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// The file was created 0600 already; chmod only guards against a lax umask.
|
|
110
|
-
}
|
|
111
|
-
// Prune again after writing to strictly observe count/size ceiling
|
|
112
|
-
cleanPastedDir({ dir });
|
|
113
|
-
return filePath;
|
|
114
|
-
}
|
|
115
|
-
export { cleanPastedDir, savePastedFile };
|