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.
Files changed (95) hide show
  1. package/bin/herdr-remote.js +3 -225
  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 +8 -8
  70. package/dist/tui.mjs +0 -5775
  71. package/src/agent-status.js +0 -139
  72. package/src/binary-plist.js +0 -146
  73. package/src/config.js +0 -454
  74. package/src/font-subset.js +0 -110
  75. package/src/herdr-api.js +0 -244
  76. package/src/herdr-command.js +0 -231
  77. package/src/herdr-plugin.js +0 -130
  78. package/src/herdr-server.js +0 -201
  79. package/src/host-connector.js +0 -1069
  80. package/src/i18n/en.js +0 -240
  81. package/src/i18n/index.js +0 -56
  82. package/src/i18n/zh.js +0 -239
  83. package/src/keepalive.js +0 -460
  84. package/src/lifecycle.js +0 -62
  85. package/src/net-interfaces.js +0 -76
  86. package/src/pasted-files.js +0 -176
  87. package/src/pty-session.js +0 -92
  88. package/src/service.js +0 -530
  89. package/src/settings-model.js +0 -323
  90. package/src/socket-discovery.js +0 -60
  91. package/src/state.js +0 -47
  92. package/src/supervisor.js +0 -250
  93. package/src/terminal-font.js +0 -1204
  94. package/src/terminal-palette.js +0 -325
  95. 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 };