herdr-remote 0.2.17 → 0.2.19

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.
Files changed (92) hide show
  1. package/bin/herdr-remote.js +3 -221
  2. package/dist/agent-status.js +130 -0
  3. package/dist/binary-plist.js +157 -0
  4. package/dist/cli.js +255 -0
  5. package/dist/config.js +270 -0
  6. package/dist/connector/agent-watch.js +110 -0
  7. package/dist/connector/font-server.js +165 -0
  8. package/dist/connector/host-connector.js +430 -0
  9. package/dist/connector/lock.js +66 -0
  10. package/dist/connector/main.js +18 -0
  11. package/dist/connector/sessions.js +366 -0
  12. package/dist/connector/update-report.js +67 -0
  13. package/dist/connector/wire.js +42 -0
  14. package/{src → dist}/exit-codes.js +1 -4
  15. package/dist/font-subset.js +110 -0
  16. package/dist/herdr-api.js +246 -0
  17. package/dist/herdr-command.js +223 -0
  18. package/dist/herdr-plugin.js +116 -0
  19. package/dist/herdr-server.js +189 -0
  20. package/dist/i18n/en.js +223 -0
  21. package/dist/i18n/index.js +48 -0
  22. package/dist/i18n/zh.js +222 -0
  23. package/dist/keepalive/detached.js +78 -0
  24. package/dist/keepalive/environment.js +44 -0
  25. package/dist/keepalive/index.js +110 -0
  26. package/dist/keepalive/launchd.js +117 -0
  27. package/dist/keepalive/systemd.js +108 -0
  28. package/dist/keepalive/types.js +1 -0
  29. package/dist/lib/process.js +12 -0
  30. package/dist/lifecycle.js +59 -0
  31. package/dist/net-interfaces.js +73 -0
  32. package/dist/pasted-files.js +115 -0
  33. package/dist/paths.js +62 -0
  34. package/dist/pty-session.js +96 -0
  35. package/dist/relay-client.js +106 -0
  36. package/dist/relay-urls.js +99 -0
  37. package/dist/runtime.js +106 -0
  38. package/dist/service.js +260 -0
  39. package/dist/settings-model.js +298 -0
  40. package/dist/socket-discovery.js +48 -0
  41. package/dist/supervisor.js +319 -0
  42. package/dist/terminal-font/deps.js +73 -0
  43. package/dist/terminal-font/files.js +354 -0
  44. package/dist/terminal-font/formats.js +335 -0
  45. package/dist/terminal-font/index.js +187 -0
  46. package/dist/terminal-font/readers.js +316 -0
  47. package/dist/terminal-font/terminals.js +128 -0
  48. package/dist/terminal-palette.js +339 -0
  49. package/dist/tui/App.js +195 -0
  50. package/dist/tui/api.js +79 -0
  51. package/dist/tui/components/TextField.js +50 -0
  52. package/dist/tui/components/common.js +80 -0
  53. package/dist/tui/index.js +36 -0
  54. package/dist/tui/mouse/index.js +164 -0
  55. package/dist/tui/mouse/protocol.js +96 -0
  56. package/dist/tui/mouse/source.js +74 -0
  57. package/dist/tui/screens/About.js +166 -0
  58. package/dist/tui/screens/Herdr.js +74 -0
  59. package/dist/tui/screens/Keepalive.js +84 -0
  60. package/dist/tui/screens/Overview.js +36 -0
  61. package/dist/tui/screens/Pair.js +74 -0
  62. package/dist/tui/screens/Relay.js +242 -0
  63. package/dist/tui/screens/Services.js +63 -0
  64. package/dist/tui/screens/Wizard.js +235 -0
  65. package/dist/tui/terminal.js +39 -0
  66. package/dist/tui/theme.js +33 -0
  67. package/dist/updater.js +382 -0
  68. package/herdr-plugin.toml +1 -1
  69. package/package.json +10 -9
  70. package/dist/tui.mjs +0 -4613
  71. package/src/agent-status.js +0 -139
  72. package/src/config.js +0 -454
  73. package/src/herdr-api.js +0 -244
  74. package/src/herdr-command.js +0 -231
  75. package/src/herdr-plugin.js +0 -130
  76. package/src/herdr-server.js +0 -201
  77. package/src/host-connector.js +0 -927
  78. package/src/i18n/en.js +0 -240
  79. package/src/i18n/index.js +0 -56
  80. package/src/i18n/zh.js +0 -239
  81. package/src/keepalive.js +0 -460
  82. package/src/lifecycle.js +0 -62
  83. package/src/net-interfaces.js +0 -76
  84. package/src/pasted-files.js +0 -176
  85. package/src/pty-session.js +0 -92
  86. package/src/service.js +0 -516
  87. package/src/settings-model.js +0 -323
  88. package/src/socket-discovery.js +0 -60
  89. package/src/state.js +0 -47
  90. package/src/supervisor.js +0 -250
  91. package/src/terminal-palette.js +0 -325
  92. package/src/updater.js +0 -394
@@ -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
- };
@@ -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 };