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
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 };