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
package/dist/service.js DELETED
@@ -1,267 +0,0 @@
1
- // The processes that make up a running herdr-remote — the local relay and the
2
- // host connector — and starting, stopping and describing them.
3
- import { spawn } from 'node:child_process';
4
- import fs from 'node:fs';
5
- import { createRequire } from 'node:module';
6
- import path from 'node:path';
7
- import { ensureDir } from 'herdr-remote-relay/state';
8
- import { loadConfig } from './config.js';
9
- import { findHerdrCommand, herdrVersion, MIN_HERDR_VERSION, resolveHerdrCommand, } from './herdr-command.js';
10
- import { preferredLanAddress } from './net-interfaces.js';
11
- import { logPath, PACKAGE_ROOT, stateDir } from './paths.js';
12
- import { healthUrl, hostHeaders, requestJson, waitForHost, waitForRelay } from './relay-client.js';
13
- import { bindAddress, resolveAdminOrigin, resolveHostRelayUrl, resolvePublicUrl, runsLocalRelay, } from './relay-urls.js';
14
- import { ensureRuntime, managedPids, readRuntime, recordManagedPid, updateRuntime, writeRuntime, } from './runtime.js';
15
- import { resolveSocketPath } from './socket-discovery.js';
16
- import { resolveHostFont } from './terminal-font/index.js';
17
- import { hostTerminalPalette } from './terminal-palette.js';
18
- export function relayBinPath() {
19
- // Resolved rather than hard-coded: the relay is a separate npm package, so
20
- // its location depends on how npm hoisted it.
21
- const manifest = createRequire(import.meta.url).resolve('herdr-remote-relay/package.json');
22
- return path.join(path.dirname(manifest), 'bin', 'herdr-remote-relay.js');
23
- }
24
- function relayAuthStatePath() {
25
- return path.join(stateDir(), 'relay-auth.json');
26
- }
27
- /**
28
- * The processes that make up a running herdr-remote, as declarative specs.
29
- *
30
- * Both the detached starter and the foreground supervisor build their children
31
- * from this one list so the two paths cannot drift in how they configure the
32
- * relay or the host connector.
33
- */
34
- export function serviceSpecs(config = loadConfig(), state = ensureRuntime()) {
35
- const specs = [];
36
- const publicUrl = resolvePublicUrl(config, preferredLanAddress());
37
- if (runsLocalRelay(config)) {
38
- specs.push({
39
- name: 'relay',
40
- command: process.execPath,
41
- args: [relayBinPath()],
42
- cwd: stateDir(),
43
- env: {
44
- // Let the shared web UI tell a private workstation relay apart from
45
- // the operator-facing self-hosted relay. Standalone relay installs
46
- // default to remote mode.
47
- RELAY_DEPLOYMENT_MODE: 'local',
48
- RELAY_BIND: bindAddress(config),
49
- RELAY_PORT: String(config.relay.port),
50
- RELAY_PUBLIC_URL: publicUrl,
51
- // A relay we start ourselves is closed to everything but this
52
- // workstation: reusing the host token as its password costs nothing and
53
- // stops another machine on the LAN from enrolling into it.
54
- RELAY_PASSWORD: state.hostToken,
55
- RELAY_AUTH_STATE_FILE: relayAuthStatePath(),
56
- RELAY_ALLOWED_ORIGINS: (config.relay.allowedOrigins || []).join(','),
57
- RELAY_MAX_CLIENTS_PER_HOST: String(config.relay.maxClientsPerHost),
58
- RELAY_MAX_HOSTS: String(config.relay.maxHosts),
59
- RELAY_MAX_PENDING_HANDSHAKES: String(config.relay.maxPendingHandshakes),
60
- RELAY_MAX_BUFFERED_BYTES_PER_CLIENT: String(config.relay.maxBufferedBytesPerClient),
61
- RELAY_HOST_RECONNECT_GRACE_MS: String(config.relay.hostReconnectGraceMs),
62
- },
63
- });
64
- }
65
- const terminalPalette = hostTerminalPalette();
66
- // What this command detected at its entry point, else what an earlier start
67
- // with a terminal wrote down. Only the family, size and terminal travel here;
68
- // the connector finds the files itself, on the machine that has them.
69
- const terminalFont = resolveHostFont();
70
- specs.push({
71
- name: 'host',
72
- command: process.execPath,
73
- args: [path.join(PACKAGE_ROOT, 'dist', 'connector', 'main.js')],
74
- cwd: stateDir(),
75
- env: {
76
- // Captured here, where a terminal may still be attached, because the
77
- // connector itself usually runs detached with no terminal to ask.
78
- ...(terminalPalette ? { HERDR_TERM_PALETTE_JSON: JSON.stringify(terminalPalette) } : {}),
79
- ...(terminalFont ? { HERDR_TERM_FONT_JSON: JSON.stringify(terminalFont) } : {}),
80
- RELAY_URL: resolveHostRelayUrl(config),
81
- RELAY_HOST_ID: state.hostId,
82
- RELAY_HOST_TOKEN: state.hostToken,
83
- RELAY_PASSWORD: runsLocalRelay(config) ? state.hostToken : state.relayPassword || '',
84
- HERDR_SOCKET_PATH: resolveSocketPath(config.herdr.socketPath),
85
- HERDR_ARGS_JSON: JSON.stringify(config.herdr.args),
86
- HERDR_CWD: config.herdr.cwd,
87
- HERDR_BIN_PATH: resolveHerdrCommand(),
88
- },
89
- });
90
- return specs;
91
- }
92
- export function baseEnvironment() {
93
- return { ...process.env, HERDR_REMOTE_SERVICE: '1' };
94
- }
95
- function spawnDetached(spec) {
96
- ensureDir(stateDir());
97
- const logFd = fs.openSync(logPath(spec.name), 'a');
98
- try {
99
- const child = spawn(spec.command, spec.args, {
100
- cwd: spec.cwd,
101
- env: { ...baseEnvironment(), ...spec.env },
102
- detached: true,
103
- // A detached console program can open a visible window from a console-less Windows parent.
104
- windowsHide: true,
105
- stdio: ['ignore', logFd, logFd],
106
- });
107
- child.unref();
108
- return child.pid;
109
- }
110
- finally {
111
- fs.closeSync(logFd);
112
- }
113
- }
114
- /** Where each service's pid is kept in runtime.json. */
115
- const PID_FIELDS = { relay: 'relayPid', host: 'hostPid' };
116
- export function startServices() {
117
- const config = loadConfig();
118
- const state = ensureRuntime();
119
- const next = { ...state };
120
- const specs = serviceSpecs(config, state);
121
- const running = managedPids(next);
122
- const isRunning = (pid) => running.some((entry) => entry.pid === pid);
123
- for (const spec of specs) {
124
- const field = PID_FIELDS[spec.name];
125
- if (isRunning(next[field]))
126
- continue;
127
- next[field] = spawnDetached(spec) ?? null;
128
- recordManagedPid(next, spec.name, next[field]);
129
- }
130
- // A relay that is no longer part of the plan (switched to remote mode) must
131
- // not be left running on the old port.
132
- if (!specs.some((spec) => spec.name === 'relay') && isRunning(next.relayPid)) {
133
- try {
134
- process.kill(next.relayPid, 'SIGTERM');
135
- }
136
- catch {
137
- // Exited between the check and the signal: the port is free either way.
138
- }
139
- next.relayPid = null;
140
- }
141
- next.startedAt = next.startedAt || new Date().toISOString();
142
- next.mode = config.relay.mode;
143
- writeRuntime(next);
144
- const live = managedPids(next);
145
- return {
146
- ok: true,
147
- mode: config.relay.mode,
148
- relay: {
149
- local: runsLocalRelay(config),
150
- pid: next.relayPid || null,
151
- alive: live.some(({ pid }) => pid === next.relayPid),
152
- bind: runsLocalRelay(config) ? bindAddress(config) : null,
153
- port: config.relay.port,
154
- remoteUrl: config.relay.remoteUrl || null,
155
- },
156
- host: {
157
- pid: next.hostPid || null,
158
- alive: live.some(({ pid }) => pid === next.hostPid),
159
- socketPath: resolveSocketPath(config.herdr.socketPath),
160
- },
161
- publicUrl: resolvePublicUrl(config, preferredLanAddress()),
162
- };
163
- }
164
- export function stopServices() {
165
- const stopped = [];
166
- ensureDir(stateDir());
167
- updateRuntime((state) => {
168
- // Supervisor first: it would otherwise see its children die and restart
169
- // them faster than we can kill them.
170
- const targets = managedPids(state).sort((a, b) => (a.name === 'supervisor' ? 0 : 1) - (b.name === 'supervisor' ? 0 : 1));
171
- for (const { pid, name } of targets) {
172
- try {
173
- process.kill(pid, 'SIGTERM');
174
- stopped.push({ name, pid });
175
- }
176
- catch (error) {
177
- process.stderr.write(`herdr-remote: could not stop ${name} (pid ${pid}): ${error.message}\n`);
178
- }
179
- }
180
- state.hostPid = null;
181
- state.relayPid = null;
182
- state.supervisorPid = null;
183
- state.managedPids = [];
184
- state.startedAt = null;
185
- });
186
- return { ok: true, stopped };
187
- }
188
- export function restartServices() {
189
- stopServices();
190
- return startServices();
191
- }
192
- export async function statusServices() {
193
- const config = loadConfig();
194
- const state = readRuntime();
195
- const live = managedPids(state);
196
- const lanAddress = preferredLanAddress();
197
- let health;
198
- try {
199
- health = await requestJson(healthUrl(config), { timeout: 1200 });
200
- }
201
- catch (error) {
202
- health = { ok: false, message: error.message };
203
- }
204
- const socketPath = resolveSocketPath(config.herdr.socketPath);
205
- // Where Herdr was found, and whether it was found at all: under a service
206
- // manager this is the first thing to check when sessions will not start.
207
- const herdr = findHerdrCommand();
208
- // Only worth spawning when there is something to spawn; an unresolved command
209
- // has already answered the question the version would.
210
- const installed = herdr.found
211
- ? herdrVersion({ command: herdr.command })
212
- : { version: null, supported: true };
213
- return {
214
- ok: true,
215
- mode: config.relay.mode,
216
- relay: {
217
- local: runsLocalRelay(config),
218
- pid: state.relayPid || null,
219
- alive: runsLocalRelay(config) ? live.some(({ pid }) => pid === state.relayPid) : null,
220
- bind: runsLocalRelay(config) ? bindAddress(config) : null,
221
- port: config.relay.port,
222
- remoteUrl: config.relay.remoteUrl || null,
223
- health,
224
- },
225
- host: {
226
- pid: state.hostPid || null,
227
- alive: live.some(({ pid }) => pid === state.hostPid),
228
- hostId: state.hostId || null,
229
- socketPath,
230
- socketExists: Boolean(socketPath) && fs.existsSync(socketPath),
231
- herdrCommand: herdr.command,
232
- herdrCommandFound: herdr.found,
233
- herdrCommandSource: herdr.source,
234
- herdrVersion: installed.version,
235
- herdrVersionSupported: installed.supported,
236
- herdrVersionMinimum: MIN_HERDR_VERSION,
237
- },
238
- publicUrl: resolvePublicUrl(config, lanAddress),
239
- startedAt: state.startedAt || null,
240
- };
241
- }
242
- /** Start what pairing needs, then ask the relay for a code. */
243
- export async function pair() {
244
- const config = loadConfig();
245
- if (runsLocalRelay(config))
246
- startServices();
247
- await waitForRelay(config);
248
- await waitForHost(config);
249
- // Authenticated with this workstation's own host token: on a shared relay
250
- // that is what proves the pairing code is being minted for our terminal and
251
- // nobody else's.
252
- return requestJson(`${resolveAdminOrigin(config)}/api/pair/start`, {
253
- method: 'POST',
254
- headers: hostHeaders(),
255
- timeout: 5000,
256
- });
257
- }
258
- export function readLogTail(name, lines = 40) {
259
- try {
260
- const content = fs.readFileSync(logPath(name), 'utf8');
261
- return content.split('\n').filter(Boolean).slice(-lines);
262
- }
263
- catch {
264
- // No log yet: the service has never run.
265
- return [];
266
- }
267
- }
@@ -1,298 +0,0 @@
1
- // Editable settings, independent of how they are rendered.
2
- //
3
- // The TUI holds a *draft* copy of the config, mutates it through these pure
4
- // functions and writes it out on save. Keeping the rules here (rather than in
5
- // the view) means they can be tested without a terminal, and the same
6
- // validation applies to the first-run wizard and the settings screen.
7
- import { ACCESS_MODES, KEEPALIVE_MANAGERS, LANGUAGES, OFFICIAL_RELAY_URL, loadConfig, } from './config.js';
8
- import { configDir, configPath } from './paths.js';
9
- import { isLoopbackHost, isUnspecifiedAddress, isUnspecifiedHost, resolvePublicUrl, } from './relay-urls.js';
10
- import { ensureDir, readJson, writeJsonAtomic } from 'herdr-remote-relay/state';
11
- import { preferredLanAddress } from './net-interfaces.js';
12
- function isOneOf(list, value) {
13
- return list.includes(value);
14
- }
15
- /**
16
- * Field metadata. `kind` drives the editor the TUI shows; `visibleFor` limits a
17
- * field to the access modes where it means anything.
18
- */
19
- const FIELDS = [
20
- { id: 'mode', kind: 'choice', choices: ACCESS_MODES, labelKey: 'field.mode' },
21
- { id: 'port', kind: 'text', labelKey: 'field.port', visibleFor: ['local', 'lan'] },
22
- { id: 'lanHost', kind: 'address', labelKey: 'field.lanHost', visibleFor: ['lan'] },
23
- { id: 'remoteUrl', kind: 'text', labelKey: 'field.remoteUrl', visibleFor: ['remote'] },
24
- { id: 'publicUrl', kind: 'text', labelKey: 'field.publicUrl' },
25
- { id: 'socketPath', kind: 'text', labelKey: 'field.socketPath' },
26
- { id: 'herdrArgs', kind: 'text', labelKey: 'field.herdrArgs' },
27
- { id: 'herdrAutoStart', kind: 'toggle', labelKey: 'field.herdrAutoStart' },
28
- { id: 'language', kind: 'choice', choices: LANGUAGES, labelKey: 'field.language' },
29
- {
30
- id: 'keepaliveManager',
31
- kind: 'choice',
32
- choices: KEEPALIVE_MANAGERS,
33
- labelKey: 'field.keepalive',
34
- },
35
- ];
36
- const EMPTY = '';
37
- /**
38
- * The access modes a *person* chooses between.
39
- *
40
- * The official relay is stored as `remote` with a known URL, because that is
41
- * exactly what it is and nothing downstream should have to learn a fourth mode.
42
- * It is still a separate answer to "how do I reach this workstation", though:
43
- * one option needs no server and no credentials, the other needs both. Keeping
44
- * that distinction here — rather than in whichever screen happens to draw it —
45
- * is what stops the official relay from being displayed as "self-hosted relay"
46
- * that merely happens to hold our address.
47
- */
48
- const SELECTABLE_MODES = ['local', 'lan', 'official', 'remote'];
49
- /** Which of `SELECTABLE_MODES` this draft represents. */
50
- function selectedMode(draft) {
51
- if (draft.relay.mode === 'remote' && draft.relay.remoteUrl === OFFICIAL_RELAY_URL)
52
- return 'official';
53
- return draft.relay.mode;
54
- }
55
- /** True when the relay is the one we run, so its address and password are ours. */
56
- function isOfficialRelay(draft) {
57
- return selectedMode(draft) === 'official';
58
- }
59
- function clone(value) {
60
- return JSON.parse(JSON.stringify(value));
61
- }
62
- function createDraft(config = loadConfig()) {
63
- return clone(config);
64
- }
65
- function fieldsForMode(mode) {
66
- return FIELDS.filter((field) => !field.visibleFor || field.visibleFor.includes(mode));
67
- }
68
- /** Current value of a field as an editable string. */
69
- function getField(draft, id) {
70
- switch (id) {
71
- case 'mode':
72
- return draft.relay.mode;
73
- case 'port':
74
- return String(draft.relay.port);
75
- case 'lanHost':
76
- return draft.relay.lanHost || EMPTY;
77
- case 'remoteUrl':
78
- return draft.relay.remoteUrl || EMPTY;
79
- case 'publicUrl':
80
- return draft.relay.publicUrl || EMPTY;
81
- case 'socketPath':
82
- return draft.herdr.socketPath || EMPTY;
83
- case 'herdrArgs':
84
- return (draft.herdr.args || []).join(' ');
85
- case 'herdrAutoStart':
86
- return draft.herdr.autoStart ? 'on' : 'off';
87
- case 'language':
88
- return draft.ui.language;
89
- case 'keepaliveManager':
90
- return draft.keepalive.manager;
91
- default:
92
- return EMPTY;
93
- }
94
- }
95
- /**
96
- * What the field shows when it is empty: the value that will actually be used.
97
- * Displaying the derived value beats an empty box the user cannot interpret.
98
- */
99
- function getFieldPlaceholder(draft, id) {
100
- switch (id) {
101
- case 'lanHost':
102
- return preferredLanAddress() || '0.0.0.0';
103
- case 'publicUrl':
104
- return resolvePublicUrl(draft, preferredLanAddress());
105
- case 'socketPath':
106
- return 'placeholder.autoDiscovered';
107
- case 'herdrArgs':
108
- return 'placeholder.none';
109
- case 'remoteUrl':
110
- return 'wss://relay.example.com';
111
- default:
112
- return EMPTY;
113
- }
114
- }
115
- function isValidUrl(value, protocols) {
116
- try {
117
- const url = new URL(value);
118
- return protocols.includes(url.protocol);
119
- }
120
- catch {
121
- return false;
122
- }
123
- }
124
- /**
125
- * Apply a value to a copy of the draft.
126
- *
127
- * Returns `{ draft, errorKey }`. On a validation failure the draft comes back
128
- * unchanged and `errorKey` names a translatable message, so callers never have
129
- * to guess whether the edit landed.
130
- */
131
- function setField(draft, id, rawValue) {
132
- const next = clone(draft);
133
- const value = typeof rawValue === 'string' ? rawValue.trim() : rawValue;
134
- // Every field but the toggle is edited as text.
135
- const text = value;
136
- switch (id) {
137
- case 'mode': {
138
- if (!isOneOf(SELECTABLE_MODES, value))
139
- return { draft, errorKey: 'error.invalidMode' };
140
- // Picking the official relay fills in its address in the same edit: a
141
- // remote mode with no URL is not a valid state, and asking for the
142
- // address we already know would be asking the user to do our filing.
143
- if (value === 'official') {
144
- next.relay.mode = 'remote';
145
- next.relay.remoteUrl = OFFICIAL_RELAY_URL;
146
- next.relay.publicUrl = EMPTY;
147
- break;
148
- }
149
- // Leaving the official relay clears its address, so the self-hosted URL
150
- // field is empty and asking to be filled in rather than pre-loaded with
151
- // an address that belongs to somebody else's server.
152
- if (value === 'remote' && next.relay.remoteUrl === OFFICIAL_RELAY_URL) {
153
- next.relay.remoteUrl = EMPTY;
154
- }
155
- next.relay.mode = value;
156
- if (value === 'lan' &&
157
- (isLoopbackHost(next.relay.lanHost) || isUnspecifiedAddress(next.relay.lanHost))) {
158
- next.relay.lanHost = EMPTY;
159
- }
160
- // A public URL pinned for one mode is wrong for the next one; clearing it
161
- // lets derivation produce the right address again.
162
- next.relay.publicUrl = EMPTY;
163
- break;
164
- }
165
- case 'port': {
166
- const port = Number.parseInt(text, 10);
167
- if (!Number.isInteger(port) || port < 1 || port > 65535)
168
- return { draft, errorKey: 'error.invalidPort' };
169
- next.relay.port = port;
170
- break;
171
- }
172
- case 'lanHost': {
173
- if (text && (isLoopbackHost(text) || isUnspecifiedAddress(text))) {
174
- return { draft, errorKey: 'error.invalidLanHost' };
175
- }
176
- next.relay.lanHost = text || EMPTY;
177
- break;
178
- }
179
- case 'remoteUrl': {
180
- if (text && !isValidUrl(text, ['ws:', 'wss:', 'http:', 'https:'])) {
181
- return { draft, errorKey: 'error.invalidRelayUrl' };
182
- }
183
- next.relay.remoteUrl = text.replace(/\/+$/, '');
184
- break;
185
- }
186
- case 'publicUrl': {
187
- if (text && (!isValidUrl(text, ['http:', 'https:']) || isUnspecifiedHost(text))) {
188
- return { draft, errorKey: 'error.invalidPublicUrl' };
189
- }
190
- next.relay.publicUrl = text.replace(/\/+$/, '');
191
- break;
192
- }
193
- case 'socketPath': {
194
- next.herdr.socketPath = text || null;
195
- break;
196
- }
197
- case 'herdrArgs': {
198
- const args = text ? text.split(/\s+/).filter(Boolean) : [];
199
- if (args.includes('--no-session')) {
200
- return { draft, errorKey: 'error.removedHerdrArg' };
201
- }
202
- next.herdr.args = args;
203
- break;
204
- }
205
- case 'herdrAutoStart': {
206
- if (value !== true && value !== false && value !== 'on' && value !== 'off') {
207
- return { draft, errorKey: 'error.unknownField' };
208
- }
209
- next.herdr.autoStart = value === true || value === 'on';
210
- break;
211
- }
212
- case 'language': {
213
- if (!isOneOf(LANGUAGES, value))
214
- return { draft, errorKey: 'error.invalidLanguage' };
215
- next.ui.language = value;
216
- break;
217
- }
218
- case 'keepaliveManager': {
219
- if (!isOneOf(KEEPALIVE_MANAGERS, value))
220
- return { draft, errorKey: 'error.invalidKeepalive' };
221
- next.keepalive.manager = value;
222
- break;
223
- }
224
- default:
225
- return { draft, errorKey: 'error.unknownField' };
226
- }
227
- return { draft: next, errorKey: null };
228
- }
229
- /** Problems that should block saving, as translatable keys. */
230
- function validateDraft(draft) {
231
- const problems = [];
232
- if (draft.relay.mode === 'remote' && !draft.relay.remoteUrl)
233
- problems.push('error.remoteUrlRequired');
234
- return problems;
235
- }
236
- /**
237
- * Persist a draft, merging into whatever is already on disk so keys this
238
- * version does not know about survive the round trip.
239
- */
240
- function saveDraft(draft) {
241
- const problems = validateDraft(draft);
242
- if (problems.length > 0) {
243
- throw Object.assign(new Error(`configuration is incomplete: ${problems.join(', ')}`), {
244
- problems,
245
- });
246
- }
247
- const current = (readJson(configPath(), {}) || {});
248
- const merged = { ...current };
249
- merged.ui = { ...(current.ui || {}), language: draft.ui.language };
250
- const relay = {
251
- ...(current.relay || {}),
252
- mode: draft.relay.mode,
253
- port: draft.relay.port,
254
- lanHost: draft.relay.mode === 'lan' &&
255
- (isLoopbackHost(draft.relay.lanHost) || isUnspecifiedAddress(draft.relay.lanHost))
256
- ? EMPTY
257
- : draft.relay.lanHost,
258
- publicUrl: draft.relay.publicUrl,
259
- remoteUrl: draft.relay.remoteUrl,
260
- };
261
- merged.relay = relay;
262
- // Fields from the 0.1 schema would otherwise keep overriding the new ones on
263
- // the next load.
264
- delete relay.local;
265
- delete relay.host;
266
- delete relay.url;
267
- // The optional Herdr source patch is gone. Nothing reads this section any
268
- // more, so the next save is where a config written by an older build sheds
269
- // it rather than carrying stale checksums and paths forever.
270
- delete merged.patch;
271
- const herdr = {
272
- ...(current.herdr || {}),
273
- socketPath: draft.herdr.socketPath,
274
- args: draft.herdr.args,
275
- autoStart: draft.herdr.autoStart === true,
276
- };
277
- merged.herdr = herdr;
278
- if (!herdr.socketPath)
279
- delete herdr.socketPath;
280
- merged.keepalive = { ...(current.keepalive || {}), manager: draft.keepalive.manager };
281
- ensureDir(configDir());
282
- writeJsonAtomic(configPath(), merged);
283
- return { ok: true, path: configPath() };
284
- }
285
- /**
286
- * Settings that only take effect after the services restart. Used to show a
287
- * "restart required" hint rather than silently doing nothing.
288
- */
289
- function requiresRestart(before, after) {
290
- return (before.relay.mode !== after.relay.mode ||
291
- before.relay.port !== after.relay.port ||
292
- before.relay.lanHost !== after.relay.lanHost ||
293
- before.relay.remoteUrl !== after.relay.remoteUrl ||
294
- before.relay.publicUrl !== after.relay.publicUrl ||
295
- before.herdr.socketPath !== after.herdr.socketPath ||
296
- before.herdr.args.join(' ') !== after.herdr.args.join(' '));
297
- }
298
- export { createDraft, fieldsForMode, isOfficialRelay, selectedMode, getField, getFieldPlaceholder, setField, validateDraft, saveDraft, requiresRestart, };
@@ -1,70 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- /**
5
- * Herdr's own config directory, resolved as Herdr does (`XDG_CONFIG_HOME`
6
- * first on every platform, then the platform's home for it), so the socket
7
- * and `config.toml` are found wherever Herdr put them.
8
- */
9
- function herdrConfigDir(env = process.env, platform = process.platform) {
10
- if (platform === 'win32') {
11
- const configHome = env.XDG_CONFIG_HOME ||
12
- env.APPDATA ||
13
- (env.USERPROFILE && path.join(env.USERPROFILE, 'AppData', 'Roaming')) ||
14
- path.join(os.homedir(), 'AppData', 'Roaming');
15
- return path.join(configHome, 'herdr');
16
- }
17
- const configHome = env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
18
- return path.join(configHome, 'herdr');
19
- }
20
- function defaultSocketPath(env = process.env, platform = process.platform) {
21
- return env.HERDR_SOCKET_PATH || path.join(herdrConfigDir(env, platform), 'herdr.sock');
22
- }
23
- /** The `config.toml` Herdr reads; `HERDR_CONFIG_PATH` names it outright. */
24
- function herdrConfigPath(env = process.env, platform = process.platform) {
25
- return env.HERDR_CONFIG_PATH || path.join(herdrConfigDir(env, platform), 'config.toml');
26
- }
27
- /**
28
- * On Windows, Herdr uses this path itself as the GenericNamespaced pipe name;
29
- * the file only contains the `pid:nanos` marker.
30
- */
31
- function herdrEndpoint(socketPath, platform = process.platform) {
32
- return platform === 'win32' ? `\\\\.\\pipe\\${socketPath}` : socketPath;
33
- }
34
- function resolveSocketPath(configuredPath = null, env = process.env) {
35
- return configuredPath || env.HERDR_SOCKET_PATH || defaultSocketPath(env);
36
- }
37
- function inspectSocket(socketPath, options = {}) {
38
- if (typeof socketPath !== 'string' || socketPath.length === 0) {
39
- return { ok: false, reason: 'socket path is empty', path: socketPath };
40
- }
41
- try {
42
- const stat = fs.statSync(socketPath);
43
- // Herdr leaves an ordinary marker file on Windows and connects through the pipe named by its path.
44
- if (process.platform !== 'win32' && !stat.isSocket()) {
45
- return { ok: false, reason: 'path is not a Unix socket', path: socketPath };
46
- }
47
- // Windows has no per-process uid to compare, so ownership checks naturally skip there.
48
- if (options.requireOwner !== false &&
49
- typeof process.getuid === 'function' &&
50
- stat.uid !== process.getuid()) {
51
- return {
52
- ok: false,
53
- reason: 'socket is not owned by the current user',
54
- path: socketPath,
55
- uid: stat.uid,
56
- };
57
- }
58
- return { ok: true, path: socketPath, uid: stat.uid, mode: stat.mode };
59
- }
60
- catch (error) {
61
- const { code, message } = error;
62
- return {
63
- ok: false,
64
- missing: code === 'ENOENT',
65
- reason: code === 'ENOENT' ? 'socket does not exist' : message,
66
- path: socketPath,
67
- };
68
- }
69
- }
70
- export { defaultSocketPath, herdrConfigPath, herdrEndpoint, resolveSocketPath, inspectSocket };