herdr-remote 0.2.39 → 0.3.32

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 (205) hide show
  1. package/README.md +4 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/chunks/3FQAXVGC.js +3 -0
  4. package/dist/chunks/42KARAL4.js +31 -0
  5. package/dist/chunks/44IGVPMA.js +5 -0
  6. package/dist/chunks/5LZFJ6UZ.js +3 -0
  7. package/dist/chunks/7SMZ43LI.js +9 -0
  8. package/dist/chunks/AX4EPJCP.js +4 -0
  9. package/dist/chunks/B76DFI4H.js +16 -0
  10. package/dist/chunks/BYGDPZZD.js +3 -0
  11. package/dist/chunks/C2UUZY6J.js +2 -0
  12. package/dist/chunks/CVJ4HBMP.js +3 -0
  13. package/dist/chunks/DGUZCGQN.js +3 -0
  14. package/dist/chunks/FONUO5O5.js +4 -0
  15. package/dist/chunks/H24ZP5D5.js +16 -0
  16. package/dist/chunks/I6NRCYLI.js +3 -0
  17. package/dist/chunks/JJGEV4EZ.js +3 -0
  18. package/dist/chunks/K5RFPHSF.js +5 -0
  19. package/dist/chunks/KGOQKOHH.js +5 -0
  20. package/dist/chunks/KRVF37OW.js +3 -0
  21. package/dist/chunks/KUDCC7ZO.js +6 -0
  22. package/dist/chunks/LPR662JH.js +77 -0
  23. package/dist/chunks/LY2ZGCXN.js +4 -0
  24. package/dist/chunks/M52LJEES.js +6 -0
  25. package/dist/chunks/MCUVFSSK.js +3 -0
  26. package/dist/chunks/MEFDV375.js +10 -0
  27. package/dist/chunks/MKAEXKXG.js +10 -0
  28. package/dist/chunks/O2LNVFUT.js +3 -0
  29. package/dist/chunks/PBMBFWLQ.js +3 -0
  30. package/dist/chunks/PBRQ3HEG.js +3 -0
  31. package/dist/chunks/QCSILMFB.js +3 -0
  32. package/dist/chunks/QRGNEKME.js +5 -0
  33. package/dist/chunks/RMHH3IAX.js +63 -0
  34. package/dist/chunks/TNLCNVA2.js +3 -0
  35. package/dist/chunks/U6QAYG3E.js +3 -0
  36. package/dist/chunks/VD54DF7R.js +45 -0
  37. package/dist/chunks/WY7WHEJ4.js +3 -0
  38. package/dist/cli.js +21 -294
  39. package/dist/connector/admin-broker.js +3 -303
  40. package/dist/connector/main.js +20 -18
  41. package/dist/harfbuzz-subset.wasm +0 -0
  42. package/dist/postinstall.js +17 -0
  43. package/herdr-plugin.toml +1 -1
  44. package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
  45. package/node_modules/herdr-remote-relay/package.json +3 -46
  46. package/package.json +8 -27
  47. package/dist/agent-status.js +0 -130
  48. package/dist/binary-plist.js +0 -157
  49. package/dist/config.js +0 -278
  50. package/dist/connector/admin-broker-setup.js +0 -40
  51. package/dist/connector/admin-shell.js +0 -67
  52. package/dist/connector/admin-tab.js +0 -90
  53. package/dist/connector/adminBrokerEndpoint.js +0 -68
  54. package/dist/connector/agent-watch.js +0 -123
  55. package/dist/connector/elevatedPty.js +0 -141
  56. package/dist/connector/font-server.js +0 -165
  57. package/dist/connector/herdr-prefix-keys.js +0 -28
  58. package/dist/connector/host-connector.js +0 -433
  59. package/dist/connector/host-hello.js +0 -32
  60. package/dist/connector/lock.js +0 -73
  61. package/dist/connector/sessions.js +0 -381
  62. package/dist/connector/update-report.js +0 -70
  63. package/dist/connector/wire.js +0 -42
  64. package/dist/exit-codes.js +0 -13
  65. package/dist/font-subset.js +0 -116
  66. package/dist/herdr-api.js +0 -254
  67. package/dist/herdr-command.js +0 -245
  68. package/dist/herdr-plugin.js +0 -120
  69. package/dist/herdr-server.js +0 -190
  70. package/dist/i18n/en.js +0 -257
  71. package/dist/i18n/index.js +0 -48
  72. package/dist/i18n/zh.js +0 -256
  73. package/dist/keepalive/detached.js +0 -97
  74. package/dist/keepalive/environment.js +0 -44
  75. package/dist/keepalive/index.js +0 -129
  76. package/dist/keepalive/launchd.js +0 -117
  77. package/dist/keepalive/systemd.js +0 -112
  78. package/dist/keepalive/types.js +0 -1
  79. package/dist/keepalive/windows-service-setup.js +0 -138
  80. package/dist/keepalive/windows-service.js +0 -272
  81. package/dist/keepalive/windows-task.js +0 -191
  82. package/dist/lib/process.js +0 -132
  83. package/dist/lifecycle.js +0 -79
  84. package/dist/net-interfaces.js +0 -82
  85. package/dist/node-pty-loader.js +0 -80
  86. package/dist/pasted-files.js +0 -115
  87. package/dist/paths.js +0 -74
  88. package/dist/pty-session.js +0 -133
  89. package/dist/relay-client.js +0 -106
  90. package/dist/relay-urls.js +0 -99
  91. package/dist/runtime.js +0 -107
  92. package/dist/service.js +0 -267
  93. package/dist/settings-model.js +0 -298
  94. package/dist/socket-discovery.js +0 -70
  95. package/dist/supervisor.js +0 -323
  96. package/dist/terminal-font/deps.js +0 -83
  97. package/dist/terminal-font/files.js +0 -357
  98. package/dist/terminal-font/formats.js +0 -355
  99. package/dist/terminal-font/index.js +0 -187
  100. package/dist/terminal-font/readers.js +0 -373
  101. package/dist/terminal-font/terminals.js +0 -147
  102. package/dist/terminal-palette.js +0 -399
  103. package/dist/tui/App.js +0 -199
  104. package/dist/tui/api.js +0 -91
  105. package/dist/tui/components/TextField.js +0 -50
  106. package/dist/tui/components/common.js +0 -80
  107. package/dist/tui/index.js +0 -36
  108. package/dist/tui/mouse/index.js +0 -164
  109. package/dist/tui/mouse/protocol.js +0 -96
  110. package/dist/tui/mouse/source.js +0 -79
  111. package/dist/tui/screens/About.js +0 -172
  112. package/dist/tui/screens/Herdr.js +0 -75
  113. package/dist/tui/screens/Keepalive.js +0 -141
  114. package/dist/tui/screens/Overview.js +0 -36
  115. package/dist/tui/screens/Pair.js +0 -74
  116. package/dist/tui/screens/Relay.js +0 -243
  117. package/dist/tui/screens/Services.js +0 -63
  118. package/dist/tui/screens/Wizard.js +0 -241
  119. package/dist/tui/terminal.js +0 -39
  120. package/dist/tui/theme.js +0 -33
  121. package/dist/update-command.js +0 -118
  122. package/dist/updater/in-place.js +0 -83
  123. package/dist/updater/npm.js +0 -62
  124. package/dist/updater.js +0 -401
  125. package/node_modules/herdr-remote-relay/README.md +0 -76
  126. package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
  127. package/node_modules/herdr-remote-relay/config.example.json +0 -18
  128. package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
  129. package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
  130. package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
  131. package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
  132. package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
  133. package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
  134. package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
  135. package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
  136. package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
  137. package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
  138. package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
  139. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
  140. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
  141. package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
  142. package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
  143. package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
  144. package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
  145. package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
  146. package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
  147. package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
  148. package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
  149. package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
  150. package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
  151. package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
  152. package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
  153. package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
  154. package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
  155. package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
  156. package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
  157. package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
  158. package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
  159. package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
  160. package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
  161. package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
  162. package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
  163. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
  164. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
  165. package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
  166. package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
  167. package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
  168. package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
  169. package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
  170. package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
  171. package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
  172. package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
  173. package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
  174. package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
  175. package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
  176. package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
  177. package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
  178. package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
  179. package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
  180. package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
  181. package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
  182. package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
  183. package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
  184. package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
  185. package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
  186. package/node_modules/herdr-remote-relay/dist/state.js +0 -47
  187. package/node_modules/ws/LICENSE +0 -20
  188. package/node_modules/ws/README.md +0 -548
  189. package/node_modules/ws/browser.js +0 -8
  190. package/node_modules/ws/index.js +0 -22
  191. package/node_modules/ws/lib/buffer-util.js +0 -131
  192. package/node_modules/ws/lib/constants.js +0 -19
  193. package/node_modules/ws/lib/event-target.js +0 -292
  194. package/node_modules/ws/lib/extension.js +0 -203
  195. package/node_modules/ws/lib/limiter.js +0 -55
  196. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  197. package/node_modules/ws/lib/receiver.js +0 -743
  198. package/node_modules/ws/lib/sender.js +0 -607
  199. package/node_modules/ws/lib/stream.js +0 -161
  200. package/node_modules/ws/lib/subprotocol.js +0 -62
  201. package/node_modules/ws/lib/validation.js +0 -152
  202. package/node_modules/ws/lib/websocket-server.js +0 -562
  203. package/node_modules/ws/lib/websocket.js +0 -1407
  204. package/node_modules/ws/package.json +0 -74
  205. package/node_modules/ws/wrapper.mjs +0 -21
@@ -1,433 +0,0 @@
1
- // The workstation's link to the relay: connect and reconnect, say hello, keep
2
- // the socket alive, report load while someone watches, and hand each relay
3
- // message to the part of the connector that owns it.
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { unpackStreamFrame } from 'herdr-remote-relay/protocol';
7
- import { WebSocket } from 'ws';
8
- import { loadConfig } from '../config.js';
9
- import { EXIT_AUTH_FAILED, EXIT_REPLACED } from '../exit-codes.js';
10
- import { cleanPastedDir, savePastedFile } from '../pasted-files.js';
11
- import { stateDir } from '../paths.js';
12
- import { hostWebSocketUrl, resolveHostRelayUrl } from '../relay-urls.js';
13
- import { resolveSocketPath } from '../socket-discovery.js';
14
- import { resolveHostPalette } from '../terminal-palette.js';
15
- import { randomToken } from 'herdr-remote-relay/state';
16
- import { openAdminTabFor } from './admin-tab.js';
17
- import { AgentWatch } from './agent-watch.js';
18
- import { FontServer } from './font-server.js';
19
- import { hostHello } from './host-hello.js';
20
- import { acquireHostLock, releaseHostLock } from './lock.js';
21
- import { Sessions } from './sessions.js';
22
- import { UpdateReport } from './update-report.js';
23
- import { closeSocket, sendJson, streamOf, terminateSocket } from './wire.js';
24
- /** Handshake errors that no reconnect can fix. */
25
- const AUTH_ERROR_CODES = [
26
- 'relay_password_required',
27
- 'host_auth_failed',
28
- 'invalid_host_credentials',
29
- ];
30
- export class HostConnector {
31
- config;
32
- relayUrl;
33
- hostId;
34
- hostToken;
35
- relayPassword;
36
- socketPath;
37
- /**
38
- * What the workstation's own terminal looks like. A browser has no way to
39
- * know it — the PTY carries color indices, not colors — so the host reports
40
- * it and the browser renders the session the way the workstation sees it.
41
- * Normally captured by the start path that still had a terminal and passed
42
- * down in the environment; `null` when nobody could ask.
43
- */
44
- terminalPalette;
45
- sessions;
46
- font;
47
- agents;
48
- updates;
49
- ws = null;
50
- reconnectTimer = null;
51
- heartbeatTimer = null;
52
- keepaliveTimer = null;
53
- reconnectAttempts = 0;
54
- clientCount = 0;
55
- /** The relay predates `client_count`; report load all the time, as it expects. */
56
- legacyHeartbeat = false;
57
- authFailure = false;
58
- stopping = false;
59
- lockPath;
60
- lockFd = null;
61
- onFatal;
62
- constructor(options = {}) {
63
- const config = options.config || loadConfig();
64
- this.config = config;
65
- this.relayUrl =
66
- options.relayUrl ||
67
- (process.env.RELAY_URL
68
- ? hostWebSocketUrl(process.env.RELAY_URL)
69
- : resolveHostRelayUrl(config));
70
- this.hostId = options.hostId || process.env.RELAY_HOST_ID || `host-${randomToken(9)}`;
71
- this.hostToken = options.hostToken || process.env.RELAY_HOST_TOKEN || '';
72
- // Optional; a relay without a password accepts any workstation.
73
- this.relayPassword = options.relayPassword ?? process.env.RELAY_PASSWORD ?? '';
74
- this.socketPath = options.socketPath || resolveSocketPath(config.herdr.socketPath);
75
- this.terminalPalette =
76
- options.terminalPalette !== undefined ? options.terminalPalette : resolveHostPalette();
77
- const send = (payload) => sendJson(this.ws, payload);
78
- this.sessions = new Sessions({
79
- ...options,
80
- socketPath: this.socketPath,
81
- herdrArgs: options.herdrArgs || config.herdr.args,
82
- cwd: options.cwd || config.herdr.cwd,
83
- herdrLogPath: options.herdrLogPath || path.join(stateDir(), 'herdr-server.log'),
84
- }, {
85
- send,
86
- sendFrame: (frame) => {
87
- if (this.ws?.readyState === WebSocket.OPEN)
88
- this.ws.send(frame);
89
- },
90
- changed: () => this.sendHeartbeat(),
91
- });
92
- this.font = new FontServer(options, send);
93
- this.agents = new AgentWatch({ ...options, socketPath: this.socketPath }, send);
94
- this.updates = new UpdateReport(options, send);
95
- this.lockPath =
96
- options.lockPath ||
97
- process.env.HERDR_REMOTE_HOST_LOCK ||
98
- path.join(stateDir(), 'host-connector.lock');
99
- this.onFatal = options.onFatal || ((exitCode) => process.exit(exitCode));
100
- try {
101
- cleanPastedDir();
102
- }
103
- catch {
104
- // Old pastes are tidied again next start; a connector must still come up.
105
- }
106
- }
107
- acquireLock() {
108
- if (this.lockFd !== null)
109
- return;
110
- this.lockFd = acquireHostLock(this.lockPath, this.hostId);
111
- }
112
- releaseLock() {
113
- if (this.lockFd === null)
114
- return;
115
- releaseHostLock(this.lockPath, this.lockFd);
116
- this.lockFd = null;
117
- }
118
- start() {
119
- if (!this.hostToken) {
120
- throw new Error('RELAY_HOST_TOKEN is required');
121
- }
122
- this.acquireLock();
123
- this.stopping = false;
124
- this.connect();
125
- // Off unless the user switched it on: Herdr is theirs to start.
126
- if (this.config.herdr?.autoStart) {
127
- this.sessions.startHerdr().catch((error) => {
128
- process.stderr.write(`herdr-remote host connector: could not start Herdr: ${error.message}\n`);
129
- });
130
- }
131
- }
132
- stop() {
133
- this.stopping = true;
134
- if (this.reconnectTimer)
135
- clearTimeout(this.reconnectTimer);
136
- this.reconnectTimer = null;
137
- sendJson(this.ws, { type: 'host_shutdown' });
138
- this.disconnected();
139
- closeSocket(this.ws, 'host_shutdown');
140
- this.ws = null;
141
- this.releaseLock();
142
- }
143
- /** Forget everything that lived on the relay connection. */
144
- disconnected() {
145
- if (this.heartbeatTimer)
146
- clearInterval(this.heartbeatTimer);
147
- this.heartbeatTimer = null;
148
- this.stopKeepalive();
149
- // Nothing to report to, so stop asking Herdr. A reconnect brings the
150
- // watcher back with the relay's next client count.
151
- this.agents.stop();
152
- this.clientCount = 0;
153
- this.legacyHeartbeat = false;
154
- this.sessions.stopAll();
155
- }
156
- connect() {
157
- if (this.stopping ||
158
- this.ws?.readyState === WebSocket.OPEN ||
159
- this.ws?.readyState === WebSocket.CONNECTING)
160
- return;
161
- this.stopKeepalive();
162
- let ws;
163
- try {
164
- ws = new WebSocket(this.relayUrl);
165
- }
166
- catch (error) {
167
- this.scheduleReconnect(error);
168
- return;
169
- }
170
- this.ws = ws;
171
- ws.isAlive = true;
172
- ws.on('open', () => {
173
- // Disable Nagle's algorithm to prevent small frames from stalling ~40ms when delayed ACK is active.
174
- try {
175
- ws._socket?.setNoDelay(true);
176
- }
177
- catch {
178
- // The socket may not be TCP (tests) or already gone; Nagle is only a latency tweak.
179
- }
180
- ws.isAlive = true;
181
- this.authFailure = false;
182
- sendJson(ws, hostHello({
183
- hostId: this.hostId,
184
- token: this.hostToken,
185
- password: this.relayPassword,
186
- version: this.updates.runningVersion,
187
- terminalPalette: this.terminalPalette,
188
- terminalFont: this.font.publicFont(),
189
- }));
190
- // The relay sends host_ready with the current browser count. No business
191
- // heartbeat is started until that message says somebody is watching.
192
- });
193
- ws.on('pong', () => {
194
- ws.isAlive = true;
195
- });
196
- ws.on('message', (raw, isBinary) => this.handleMessage(raw, isBinary));
197
- ws.on('close', (_code, rawReason) => {
198
- // A replacement socket may be live while an older socket is still
199
- // delivering its close event. Never let that stale event destroy the new
200
- // session or clear its heartbeat timer.
201
- if (this.ws !== ws)
202
- return;
203
- this.ws = null;
204
- this.disconnected();
205
- // Another connector has claimed this workstation. Reconnecting would just
206
- // displace it in turn, and each swap drops every attached browser, so
207
- // stand down instead of fighting for the slot.
208
- const reason = rawReason ? rawReason.toString() : '';
209
- if (reason === 'host_replaced') {
210
- process.stderr.write('herdr-remote host connector: another instance took over this workstation; exiting\n');
211
- this.stop();
212
- this.onFatal(EXIT_REPLACED);
213
- return;
214
- }
215
- if (this.authFailure) {
216
- this.stopping = true;
217
- process.stderr.write('herdr-remote host connector: authentication failed; update relay credentials and restart the service\n');
218
- this.releaseLock();
219
- this.onFatal(EXIT_AUTH_FAILED);
220
- return;
221
- }
222
- this.scheduleReconnect();
223
- });
224
- ws.on('error', (error) => {
225
- process.stderr.write(`herdr-remote host connector: ${error.message}\n`);
226
- });
227
- }
228
- scheduleReconnect(error = null) {
229
- if (this.stopping || this.reconnectTimer)
230
- return;
231
- if (error)
232
- process.stderr.write(`herdr-remote host connector: ${error.message}\n`);
233
- const delay = Math.min(30_000, 500 * 1.5 ** this.reconnectAttempts + Math.floor(Math.random() * 250));
234
- this.reconnectAttempts += 1;
235
- this.reconnectTimer = setTimeout(() => {
236
- this.reconnectTimer = null;
237
- this.connect();
238
- }, delay);
239
- }
240
- handleMessage(raw, isBinary = false) {
241
- if (isBinary) {
242
- this.handleFrame(raw);
243
- return undefined;
244
- }
245
- let message;
246
- try {
247
- message = JSON.parse(typeof raw === 'string' ? raw : raw.toString('utf8'));
248
- }
249
- catch {
250
- // Not JSON: nothing a relay speaking this protocol would send.
251
- return undefined;
252
- }
253
- switch (message.type) {
254
- case 'error':
255
- // A relay-level error (no clientId) is a rejected handshake — a wrong
256
- // relay password, or a host token the relay does not recognise. Without
257
- // this the connection just closed silently and reconnected forever,
258
- // leaving the user with an empty log and no idea what was wrong.
259
- if (message.clientId)
260
- return undefined;
261
- this.authFailure = AUTH_ERROR_CODES.includes(message.code);
262
- process.stderr.write(`herdr-remote host connector: relay rejected the connection: ${message.message || message.code}\n`);
263
- return undefined;
264
- case 'host_ready':
265
- this.hostReady(message);
266
- return undefined;
267
- case 'client_count':
268
- this.setClientCount(message.clientCount);
269
- return undefined;
270
- case 'session_start':
271
- return this.startSession(message);
272
- case 'herdr_start':
273
- return this.sessions.startHerdrFor(streamOf(message));
274
- case 'admin_tab_open':
275
- return openAdminTabFor(streamOf(message), {
276
- socketPath: this.socketPath,
277
- language: this.config.ui?.language,
278
- send: (payload) => sendJson(this.ws, payload),
279
- });
280
- case 'session_stop': {
281
- const streamId = streamOf(message);
282
- if (streamId)
283
- this.sessions.stop(streamId);
284
- return undefined;
285
- }
286
- case 'resize':
287
- this.sessions.resize(message);
288
- return undefined;
289
- case 'paste_file':
290
- this.savePaste(streamOf(message), message);
291
- return undefined;
292
- case 'host_font_chunk_request':
293
- this.font.sendChunk(streamOf(message), message);
294
- return undefined;
295
- case 'host_font_subset_request':
296
- this.font.sendSubset(streamOf(message), message);
297
- return undefined;
298
- case 'host_font_refresh':
299
- this.font.refresh();
300
- return undefined;
301
- default:
302
- return undefined;
303
- }
304
- }
305
- handleFrame(raw) {
306
- let frame;
307
- try {
308
- frame = unpackStreamFrame(raw);
309
- }
310
- catch (error) {
311
- process.stderr.write(`herdr-remote host connector: invalid relay frame: ${error.message}\n`);
312
- return;
313
- }
314
- if (frame.type !== 'input')
315
- return;
316
- this.sessions.write(frame.version === 2 ? { streamIndex: frame.streamIndex } : { streamId: frame.streamId }, frame.payload);
317
- }
318
- hostReady(message) {
319
- this.reconnectAttempts = 0;
320
- this.startKeepalive();
321
- if (Object.hasOwn(message, 'clientCount')) {
322
- this.legacyHeartbeat = false;
323
- this.setClientCount(message.clientCount);
324
- return;
325
- }
326
- // An older relay does not know client_count. Keep its historical
327
- // telemetry behavior so rolling upgrades do not silently lose status.
328
- this.legacyHeartbeat = true;
329
- this.sendHeartbeat(true);
330
- this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.config.cleanup.heartbeatIntervalMs);
331
- }
332
- startSession(message) {
333
- if (!streamOf(message))
334
- return undefined;
335
- // A window opening is when a person is looking; that is when to ask.
336
- this.updates.report();
337
- this.font.pickUp();
338
- return this.sessions.start(message);
339
- }
340
- savePaste(streamId, message) {
341
- try {
342
- const savedPath = savePastedFile({ mime: message.mime, dataBase64: message.dataBase64 });
343
- sendJson(this.ws, { type: 'paste_file_ready', clientId: streamId, path: savedPath });
344
- }
345
- catch (error) {
346
- sendJson(this.ws, {
347
- type: 'error',
348
- clientId: streamId,
349
- code: 'paste_file_write_failed',
350
- message: error.message || 'Failed to save pasted file',
351
- });
352
- }
353
- }
354
- startKeepalive() {
355
- this.stopKeepalive();
356
- if (!this.ws || this.stopping)
357
- return;
358
- this.ws.isAlive = true;
359
- const interval = this.config?.cleanup?.heartbeatIntervalMs || 30_000;
360
- this.keepaliveTimer = setInterval(() => this.tickKeepalive(), interval);
361
- }
362
- stopKeepalive() {
363
- if (this.keepaliveTimer) {
364
- clearInterval(this.keepaliveTimer);
365
- this.keepaliveTimer = null;
366
- }
367
- }
368
- tickKeepalive() {
369
- const ws = this.ws;
370
- if (!ws || this.stopping)
371
- return;
372
- if (ws.readyState === WebSocket.CLOSING) {
373
- terminateSocket(ws);
374
- return;
375
- }
376
- if (ws.readyState !== WebSocket.OPEN)
377
- return;
378
- if (!ws.isAlive) {
379
- terminateSocket(ws);
380
- return;
381
- }
382
- ws.isAlive = false;
383
- try {
384
- if (typeof ws.ping === 'function')
385
- ws.ping();
386
- }
387
- catch {
388
- // A socket failing mid-ping is caught by the next heartbeat, which terminates it.
389
- }
390
- if (this.clientCount <= 0 && !this.legacyHeartbeat) {
391
- sendJson(ws, { type: 'heartbeat', load: {}, ptys: [] });
392
- }
393
- }
394
- setClientCount(value) {
395
- const next = Number.isInteger(value) ? Math.max(0, value) : 0;
396
- if (next === this.clientCount && (next === 0 || this.heartbeatTimer))
397
- return;
398
- const wasActive = this.clientCount > 0;
399
- this.clientCount = next;
400
- if (this.heartbeatTimer)
401
- clearInterval(this.heartbeatTimer);
402
- this.heartbeatTimer = null;
403
- if (next > 0) {
404
- this.sendHeartbeat(true);
405
- this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.config.cleanup.heartbeatIntervalMs);
406
- }
407
- else if (wasActive) {
408
- this.sendHeartbeat(true);
409
- }
410
- // Nobody watching, nothing to ask Herdr about itself.
411
- if (next > 0 && !this.stopping)
412
- this.agents.start();
413
- else
414
- this.agents.stop();
415
- }
416
- sendHeartbeat(force = false) {
417
- if (!force && this.clientCount <= 0 && !this.legacyHeartbeat)
418
- return;
419
- const memory = process.memoryUsage();
420
- const load = os.loadavg();
421
- sendJson(this.ws, {
422
- type: 'heartbeat',
423
- load: {
424
- load1m: load[0] || 0,
425
- load5m: load[1] || 0,
426
- load15m: load[2] || 0,
427
- rssBytes: memory.rss,
428
- heapUsedBytes: memory.heapUsed,
429
- },
430
- ptys: this.sessions.summaries(),
431
- });
432
- }
433
- }
@@ -1,32 +0,0 @@
1
- // The first message a workstation sends the relay: who it is, what it runs,
2
- // and the facts about its terminal and Herdr that a browser cannot learn itself.
3
- import os from 'node:os';
4
- import { CAPABILITY, PROTOCOL_VERSION } from 'herdr-remote-relay/protocol';
5
- import { readHerdrPrefixKeys } from './herdr-prefix-keys.js';
6
- /** Built afresh for each connection, so a reconnect reports the config as it is now. */
7
- export function hostHello(identity) {
8
- return {
9
- type: 'host_hello',
10
- protocol: PROTOCOL_VERSION,
11
- hostId: identity.hostId,
12
- token: identity.token,
13
- password: identity.password || null,
14
- hostname: os.hostname(),
15
- platform: process.platform,
16
- ...(process.platform === 'win32' &&
17
- (process.env.MSYSTEM || /(?:^|[\\/])(?:bash|sh)(?:\.exe)?$/i.test(process.env.SHELL || ''))
18
- ? { shellProfile: 'git-bash' }
19
- : {}),
20
- arch: process.arch,
21
- version: identity.version,
22
- terminalPalette: identity.terminalPalette || null,
23
- terminalFont: identity.terminalFont,
24
- herdrPrefixKeys: readHerdrPrefixKeys(),
25
- capabilities: [
26
- CAPABILITY.hostHandoff,
27
- CAPABILITY.idleHeartbeat,
28
- CAPABILITY.binaryFrameV2,
29
- ...(process.platform === 'win32' ? [CAPABILITY.adminTabs] : []),
30
- ],
31
- };
32
- }
@@ -1,73 +0,0 @@
1
- // One host connector per workstation: a lock file naming the process that holds it.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { ensureDir } from 'herdr-remote-relay/state';
5
- import { processStart, stillRunning } from '../lib/process.js';
6
- function readOwner(lockPath) {
7
- try {
8
- return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
9
- }
10
- catch {
11
- // Missing, or half-written by a process that died mid-write: nobody owns it.
12
- return null;
13
- }
14
- }
15
- function lockError(message, code) {
16
- const error = new Error(message);
17
- error.code = code;
18
- return error;
19
- }
20
- /**
21
- * Take the lock, or throw `HOST_ALREADY_RUNNING` when a live process holds it.
22
- * A lock left by a dead process is removed and taken over — including one whose
23
- * pid a reboot handed to an unrelated process, which would otherwise keep every
24
- * later connector standing down.
25
- */
26
- export function acquireHostLock(lockPath, hostId) {
27
- ensureDir(path.dirname(lockPath));
28
- for (let attempt = 0; attempt < 2; attempt += 1) {
29
- try {
30
- const fd = fs.openSync(lockPath, 'wx', 0o600);
31
- fs.writeFileSync(fd, `${JSON.stringify({
32
- pid: process.pid,
33
- processStart: processStart(process.pid),
34
- hostId,
35
- startedAt: new Date().toISOString(),
36
- })}\n`);
37
- return fd;
38
- }
39
- catch (error) {
40
- if (error.code !== 'EEXIST')
41
- throw error;
42
- const owner = readOwner(lockPath);
43
- if (owner && stillRunning([owner]).length > 0) {
44
- throw lockError(`another host connector is already running (pid ${owner.pid})`, 'HOST_ALREADY_RUNNING');
45
- }
46
- try {
47
- fs.rmSync(lockPath, { force: true });
48
- }
49
- catch {
50
- // Not ours to remove; the second attempt fails and says so below.
51
- }
52
- }
53
- }
54
- throw lockError('could not acquire host connector lock', 'HOST_LOCK_FAILED');
55
- }
56
- /** Close `fd` and remove the lock, unless another process has taken it since. */
57
- export function releaseHostLock(lockPath, fd) {
58
- try {
59
- fs.closeSync(fd);
60
- }
61
- catch {
62
- // Already closed; the file below is what matters.
63
- }
64
- const owner = readOwner(lockPath);
65
- if (owner && owner.pid !== process.pid)
66
- return;
67
- try {
68
- fs.rmSync(lockPath, { force: true });
69
- }
70
- catch {
71
- // Stopping must not fail over a lock file; a dead owner's lock is taken over.
72
- }
73
- }