herdr-remote 0.2.38 → 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.
Files changed (203) hide show
  1. package/dist/chunks/353DMT35.js +3 -0
  2. package/dist/chunks/42KARAL4.js +31 -0
  3. package/dist/chunks/7AUQHB3O.js +10 -0
  4. package/dist/chunks/7SMZ43LI.js +9 -0
  5. package/dist/chunks/AM3OEMFK.js +16 -0
  6. package/dist/chunks/ANNJESOL.js +10 -0
  7. package/dist/chunks/AWB4A65P.js +63 -0
  8. package/dist/chunks/C2UUZY6J.js +2 -0
  9. package/dist/chunks/DGUZCGQN.js +3 -0
  10. package/dist/chunks/H24ZP5D5.js +16 -0
  11. package/dist/chunks/I6NRCYLI.js +3 -0
  12. package/dist/chunks/JJGEV4EZ.js +3 -0
  13. package/dist/chunks/KRSURB5L.js +5 -0
  14. package/dist/chunks/KRVF37OW.js +3 -0
  15. package/dist/chunks/KUDCC7ZO.js +6 -0
  16. package/dist/chunks/M3NRPJK2.js +77 -0
  17. package/dist/chunks/M4AFZHKH.js +3 -0
  18. package/dist/chunks/M52LJEES.js +6 -0
  19. package/dist/chunks/O2LNVFUT.js +3 -0
  20. package/dist/chunks/OJPBI7KB.js +3 -0
  21. package/dist/chunks/OMN6C7J6.js +3 -0
  22. package/dist/chunks/PBMBFWLQ.js +3 -0
  23. package/dist/chunks/PBRQ3HEG.js +3 -0
  24. package/dist/chunks/PFFVGFPL.js +45 -0
  25. package/dist/chunks/QRGNEKME.js +5 -0
  26. package/dist/chunks/TNLCNVA2.js +3 -0
  27. package/dist/chunks/U27JJKJ6.js +3 -0
  28. package/dist/chunks/U6QAYG3E.js +3 -0
  29. package/dist/chunks/UEOQE77N.js +4 -0
  30. package/dist/chunks/WSACSE45.js +4 -0
  31. package/dist/chunks/WY7WHEJ4.js +3 -0
  32. package/dist/chunks/XJKWLK43.js +4 -0
  33. package/dist/chunks/XN6D5S55.js +3 -0
  34. package/dist/chunks/YD3MIP7B.js +5 -0
  35. package/dist/chunks/YXDQ62BB.js +4 -0
  36. package/dist/cli.js +21 -294
  37. package/dist/connector/admin-broker.js +3 -302
  38. package/dist/connector/main.js +20 -18
  39. package/dist/harfbuzz-subset.wasm +0 -0
  40. package/dist/postinstall.js +17 -0
  41. package/herdr-plugin.toml +1 -1
  42. package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
  43. package/node_modules/herdr-remote-relay/package.json +3 -46
  44. package/package.json +8 -27
  45. package/dist/agent-status.js +0 -130
  46. package/dist/binary-plist.js +0 -157
  47. package/dist/config.js +0 -278
  48. package/dist/connector/admin-broker-setup.js +0 -40
  49. package/dist/connector/admin-shell.js +0 -67
  50. package/dist/connector/admin-tab.js +0 -90
  51. package/dist/connector/adminBrokerEndpoint.js +0 -68
  52. package/dist/connector/agent-watch.js +0 -123
  53. package/dist/connector/elevatedPty.js +0 -141
  54. package/dist/connector/font-server.js +0 -165
  55. package/dist/connector/herdr-prefix-keys.js +0 -28
  56. package/dist/connector/host-connector.js +0 -433
  57. package/dist/connector/host-hello.js +0 -32
  58. package/dist/connector/lock.js +0 -73
  59. package/dist/connector/sessions.js +0 -381
  60. package/dist/connector/update-report.js +0 -70
  61. package/dist/connector/wire.js +0 -42
  62. package/dist/exit-codes.js +0 -13
  63. package/dist/font-subset.js +0 -110
  64. package/dist/herdr-api.js +0 -254
  65. package/dist/herdr-command.js +0 -245
  66. package/dist/herdr-plugin.js +0 -120
  67. package/dist/herdr-server.js +0 -190
  68. package/dist/i18n/en.js +0 -257
  69. package/dist/i18n/index.js +0 -48
  70. package/dist/i18n/zh.js +0 -256
  71. package/dist/keepalive/detached.js +0 -97
  72. package/dist/keepalive/environment.js +0 -44
  73. package/dist/keepalive/index.js +0 -129
  74. package/dist/keepalive/launchd.js +0 -117
  75. package/dist/keepalive/systemd.js +0 -112
  76. package/dist/keepalive/types.js +0 -1
  77. package/dist/keepalive/windows-service-setup.js +0 -138
  78. package/dist/keepalive/windows-service.js +0 -272
  79. package/dist/keepalive/windows-task.js +0 -191
  80. package/dist/lib/process.js +0 -132
  81. package/dist/lifecycle.js +0 -79
  82. package/dist/net-interfaces.js +0 -82
  83. package/dist/node-pty-loader.js +0 -80
  84. package/dist/pasted-files.js +0 -115
  85. package/dist/paths.js +0 -74
  86. package/dist/pty-session.js +0 -133
  87. package/dist/relay-client.js +0 -106
  88. package/dist/relay-urls.js +0 -99
  89. package/dist/runtime.js +0 -107
  90. package/dist/service.js +0 -267
  91. package/dist/settings-model.js +0 -298
  92. package/dist/socket-discovery.js +0 -70
  93. package/dist/supervisor.js +0 -323
  94. package/dist/terminal-font/deps.js +0 -83
  95. package/dist/terminal-font/files.js +0 -357
  96. package/dist/terminal-font/formats.js +0 -355
  97. package/dist/terminal-font/index.js +0 -187
  98. package/dist/terminal-font/readers.js +0 -373
  99. package/dist/terminal-font/terminals.js +0 -147
  100. package/dist/terminal-palette.js +0 -399
  101. package/dist/tui/App.js +0 -199
  102. package/dist/tui/api.js +0 -91
  103. package/dist/tui/components/TextField.js +0 -50
  104. package/dist/tui/components/common.js +0 -80
  105. package/dist/tui/index.js +0 -36
  106. package/dist/tui/mouse/index.js +0 -164
  107. package/dist/tui/mouse/protocol.js +0 -96
  108. package/dist/tui/mouse/source.js +0 -79
  109. package/dist/tui/screens/About.js +0 -172
  110. package/dist/tui/screens/Herdr.js +0 -75
  111. package/dist/tui/screens/Keepalive.js +0 -141
  112. package/dist/tui/screens/Overview.js +0 -36
  113. package/dist/tui/screens/Pair.js +0 -74
  114. package/dist/tui/screens/Relay.js +0 -243
  115. package/dist/tui/screens/Services.js +0 -63
  116. package/dist/tui/screens/Wizard.js +0 -241
  117. package/dist/tui/terminal.js +0 -39
  118. package/dist/tui/theme.js +0 -33
  119. package/dist/update-command.js +0 -118
  120. package/dist/updater/in-place.js +0 -83
  121. package/dist/updater/npm.js +0 -62
  122. package/dist/updater.js +0 -401
  123. package/node_modules/herdr-remote-relay/README.md +0 -76
  124. package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
  125. package/node_modules/herdr-remote-relay/config.example.json +0 -18
  126. package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
  127. package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
  128. package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
  129. package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
  130. package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
  131. package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
  132. package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
  133. package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
  134. package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
  135. package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
  136. package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
  137. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
  138. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
  139. package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
  140. package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
  141. package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
  142. package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
  143. package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
  144. package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
  145. package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
  146. package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
  147. package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
  148. package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
  149. package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
  150. package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
  151. package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
  152. package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
  153. package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
  154. package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
  155. package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
  156. package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
  157. package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
  158. package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
  159. package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
  160. package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
  161. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
  162. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
  163. package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
  164. package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
  165. package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
  166. package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
  167. package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
  168. package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
  169. package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
  170. package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
  171. package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
  172. package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
  173. package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
  174. package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
  175. package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
  176. package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
  177. package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
  178. package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
  179. package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
  180. package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
  181. package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
  182. package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
  183. package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
  184. package/node_modules/herdr-remote-relay/dist/state.js +0 -47
  185. package/node_modules/ws/LICENSE +0 -20
  186. package/node_modules/ws/README.md +0 -548
  187. package/node_modules/ws/browser.js +0 -8
  188. package/node_modules/ws/index.js +0 -22
  189. package/node_modules/ws/lib/buffer-util.js +0 -131
  190. package/node_modules/ws/lib/constants.js +0 -19
  191. package/node_modules/ws/lib/event-target.js +0 -292
  192. package/node_modules/ws/lib/extension.js +0 -203
  193. package/node_modules/ws/lib/limiter.js +0 -55
  194. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  195. package/node_modules/ws/lib/receiver.js +0 -743
  196. package/node_modules/ws/lib/sender.js +0 -607
  197. package/node_modules/ws/lib/stream.js +0 -161
  198. package/node_modules/ws/lib/subprotocol.js +0 -62
  199. package/node_modules/ws/lib/validation.js +0 -152
  200. package/node_modules/ws/lib/websocket-server.js +0 -562
  201. package/node_modules/ws/lib/websocket.js +0 -1407
  202. package/node_modules/ws/package.json +0 -74
  203. package/node_modules/ws/wrapper.mjs +0 -21
@@ -1,67 +0,0 @@
1
- // `herdr-remote admin-shell`: the administrator terminal inside a Herdr tab.
2
- //
3
- // This process runs in the tab's pane with the pane's standard rights and
4
- // relays its console to an elevated PowerShell that the admin broker runs. On
5
- // Windows, Node's raw mode reads keys as VT input and writes VT output through
6
- // unchanged, so the relay is byte for byte.
7
- import { requestHerdr } from '../herdr-api.js';
8
- import { ADMIN_TAB_ENV } from './admin-tab.js';
9
- import { ElevatedPty } from './elevatedPty.js';
10
- const BROKER_UNAVAILABLE_CODES = ['ENOENT', 'ECONNREFUSED', 'ETIMEDOUT'];
11
- /** Close the pane this runs in, which closes the tab `openAdminTab` made for it. */
12
- async function closeOwnPane() {
13
- const socketPath = process.env.HERDR_SOCKET_PATH;
14
- const paneId = process.env.HERDR_PANE_ID;
15
- if (process.env[ADMIN_TAB_ENV] !== '1' || !socketPath || !paneId)
16
- return;
17
- try {
18
- await requestHerdr(socketPath, 'pane.close', { pane_id: paneId });
19
- }
20
- catch {
21
- // The shell's prompt comes back instead; the user can close the tab.
22
- }
23
- }
24
- export async function runAdminShell(t, { stdin = process.stdin, stdout = process.stdout, platform = process.platform, } = {}) {
25
- if (platform !== 'win32')
26
- throw new Error(t('adminBroker.windowsOnly'));
27
- if (!stdin.isTTY || !stdout.isTTY)
28
- throw new Error(t('adminShell.needsTerminal'));
29
- const pty = new ElevatedPty({
30
- command: 'powershell.exe',
31
- args: ['-NoLogo', '-NoProfile'],
32
- cwd: process.cwd(),
33
- });
34
- const relayInput = (data) => pty.write(data);
35
- const relayResize = () => pty.resize(stdout.columns, stdout.rows);
36
- stdin.setRawMode(true);
37
- stdin.on('data', relayInput);
38
- // The CLI's Windows color probe pauses stdin before any command runs, and a
39
- // paused stream ignores a new 'data' listener: no key would reach PowerShell.
40
- stdin.resume();
41
- stdout.on('resize', relayResize);
42
- try {
43
- const exitCode = await new Promise((resolve, reject) => {
44
- pty.start({
45
- cols: stdout.columns,
46
- rows: stdout.rows,
47
- onData: (data) => stdout.write(data),
48
- onExit: (event) => resolve(event.exitCode),
49
- onError: reject,
50
- });
51
- });
52
- await closeOwnPane();
53
- return exitCode;
54
- }
55
- catch (error) {
56
- const { code } = error;
57
- if (code && BROKER_UNAVAILABLE_CODES.includes(code))
58
- throw new Error(t('adminShell.brokerUnavailable'));
59
- throw error;
60
- }
61
- finally {
62
- stdin.off('data', relayInput);
63
- stdout.off('resize', relayResize);
64
- stdin.setRawMode(false);
65
- stdin.pause();
66
- }
67
- }
@@ -1,90 +0,0 @@
1
- // An administrator terminal as a tab of the workstation's own Herdr, opened at
2
- // a browser's request.
3
- //
4
- // Herdr's API cannot start a tab running a command of our choosing, so the tab
5
- // starts with the user's shell and `herdr-remote admin-shell` is typed into it
6
- // once that shell is running. Nor can the API focus a tab for one client:
7
- // `tab.focus` moves every client attached to this Herdr, which is the price of
8
- // the requesting window showing the new tab at once.
9
- import path from 'node:path';
10
- import { requestHerdr } from '../herdr-api.js';
11
- import { createTranslator, detectLocale } from '../i18n/index.js';
12
- import { cliEntryPoint } from '../keepalive/environment.js';
13
- import { adminBrokerStatus } from './admin-broker.js';
14
- const SHELL_START_TIMEOUT_MS = 5_000;
15
- const SHELL_START_POLL_MS = 100;
16
- /** Marks the tab's shell, so `admin-shell` knows it may close the pane when done. */
17
- export const ADMIN_TAB_ENV = 'HERDR_REMOTE_ADMIN_TAB';
18
- function powershellQuote(value) {
19
- return `'${value.replaceAll("'", "''")}'`;
20
- }
21
- function posixQuote(value) {
22
- return `'${value.replaceAll("'", "'\\''")}'`;
23
- }
24
- /** The command line that starts `admin-shell`, in the syntax of the tab's shell. */
25
- export function adminShellCommand(shell, node, entry) {
26
- const name = path.win32
27
- .basename(shell)
28
- .toLowerCase()
29
- .replace(/\.exe$/, '');
30
- if (name === 'powershell' || name === 'pwsh') {
31
- return `& ${powershellQuote(node)} ${powershellQuote(entry)} admin-shell`;
32
- }
33
- if (name === 'cmd')
34
- return `"${node}" "${entry}" admin-shell`;
35
- // Git Bash and other POSIX shells: forward slashes, or C:\ reads as escapes.
36
- const posix = (file) => posixQuote(file.replaceAll('\\', '/'));
37
- return `${posix(node)} ${posix(entry)} admin-shell`;
38
- }
39
- /** The new tab's shell, once Herdr reports it running. */
40
- async function waitForShell(request, socketPath, paneId) {
41
- const deadline = Date.now() + SHELL_START_TIMEOUT_MS;
42
- while (true) {
43
- const info = await request(socketPath, 'pane.process_info', {
44
- pane_id: paneId,
45
- });
46
- const shell = info.process_info?.foreground_processes?.[0]?.name;
47
- if (shell)
48
- return shell;
49
- if (Date.now() >= deadline)
50
- throw new Error('The new tab did not start a shell in time.');
51
- await new Promise((resolve) => setTimeout(resolve, SHELL_START_POLL_MS));
52
- }
53
- }
54
- export async function openAdminTab({ socketPath, label, request = requestHerdr, brokerReady = adminBrokerStatus, }) {
55
- if (!(await brokerReady())) {
56
- throw Object.assign(new Error('The administrator terminal broker is not running.'), {
57
- code: 'admin_broker_unavailable',
58
- });
59
- }
60
- const created = await request(socketPath, 'tab.create', {
61
- label,
62
- focus: false,
63
- env: { [ADMIN_TAB_ENV]: '1' },
64
- });
65
- const paneId = created.root_pane.pane_id;
66
- await request(socketPath, 'tab.focus', { tab_id: created.tab.tab_id });
67
- const shell = await waitForShell(request, socketPath, paneId);
68
- await request(socketPath, 'pane.send_input', {
69
- pane_id: paneId,
70
- text: adminShellCommand(shell, process.execPath, cliEntryPoint()),
71
- keys: ['enter'],
72
- });
73
- }
74
- /** Open the tab for a browser window, and tell that window when it could not. */
75
- export async function openAdminTabFor(streamId, options) {
76
- const t = createTranslator(detectLocale({ preference: options.language }));
77
- try {
78
- await openAdminTab({ socketPath: options.socketPath, label: t('adminShell.tabLabel') });
79
- }
80
- catch (error) {
81
- const { code, message } = error;
82
- process.stderr.write(`herdr-remote host connector: could not open an admin tab: ${message}\n`);
83
- options.send({
84
- type: 'error',
85
- clientId: streamId,
86
- code: code === 'admin_broker_unavailable' ? code : 'admin_tab_failed',
87
- message,
88
- });
89
- }
90
- }
@@ -1,68 +0,0 @@
1
- import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
- import fs from 'node:fs';
3
- import net from 'node:net';
4
- import path from 'node:path';
5
- import { stateDir } from '../paths.js';
6
- const ADMIN_BROKER_TASK_NAME = 'HerdrRemoteAdminBroker';
7
- const ADMIN_BROKER_HOST = '127.0.0.1';
8
- export const ADMIN_BROKER_MAX_MESSAGE_BYTES = 2 * 1_024 * 1_024;
9
- function adminBrokerAccountId() {
10
- const account = `${process.env.USERDOMAIN || ''}\\${process.env.USERNAME || process.env.USERPROFILE || 'default'}`;
11
- return createHash('sha256').update(account.toLowerCase()).digest('hex').slice(0, 24);
12
- }
13
- export function adminBrokerTaskName() {
14
- return `${ADMIN_BROKER_TASK_NAME}-${adminBrokerAccountId()}`;
15
- }
16
- // Not a named pipe: one created by an elevated process admits only
17
- // Administrators, so the connector's standard token gets EPERM, and opening it
18
- // to everyone would let any account add an instance of the same name and sit
19
- // in the middle. A loopback port cannot be taken over by another account, and
20
- // the token lives in the state directory, which only this account can read.
21
- function endpointPath() {
22
- return path.join(stateDir(), 'admin-broker.json');
23
- }
24
- /** Record where the broker listens, with a fresh token callers must present. */
25
- export function publishAdminBrokerEndpoint(port) {
26
- const token = randomBytes(32).toString('hex');
27
- const file = endpointPath();
28
- fs.mkdirSync(path.dirname(file), { recursive: true });
29
- const temporary = `${file}.${process.pid}.tmp`;
30
- fs.writeFileSync(temporary, JSON.stringify({ port, token }), { mode: 0o600 });
31
- fs.renameSync(temporary, file);
32
- return token;
33
- }
34
- export function adminBrokerTokenMatches(expected, received) {
35
- if (!expected || typeof received !== 'string')
36
- return false;
37
- const want = Buffer.from(expected);
38
- const got = Buffer.from(received);
39
- return want.length === got.length && timingSafeEqual(want, got);
40
- }
41
- function brokerUnavailable() {
42
- return Object.assign(new Error('The administrator terminal broker is not running.'), {
43
- code: 'ENOENT',
44
- });
45
- }
46
- /** Connect to the running broker; the token goes in the first message. */
47
- export function connectAdminBroker() {
48
- let endpoint;
49
- try {
50
- endpoint = JSON.parse(fs.readFileSync(endpointPath(), 'utf8'));
51
- }
52
- catch {
53
- // Missing or half-written: either way no broker has published itself.
54
- throw brokerUnavailable();
55
- }
56
- const { port, token } = endpoint;
57
- if (!Number.isInteger(port) || typeof token !== 'string')
58
- throw brokerUnavailable();
59
- return { socket: net.createConnection({ host: ADMIN_BROKER_HOST, port: port }), token };
60
- }
61
- export function listenAdminBroker(server, onListening) {
62
- server.listen(0, ADMIN_BROKER_HOST, () => {
63
- onListening(server.address().port);
64
- });
65
- }
66
- export function sendAdminBrokerMessage(socket, message) {
67
- socket.write(`${JSON.stringify(message)}\n`);
68
- }
@@ -1,123 +0,0 @@
1
- // Watching what the agents on this workstation are doing, for the status bar
2
- // of every window attached to it.
3
- import { sameSummary, summarizeAgents, } from '../agent-status.js';
4
- import { requestHerdr, subscribeHerdr, } from '../herdr-api.js';
5
- /** How often the agent summary is re-read while a browser is watching. */
6
- const AGENT_STATUS_POLL_MS = 5_000;
7
- const AGENT_STATUS_EVENT_DEBOUNCE_MS = 150;
8
- const AGENT_STATUS_SUBSCRIPTIONS = [
9
- { type: 'pane.focused' },
10
- { type: 'tab.focused' },
11
- { type: 'workspace.focused' },
12
- { type: 'pane.agent_detected' },
13
- ];
14
- /**
15
- * A side channel, on its own socket, alive only while a browser is attached.
16
- * Herdr's server can be restarted underneath a running herdr-remote — the
17
- * PTYs survive it — so every failure here is a retry, never anything a
18
- * terminal session notices.
19
- *
20
- * A long-lived subscription provides fast refreshes on pane and workspace
21
- * focus changes; the five-second poll still repairs missed events and a
22
- * restarted Herdr server. Both paths only read the side-channel snapshot, and
23
- * events only invalidate it: Herdr's snapshots and events share no sequence,
24
- * so each (re)started subscription and each event mid-read asks for a fresh
25
- * read rather than patching the last one.
26
- */
27
- export class AgentWatch {
28
- socketPath;
29
- request;
30
- subscribe;
31
- send;
32
- pollTimer = null;
33
- debounceTimer = null;
34
- subscription = null;
35
- pending = false;
36
- stale = false;
37
- lastSummary = null;
38
- constructor(options, send) {
39
- this.socketPath = options.socketPath;
40
- this.request = options.requestHerdr || requestHerdr;
41
- this.subscribe = options.subscribeHerdr || subscribeHerdr;
42
- this.send = send;
43
- }
44
- start() {
45
- if (this.pollTimer)
46
- return;
47
- this.pollTimer = setInterval(() => this.read(), AGENT_STATUS_POLL_MS);
48
- this.pollTimer.unref?.();
49
- try {
50
- this.subscription = this.subscribe(this.socketPath, AGENT_STATUS_SUBSCRIPTIONS, () => this.scheduleRead(), { onStarted: () => this.scheduleRead() });
51
- }
52
- catch {
53
- // A missing or restarting Herdr server leaves the regular poll in place.
54
- }
55
- this.read();
56
- }
57
- stop() {
58
- if (this.pollTimer) {
59
- clearInterval(this.pollTimer);
60
- this.pollTimer = null;
61
- }
62
- if (this.debounceTimer) {
63
- clearTimeout(this.debounceTimer);
64
- this.debounceTimer = null;
65
- }
66
- if (this.subscription) {
67
- this.subscription.close();
68
- this.subscription = null;
69
- }
70
- this.stale = false;
71
- this.lastSummary = null;
72
- }
73
- scheduleRead() {
74
- if (!this.pollTimer)
75
- return;
76
- if (this.debounceTimer)
77
- clearTimeout(this.debounceTimer);
78
- this.debounceTimer = setTimeout(() => {
79
- this.debounceTimer = null;
80
- this.read();
81
- }, AGENT_STATUS_EVENT_DEBOUNCE_MS);
82
- this.debounceTimer.unref?.();
83
- }
84
- /**
85
- * Read the summary and forward it if it changed.
86
- *
87
- * A slow answer must not stack up behind the next tick, and a failed one
88
- * leaves the last summary standing rather than blanking the browser's chip:
89
- * Herdr's server not running is an ordinary state, not news.
90
- */
91
- async read() {
92
- if (!this.pollTimer)
93
- return;
94
- if (this.pending) {
95
- this.stale = true;
96
- return;
97
- }
98
- this.pending = true;
99
- this.stale = false;
100
- let snapshot;
101
- try {
102
- const result = await this.request(this.socketPath, 'session.snapshot', {});
103
- snapshot = result?.snapshot || result;
104
- }
105
- catch {
106
- return;
107
- }
108
- finally {
109
- this.pending = false;
110
- // Something changed while this answer was in flight; it may predate it.
111
- if (this.stale)
112
- this.scheduleRead();
113
- }
114
- // Stopped while the answer was in flight; the browser it was for is gone.
115
- if (!this.pollTimer)
116
- return;
117
- const summary = summarizeAgents(snapshot);
118
- if (sameSummary(this.lastSummary, summary))
119
- return;
120
- this.lastSummary = summary;
121
- this.send({ type: 'agent_status', ...summary });
122
- }
123
- }
@@ -1,141 +0,0 @@
1
- import { ADMIN_BROKER_MAX_MESSAGE_BYTES, connectAdminBroker, sendAdminBrokerMessage, } from './adminBrokerEndpoint.js';
2
- const BROKER_CONNECT_TIMEOUT_MS = 5_000;
3
- /** A PTY the admin broker runs elevated, driven over its loopback socket. */
4
- export class ElevatedPty {
5
- socket = null;
6
- input = '';
7
- connected = false;
8
- started = false;
9
- finished = false;
10
- killed = false;
11
- /** Keystrokes typed while the elevated shell was still starting. */
12
- early = [];
13
- dataListener = null;
14
- exitListener = null;
15
- errorListener = null;
16
- options;
17
- constructor(options) {
18
- this.options = options;
19
- }
20
- start({ cols, rows, onData, onExit, onError }) {
21
- this.dataListener = onData;
22
- this.exitListener = onExit;
23
- this.errorListener = onError;
24
- let broker;
25
- try {
26
- broker = connectAdminBroker();
27
- }
28
- catch (error) {
29
- this.fail(error);
30
- return;
31
- }
32
- const { socket, token } = broker;
33
- this.socket = socket;
34
- socket.setEncoding('utf8');
35
- socket.setTimeout(BROKER_CONNECT_TIMEOUT_MS, () => {
36
- socket.destroy(Object.assign(new Error('Administrator terminal broker connection timed out'), {
37
- code: 'ETIMEDOUT',
38
- }));
39
- });
40
- socket.on('connect', () => {
41
- this.connected = true;
42
- socket.setTimeout(0);
43
- sendAdminBrokerMessage(socket, { type: 'start', token, ...this.options, cols, rows });
44
- });
45
- socket.on('data', (chunk) => this.receive(chunk));
46
- socket.on('error', (error) => this.fail(error));
47
- socket.on('close', () => {
48
- if (this.started && !this.finished && !this.killed) {
49
- this.finished = true;
50
- this.exitListener?.({ exitCode: 1 });
51
- }
52
- });
53
- }
54
- write(data) {
55
- if (this.finished)
56
- return;
57
- if (!this.started) {
58
- this.early.push(Buffer.from(data));
59
- return;
60
- }
61
- if (this.socket?.writable) {
62
- sendAdminBrokerMessage(this.socket, {
63
- type: 'input',
64
- dataBase64: Buffer.from(data).toString('base64'),
65
- });
66
- }
67
- }
68
- resize(cols, rows) {
69
- if (this.started && this.socket?.writable) {
70
- sendAdminBrokerMessage(this.socket, { type: 'resize', cols, rows });
71
- }
72
- }
73
- kill() {
74
- this.killed = true;
75
- if (!this.socket)
76
- return;
77
- if (this.connected && this.socket.writable) {
78
- sendAdminBrokerMessage(this.socket, { type: 'kill' });
79
- this.socket.end();
80
- }
81
- else {
82
- this.socket.destroy();
83
- }
84
- }
85
- receive(chunk) {
86
- this.input += chunk;
87
- if (Buffer.byteLength(this.input, 'utf8') > ADMIN_BROKER_MAX_MESSAGE_BYTES) {
88
- this.socket?.destroy(new Error('Administrator terminal broker message exceeded the limit'));
89
- return;
90
- }
91
- while (true) {
92
- const newline = this.input.indexOf('\n');
93
- if (newline < 0)
94
- return;
95
- const line = this.input.slice(0, newline);
96
- this.input = this.input.slice(newline + 1);
97
- let message;
98
- try {
99
- message = JSON.parse(line);
100
- }
101
- catch {
102
- this.socket?.destroy(new Error('Administrator terminal broker sent invalid JSON'));
103
- return;
104
- }
105
- if (message.type === 'started') {
106
- this.started = true;
107
- const early = Buffer.concat(this.early);
108
- this.early = [];
109
- if (early.length > 0)
110
- this.write(early);
111
- }
112
- else if (message.type === 'data' && typeof message.dataBase64 === 'string') {
113
- this.dataListener?.(Buffer.from(message.dataBase64, 'base64').toString('utf8'));
114
- }
115
- else if (message.type === 'exit') {
116
- this.finished = true;
117
- this.exitListener?.({
118
- exitCode: Number(message.exitCode) || 0,
119
- ...(typeof message.signal === 'number' ? { signal: message.signal } : {}),
120
- });
121
- this.socket?.end();
122
- }
123
- else if (message.type === 'error') {
124
- this.fail(new Error(typeof message.message === 'string' ? message.message : 'Admin PTY failed'));
125
- }
126
- }
127
- }
128
- fail(error) {
129
- if (this.started) {
130
- if (!this.finished && !this.killed) {
131
- this.finished = true;
132
- this.exitListener?.({ exitCode: 1 });
133
- }
134
- return;
135
- }
136
- if (this.finished || this.killed)
137
- return;
138
- this.finished = true;
139
- this.errorListener?.(error);
140
- }
141
- }
@@ -1,165 +0,0 @@
1
- // Serving the workstation's terminal font to browser windows: its public
2
- // description, its files a chunk at a time, and characters cut out of large
3
- // (CJK) fonts on demand.
4
- import crypto from 'node:crypto';
5
- import fs from 'node:fs';
6
- import { TERMINAL_FONT_CHUNK_BYTES } from 'herdr-remote-relay/protocol';
7
- import { FontSubsetter } from '../font-subset.js';
8
- import { loadHostTerminalFont, publicTerminalFont, readFontChunk, } from '../terminal-font/index.js';
9
- /** Cut fonts kept for the slices a browser has yet to fetch, by total size. */
10
- const FONT_SUBSET_CACHE_BYTES = 48 * 1024 * 1024;
11
- const FONT_CHANGED = 'The terminal font changed or is no longer on this workstation';
12
- /**
13
- * Only the family, size and file hashes leave this process; a browser that
14
- * lacks the font fetches the files by hash, one chunk at a time.
15
- */
16
- export class FontServer {
17
- font;
18
- load;
19
- subsetter;
20
- /** Subsets too large for one message, by hash, until fetched. */
21
- subsets = new Map();
22
- send;
23
- constructor(options, send) {
24
- this.load = options.loadTerminalFont || loadHostTerminalFont;
25
- this.font = options.terminalFont !== undefined ? options.terminalFont : this.load();
26
- this.subsetter = options.fontSubsetter || new FontSubsetter();
27
- this.send = send;
28
- }
29
- /** What may leave this machine: the record without its file paths. */
30
- publicFont() {
31
- return publicTerminalFont(this.font);
32
- }
33
- /**
34
- * A service started at boot runs before anything has seen a terminal, so it
35
- * starts with no font. The first `herdr-remote start` from a terminal (or
36
- * from Herdr's plugin hook) writes one down; the next window to open picks
37
- * it up here instead of waiting for a restart.
38
- */
39
- pickUp() {
40
- if (this.font)
41
- return;
42
- let next = null;
43
- try {
44
- next = this.load();
45
- }
46
- catch {
47
- // Unreadable now; the next window to open asks again.
48
- return;
49
- }
50
- if (!next)
51
- return;
52
- this.font = next;
53
- this.send({ type: 'terminal_font', terminalFont: publicTerminalFont(next) });
54
- }
55
- /**
56
- * Read the terminal's font settings again, at a browser's request. Every
57
- * window of this workstation hears the answer, since they all draw with it.
58
- */
59
- refresh() {
60
- try {
61
- const next = this.load({ refresh: true });
62
- if (next)
63
- this.font = next;
64
- }
65
- catch (error) {
66
- process.stderr.write(`herdr-remote host connector: could not read the terminal font: ${error.message}\n`);
67
- }
68
- this.send({ type: 'terminal_font', terminalFont: this.publicFont() });
69
- }
70
- /** One slice of a terminal font file, for the window that asked for it. */
71
- sendChunk(streamId, request) {
72
- const chunk = this.subsetChunk(request.sha256, request.index) ||
73
- readFontChunk(this.font, request.sha256, request.index);
74
- if (!chunk) {
75
- this.send({
76
- type: 'error',
77
- clientId: streamId,
78
- code: 'host_font_unavailable',
79
- message: FONT_CHANGED,
80
- });
81
- return;
82
- }
83
- this.send({
84
- type: 'host_font_chunk',
85
- clientId: streamId,
86
- sha256: request.sha256,
87
- index: request.index,
88
- total: chunk.total,
89
- dataBase64: chunk.data.toString('base64'),
90
- });
91
- }
92
- /**
93
- * The characters a window is about to draw, cut out of a large font (CJK).
94
- * A small result travels in the answer itself; a large one (the common
95
- * characters fetched up front) is held here and pulled in slices.
96
- */
97
- sendSubset(streamId, request) {
98
- const fail = (code, text) => this.send({ type: 'error', clientId: streamId, code, message: text });
99
- const source = this.font?.subsets?.find((candidate) => candidate.sha256 === request.sha256);
100
- if (!source || !this.unchanged(source)) {
101
- fail('host_font_unavailable', FONT_CHANGED);
102
- return;
103
- }
104
- const codepoints = [
105
- ...new Set(Array.from(String(request.text || ''), (char) => char.codePointAt(0))),
106
- ];
107
- let data;
108
- try {
109
- data = this.subsetter.subset(source, codepoints);
110
- }
111
- catch (error) {
112
- fail('host_font_subset_failed', error.message || 'The font could not be subset');
113
- return;
114
- }
115
- const subsetSha = crypto.createHash('sha256').update(data).digest('hex');
116
- const answer = {
117
- type: 'host_font_subset_ready',
118
- clientId: streamId,
119
- requestId: request.requestId,
120
- sha256: source.sha256,
121
- subsetSha,
122
- bytes: data.length,
123
- };
124
- if (data.length <= TERMINAL_FONT_CHUNK_BYTES) {
125
- answer.dataBase64 = data.toString('base64');
126
- }
127
- else {
128
- this.hold(subsetSha, data);
129
- }
130
- this.send(answer);
131
- }
132
- /** Whether the file behind `source` is still the one that was announced. */
133
- unchanged(source) {
134
- try {
135
- const stat = fs.statSync(source.path);
136
- return stat.size === source.bytes && stat.mtimeMs === source.mtimeMs;
137
- }
138
- catch {
139
- return false;
140
- }
141
- }
142
- /** Keep a cut font for its slices, dropping the oldest beyond the cache size. */
143
- hold(subsetSha, data) {
144
- this.subsets.delete(subsetSha);
145
- this.subsets.set(subsetSha, data);
146
- let held = [...this.subsets.values()].reduce((sum, item) => sum + item.length, 0);
147
- for (const [key, item] of this.subsets) {
148
- if (held <= FONT_SUBSET_CACHE_BYTES || key === subsetSha)
149
- break;
150
- this.subsets.delete(key);
151
- held -= item.length;
152
- }
153
- }
154
- /** A slice of a cut font still held for the browser that asked for it. */
155
- subsetChunk(sha256, index) {
156
- const data = this.subsets.get(sha256);
157
- if (!data)
158
- return null;
159
- const total = Math.ceil(data.length / TERMINAL_FONT_CHUNK_BYTES);
160
- if (!Number.isInteger(index) || index < 0 || index >= total)
161
- return null;
162
- const start = index * TERMINAL_FONT_CHUNK_BYTES;
163
- return { data: data.subarray(start, start + TERMINAL_FONT_CHUNK_BYTES), total };
164
- }
165
- }
@@ -1,28 +0,0 @@
1
- // The keys that put this workstation's Herdr into prefix mode. A browser has
2
- // to tell a Herdr command from typed text, and only Herdr's own config file
3
- // says which key starts one, so the host reads it and reports it in its hello.
4
- import fs from 'node:fs';
5
- import { sanitizeHerdrPrefixKeys } from 'herdr-remote-relay/protocol';
6
- import { herdrConfigPath } from '../socket-discovery.js';
7
- import { parseTomlSubset } from '../terminal-font/formats.js';
8
- /** What Herdr uses when `keys.prefix` is absent or names nothing it can read. */
9
- const DEFAULT_PREFIX_KEYS = ['ctrl+b'];
10
- /**
11
- * `keys.prefix` from the Herdr config the connector's Herdr reads: one key or
12
- * a list, each a combo like `ctrl+space`. Always at least one key, because
13
- * Herdr itself falls back to `ctrl+b` when the file, the key or every entry
14
- * is unusable.
15
- */
16
- export function readHerdrPrefixKeys(env = process.env, platform = process.platform) {
17
- let text;
18
- try {
19
- text = fs.readFileSync(herdrConfigPath(env, platform), 'utf8');
20
- }
21
- catch {
22
- // No readable config file means Herdr runs on its defaults.
23
- return [...DEFAULT_PREFIX_KEYS];
24
- }
25
- const configured = parseTomlSubset(text)['keys.prefix'];
26
- const entries = typeof configured === 'string' ? [configured] : configured;
27
- return sanitizeHerdrPrefixKeys(entries) ?? [...DEFAULT_PREFIX_KEYS];
28
- }