herdr-remote 0.2.18 → 0.2.20
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/bin/herdr-remote.js +3 -225
- package/dist/agent-status.js +130 -0
- package/dist/binary-plist.js +157 -0
- package/dist/cli.js +255 -0
- package/dist/config.js +270 -0
- package/dist/connector/agent-watch.js +110 -0
- package/dist/connector/font-server.js +165 -0
- package/dist/connector/host-connector.js +430 -0
- package/dist/connector/lock.js +66 -0
- package/dist/connector/main.js +18 -0
- package/dist/connector/sessions.js +366 -0
- package/dist/connector/update-report.js +67 -0
- package/dist/connector/wire.js +42 -0
- package/{src → dist}/exit-codes.js +1 -4
- package/dist/font-subset.js +110 -0
- package/dist/herdr-api.js +246 -0
- package/dist/herdr-command.js +223 -0
- package/dist/herdr-plugin.js +116 -0
- package/dist/herdr-server.js +189 -0
- package/dist/i18n/en.js +223 -0
- package/dist/i18n/index.js +48 -0
- package/dist/i18n/zh.js +222 -0
- package/dist/keepalive/detached.js +78 -0
- package/dist/keepalive/environment.js +44 -0
- package/dist/keepalive/index.js +110 -0
- package/dist/keepalive/launchd.js +117 -0
- package/dist/keepalive/systemd.js +108 -0
- package/dist/keepalive/types.js +1 -0
- package/dist/lib/process.js +12 -0
- package/dist/lifecycle.js +59 -0
- package/dist/net-interfaces.js +73 -0
- package/dist/pasted-files.js +115 -0
- package/dist/paths.js +62 -0
- package/dist/pty-session.js +96 -0
- package/dist/relay-client.js +106 -0
- package/dist/relay-urls.js +99 -0
- package/dist/runtime.js +106 -0
- package/dist/service.js +260 -0
- package/dist/settings-model.js +298 -0
- package/dist/socket-discovery.js +48 -0
- package/dist/supervisor.js +319 -0
- package/dist/terminal-font/deps.js +73 -0
- package/dist/terminal-font/files.js +354 -0
- package/dist/terminal-font/formats.js +335 -0
- package/dist/terminal-font/index.js +187 -0
- package/dist/terminal-font/readers.js +316 -0
- package/dist/terminal-font/terminals.js +128 -0
- package/dist/terminal-palette.js +339 -0
- package/dist/tui/App.js +195 -0
- package/dist/tui/api.js +79 -0
- package/dist/tui/components/TextField.js +50 -0
- package/dist/tui/components/common.js +80 -0
- package/dist/tui/index.js +36 -0
- package/dist/tui/mouse/index.js +164 -0
- package/dist/tui/mouse/protocol.js +96 -0
- package/dist/tui/mouse/source.js +74 -0
- package/dist/tui/screens/About.js +166 -0
- package/dist/tui/screens/Herdr.js +74 -0
- package/dist/tui/screens/Keepalive.js +84 -0
- package/dist/tui/screens/Overview.js +36 -0
- package/dist/tui/screens/Pair.js +74 -0
- package/dist/tui/screens/Relay.js +242 -0
- package/dist/tui/screens/Services.js +63 -0
- package/dist/tui/screens/Wizard.js +235 -0
- package/dist/tui/terminal.js +39 -0
- package/dist/tui/theme.js +33 -0
- package/dist/updater.js +382 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +8 -8
- package/dist/tui.mjs +0 -5775
- package/src/agent-status.js +0 -139
- package/src/binary-plist.js +0 -146
- package/src/config.js +0 -454
- package/src/font-subset.js +0 -110
- package/src/herdr-api.js +0 -244
- package/src/herdr-command.js +0 -231
- package/src/herdr-plugin.js +0 -130
- package/src/herdr-server.js +0 -201
- package/src/host-connector.js +0 -1069
- package/src/i18n/en.js +0 -240
- package/src/i18n/index.js +0 -56
- package/src/i18n/zh.js +0 -239
- package/src/keepalive.js +0 -460
- package/src/lifecycle.js +0 -62
- package/src/net-interfaces.js +0 -76
- package/src/pasted-files.js +0 -176
- package/src/pty-session.js +0 -92
- package/src/service.js +0 -530
- package/src/settings-model.js +0 -323
- package/src/socket-discovery.js +0 -60
- package/src/state.js +0 -47
- package/src/supervisor.js +0 -250
- package/src/terminal-font.js +0 -1204
- package/src/terminal-palette.js +0 -325
- package/src/updater.js +0 -394
package/src/pasted-files.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Secure file storage and validation for pasted clipboard images.
|
|
5
|
-
*
|
|
6
|
-
* Security requirements:
|
|
7
|
-
* 1. Pinned directory: writes strictly to path.join(stateDir(), 'pasted') (directory mode 0o700).
|
|
8
|
-
* 2. Unpredictable filename: generated via crypto.randomUUID(), never using client-provided strings.
|
|
9
|
-
* 3. Whitelisted extension: derived exclusively from verified MIME type.
|
|
10
|
-
* 4. Host-side size check: re-validated to enforce <= 3 MB independently of relay checks.
|
|
11
|
-
* 5. Magic bytes sniffing: inspects raw binary headers (PNG, JPEG, GIF, WebP) to prevent file-type spoofing.
|
|
12
|
-
* 6. File mode: files are created with mode 0o600.
|
|
13
|
-
* 7. Automatic pruning: caps at 20 files and 50 MB (oldest deleted first), removes >24h stale files at startup.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const crypto = require('node:crypto');
|
|
17
|
-
const fs = require('node:fs');
|
|
18
|
-
const path = require('node:path');
|
|
19
|
-
const { stateDir } = require('./config');
|
|
20
|
-
const { ensureDir } = require('./state');
|
|
21
|
-
|
|
22
|
-
const MAX_PASTE_BYTES = 3 * 1024 * 1024; // 3 MB raw payload
|
|
23
|
-
const MAX_SAVED_FILES = 20;
|
|
24
|
-
const MAX_TOTAL_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
25
|
-
const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
26
|
-
|
|
27
|
-
const MIME_CONFIG = {
|
|
28
|
-
'image/png': {
|
|
29
|
-
ext: '.png',
|
|
30
|
-
check: (buf) =>
|
|
31
|
-
buf.length >= 8 &&
|
|
32
|
-
buf[0] === 0x89 &&
|
|
33
|
-
buf[1] === 0x50 &&
|
|
34
|
-
buf[2] === 0x4e &&
|
|
35
|
-
buf[3] === 0x47,
|
|
36
|
-
},
|
|
37
|
-
'image/jpeg': {
|
|
38
|
-
ext: '.jpg',
|
|
39
|
-
check: (buf) =>
|
|
40
|
-
buf.length >= 3 &&
|
|
41
|
-
buf[0] === 0xff &&
|
|
42
|
-
buf[1] === 0xd8 &&
|
|
43
|
-
buf[2] === 0xff,
|
|
44
|
-
},
|
|
45
|
-
'image/webp': {
|
|
46
|
-
ext: '.webp',
|
|
47
|
-
check: (buf) =>
|
|
48
|
-
buf.length >= 12 &&
|
|
49
|
-
buf[0] === 0x52 &&
|
|
50
|
-
buf[1] === 0x49 &&
|
|
51
|
-
buf[2] === 0x46 &&
|
|
52
|
-
buf[3] === 0x46 && // 'RIFF'
|
|
53
|
-
buf[8] === 0x57 &&
|
|
54
|
-
buf[9] === 0x45 &&
|
|
55
|
-
buf[10] === 0x42 &&
|
|
56
|
-
buf[11] === 0x50, // 'WEBP'
|
|
57
|
-
},
|
|
58
|
-
'image/gif': {
|
|
59
|
-
ext: '.gif',
|
|
60
|
-
check: (buf) =>
|
|
61
|
-
buf.length >= 6 &&
|
|
62
|
-
buf[0] === 0x47 &&
|
|
63
|
-
buf[1] === 0x49 &&
|
|
64
|
-
buf[2] === 0x46 &&
|
|
65
|
-
buf[3] === 0x38, // 'GIF8'
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function getPastedDir() {
|
|
70
|
-
const dir = path.join(stateDir(), 'pasted');
|
|
71
|
-
ensureDir(dir);
|
|
72
|
-
return dir;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Prunes the pasted directory:
|
|
77
|
-
* - Removes files older than maxAgeMs (default 24 hours).
|
|
78
|
-
* - Caps total file count (default 20) and total disk footprint (default 50 MB),
|
|
79
|
-
* deleting oldest files first.
|
|
80
|
-
*/
|
|
81
|
-
function cleanPastedDir({
|
|
82
|
-
dir = getPastedDir(),
|
|
83
|
-
maxFiles = MAX_SAVED_FILES,
|
|
84
|
-
maxBytes = MAX_TOTAL_BYTES,
|
|
85
|
-
maxAgeMs = MAX_AGE_MS,
|
|
86
|
-
now = Date.now(),
|
|
87
|
-
} = {}) {
|
|
88
|
-
let entries;
|
|
89
|
-
try {
|
|
90
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
91
|
-
} catch {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const fileInfos = [];
|
|
96
|
-
|
|
97
|
-
for (const entry of entries) {
|
|
98
|
-
if (!entry.isFile()) continue;
|
|
99
|
-
const fullPath = path.join(dir, entry.name);
|
|
100
|
-
try {
|
|
101
|
-
const stat = fs.statSync(fullPath);
|
|
102
|
-
if (now - stat.mtimeMs > maxAgeMs) {
|
|
103
|
-
try { fs.unlinkSync(fullPath); } catch {}
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
fileInfos.push({ path: fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
107
|
-
} catch {}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Sort oldest first
|
|
111
|
-
fileInfos.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
112
|
-
|
|
113
|
-
let totalSize = fileInfos.reduce((sum, f) => sum + f.size, 0);
|
|
114
|
-
|
|
115
|
-
while (fileInfos.length > maxFiles || totalSize > maxBytes) {
|
|
116
|
-
const oldest = fileInfos.shift();
|
|
117
|
-
if (!oldest) break;
|
|
118
|
-
try {
|
|
119
|
-
fs.unlinkSync(oldest.path);
|
|
120
|
-
totalSize -= oldest.size;
|
|
121
|
-
} catch {}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Validates base64 data, checks payload limits, sniffs magic bytes against declared MIME,
|
|
127
|
-
* writes to state storage with mode 0o600, and returns the absolute local path.
|
|
128
|
-
*/
|
|
129
|
-
function savePastedFile({ mime, dataBase64, dir = getPastedDir() }) {
|
|
130
|
-
if (typeof mime !== 'string' || !Object.hasOwn(MIME_CONFIG, mime)) {
|
|
131
|
-
throw new Error(`unsupported MIME type: ${mime}`);
|
|
132
|
-
}
|
|
133
|
-
if (typeof dataBase64 !== 'string') {
|
|
134
|
-
throw new Error('missing or invalid dataBase64 payload');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const buf = Buffer.from(dataBase64, 'base64');
|
|
138
|
-
if (buf.length > MAX_PASTE_BYTES) {
|
|
139
|
-
throw new Error(`file size (${buf.length} bytes) exceeds maximum limit of 3 MB`);
|
|
140
|
-
}
|
|
141
|
-
if (buf.length === 0) {
|
|
142
|
-
throw new Error('file payload is empty');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const config = MIME_CONFIG[mime];
|
|
146
|
-
if (!config.check(buf)) {
|
|
147
|
-
throw new Error(`file magic bytes do not match declared MIME type ${mime}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Prune before writing new file
|
|
151
|
-
cleanPastedDir({ dir });
|
|
152
|
-
|
|
153
|
-
const fileName = `${crypto.randomUUID()}${config.ext}`;
|
|
154
|
-
const filePath = path.join(dir, fileName);
|
|
155
|
-
|
|
156
|
-
fs.writeFileSync(filePath, buf, { mode: 0o600, flag: 'wx' });
|
|
157
|
-
try {
|
|
158
|
-
fs.chmodSync(filePath, 0o600);
|
|
159
|
-
} catch {}
|
|
160
|
-
|
|
161
|
-
// Prune again after writing to strictly observe count/size ceiling
|
|
162
|
-
cleanPastedDir({ dir });
|
|
163
|
-
|
|
164
|
-
return filePath;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
module.exports = {
|
|
168
|
-
MAX_PASTE_BYTES,
|
|
169
|
-
MAX_SAVED_FILES,
|
|
170
|
-
MAX_TOTAL_BYTES,
|
|
171
|
-
MAX_AGE_MS,
|
|
172
|
-
MIME_CONFIG,
|
|
173
|
-
getPastedDir,
|
|
174
|
-
cleanPastedDir,
|
|
175
|
-
savePastedFile,
|
|
176
|
-
};
|
package/src/pty-session.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const pty = require('node-pty');
|
|
5
|
-
|
|
6
|
-
class PtySession {
|
|
7
|
-
static DEFAULT_COLS = 100;
|
|
8
|
-
static DEFAULT_ROWS = 30;
|
|
9
|
-
static MIN_DIMENSION = 2;
|
|
10
|
-
static MAX_DIMENSION = 500;
|
|
11
|
-
|
|
12
|
-
constructor({ command, args = [], cwd = os.homedir(), socketPath } = {}) {
|
|
13
|
-
if (typeof command !== 'string' || command.length === 0) throw new TypeError('command must be a non-empty string');
|
|
14
|
-
if (!Array.isArray(args) || !args.every((arg) => typeof arg === 'string')) throw new TypeError('args must be strings');
|
|
15
|
-
this.command = command;
|
|
16
|
-
this.args = args;
|
|
17
|
-
this.cwd = cwd;
|
|
18
|
-
this.socketPath = socketPath;
|
|
19
|
-
this.terminal = null;
|
|
20
|
-
this.startedAt = null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static childEnv(socketPath) {
|
|
24
|
-
const env = { ...process.env };
|
|
25
|
-
for (const key of Object.keys(env)) {
|
|
26
|
-
if (key.startsWith('HERDR_')) delete env[key];
|
|
27
|
-
}
|
|
28
|
-
if (socketPath) env.HERDR_SOCKET_PATH = socketPath;
|
|
29
|
-
env.TERM = 'xterm-256color';
|
|
30
|
-
env.COLORTERM = 'truecolor';
|
|
31
|
-
return env;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
static clampDimension(value, fallback) {
|
|
35
|
-
const numeric = Number(value);
|
|
36
|
-
if (!Number.isInteger(numeric)) return fallback;
|
|
37
|
-
return Math.min(PtySession.MAX_DIMENSION, Math.max(PtySession.MIN_DIMENSION, numeric));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
start({ cols, rows, onData, onExit }) {
|
|
41
|
-
if (this.terminal) throw new Error('PTY session already started');
|
|
42
|
-
this.terminal = pty.spawn(this.command, this.args, {
|
|
43
|
-
name: 'xterm-256color',
|
|
44
|
-
cols: PtySession.clampDimension(cols, PtySession.DEFAULT_COLS),
|
|
45
|
-
rows: PtySession.clampDimension(rows, PtySession.DEFAULT_ROWS),
|
|
46
|
-
cwd: this.cwd,
|
|
47
|
-
env: PtySession.childEnv(this.socketPath),
|
|
48
|
-
});
|
|
49
|
-
this.startedAt = new Date().toISOString();
|
|
50
|
-
if (onData) this.terminal.onData(onData);
|
|
51
|
-
if (onExit) {
|
|
52
|
-
this.terminal.onExit((event) => {
|
|
53
|
-
this.terminal = null;
|
|
54
|
-
onExit(event);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return this;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
write(data) {
|
|
61
|
-
if (!this.terminal) return;
|
|
62
|
-
this.terminal.write(Buffer.isBuffer(data) ? data.toString('utf8') : String(data));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
resize(cols, rows) {
|
|
66
|
-
if (!this.terminal) return;
|
|
67
|
-
this.terminal.resize(
|
|
68
|
-
PtySession.clampDimension(cols, PtySession.DEFAULT_COLS),
|
|
69
|
-
PtySession.clampDimension(rows, PtySession.DEFAULT_ROWS),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
info() {
|
|
74
|
-
return {
|
|
75
|
-
pid: this.terminal?.pid || null,
|
|
76
|
-
command: this.command,
|
|
77
|
-
cols: this.terminal?.cols || null,
|
|
78
|
-
rows: this.terminal?.rows || null,
|
|
79
|
-
cwd: this.cwd,
|
|
80
|
-
createdAt: this.startedAt,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
kill() {
|
|
85
|
-
try {
|
|
86
|
-
this.terminal?.kill();
|
|
87
|
-
} catch {}
|
|
88
|
-
this.terminal = null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = { PtySession };
|