herdr-remote 0.2.39 → 0.3.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +4 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/chunks/3FQAXVGC.js +3 -0
  4. package/dist/chunks/42KARAL4.js +31 -0
  5. package/dist/chunks/44IGVPMA.js +5 -0
  6. package/dist/chunks/5LZFJ6UZ.js +3 -0
  7. package/dist/chunks/7SMZ43LI.js +9 -0
  8. package/dist/chunks/AX4EPJCP.js +4 -0
  9. package/dist/chunks/B76DFI4H.js +16 -0
  10. package/dist/chunks/BYGDPZZD.js +3 -0
  11. package/dist/chunks/C2UUZY6J.js +2 -0
  12. package/dist/chunks/CVJ4HBMP.js +3 -0
  13. package/dist/chunks/DGUZCGQN.js +3 -0
  14. package/dist/chunks/FONUO5O5.js +4 -0
  15. package/dist/chunks/H24ZP5D5.js +16 -0
  16. package/dist/chunks/I6NRCYLI.js +3 -0
  17. package/dist/chunks/JJGEV4EZ.js +3 -0
  18. package/dist/chunks/K5RFPHSF.js +5 -0
  19. package/dist/chunks/KGOQKOHH.js +5 -0
  20. package/dist/chunks/KRVF37OW.js +3 -0
  21. package/dist/chunks/KUDCC7ZO.js +6 -0
  22. package/dist/chunks/LPR662JH.js +77 -0
  23. package/dist/chunks/LY2ZGCXN.js +4 -0
  24. package/dist/chunks/M52LJEES.js +6 -0
  25. package/dist/chunks/MCUVFSSK.js +3 -0
  26. package/dist/chunks/MEFDV375.js +10 -0
  27. package/dist/chunks/MKAEXKXG.js +10 -0
  28. package/dist/chunks/O2LNVFUT.js +3 -0
  29. package/dist/chunks/PBMBFWLQ.js +3 -0
  30. package/dist/chunks/PBRQ3HEG.js +3 -0
  31. package/dist/chunks/QCSILMFB.js +3 -0
  32. package/dist/chunks/QRGNEKME.js +5 -0
  33. package/dist/chunks/RMHH3IAX.js +63 -0
  34. package/dist/chunks/TNLCNVA2.js +3 -0
  35. package/dist/chunks/U6QAYG3E.js +3 -0
  36. package/dist/chunks/VD54DF7R.js +45 -0
  37. package/dist/chunks/WY7WHEJ4.js +3 -0
  38. package/dist/cli.js +21 -294
  39. package/dist/connector/admin-broker.js +3 -303
  40. package/dist/connector/main.js +20 -18
  41. package/dist/harfbuzz-subset.wasm +0 -0
  42. package/dist/postinstall.js +17 -0
  43. package/herdr-plugin.toml +1 -1
  44. package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
  45. package/node_modules/herdr-remote-relay/package.json +3 -46
  46. package/package.json +8 -27
  47. package/dist/agent-status.js +0 -130
  48. package/dist/binary-plist.js +0 -157
  49. package/dist/config.js +0 -278
  50. package/dist/connector/admin-broker-setup.js +0 -40
  51. package/dist/connector/admin-shell.js +0 -67
  52. package/dist/connector/admin-tab.js +0 -90
  53. package/dist/connector/adminBrokerEndpoint.js +0 -68
  54. package/dist/connector/agent-watch.js +0 -123
  55. package/dist/connector/elevatedPty.js +0 -141
  56. package/dist/connector/font-server.js +0 -165
  57. package/dist/connector/herdr-prefix-keys.js +0 -28
  58. package/dist/connector/host-connector.js +0 -433
  59. package/dist/connector/host-hello.js +0 -32
  60. package/dist/connector/lock.js +0 -73
  61. package/dist/connector/sessions.js +0 -381
  62. package/dist/connector/update-report.js +0 -70
  63. package/dist/connector/wire.js +0 -42
  64. package/dist/exit-codes.js +0 -13
  65. package/dist/font-subset.js +0 -116
  66. package/dist/herdr-api.js +0 -254
  67. package/dist/herdr-command.js +0 -245
  68. package/dist/herdr-plugin.js +0 -120
  69. package/dist/herdr-server.js +0 -190
  70. package/dist/i18n/en.js +0 -257
  71. package/dist/i18n/index.js +0 -48
  72. package/dist/i18n/zh.js +0 -256
  73. package/dist/keepalive/detached.js +0 -97
  74. package/dist/keepalive/environment.js +0 -44
  75. package/dist/keepalive/index.js +0 -129
  76. package/dist/keepalive/launchd.js +0 -117
  77. package/dist/keepalive/systemd.js +0 -112
  78. package/dist/keepalive/types.js +0 -1
  79. package/dist/keepalive/windows-service-setup.js +0 -138
  80. package/dist/keepalive/windows-service.js +0 -272
  81. package/dist/keepalive/windows-task.js +0 -191
  82. package/dist/lib/process.js +0 -132
  83. package/dist/lifecycle.js +0 -79
  84. package/dist/net-interfaces.js +0 -82
  85. package/dist/node-pty-loader.js +0 -80
  86. package/dist/pasted-files.js +0 -115
  87. package/dist/paths.js +0 -74
  88. package/dist/pty-session.js +0 -133
  89. package/dist/relay-client.js +0 -106
  90. package/dist/relay-urls.js +0 -99
  91. package/dist/runtime.js +0 -107
  92. package/dist/service.js +0 -267
  93. package/dist/settings-model.js +0 -298
  94. package/dist/socket-discovery.js +0 -70
  95. package/dist/supervisor.js +0 -323
  96. package/dist/terminal-font/deps.js +0 -83
  97. package/dist/terminal-font/files.js +0 -357
  98. package/dist/terminal-font/formats.js +0 -355
  99. package/dist/terminal-font/index.js +0 -187
  100. package/dist/terminal-font/readers.js +0 -373
  101. package/dist/terminal-font/terminals.js +0 -147
  102. package/dist/terminal-palette.js +0 -399
  103. package/dist/tui/App.js +0 -199
  104. package/dist/tui/api.js +0 -91
  105. package/dist/tui/components/TextField.js +0 -50
  106. package/dist/tui/components/common.js +0 -80
  107. package/dist/tui/index.js +0 -36
  108. package/dist/tui/mouse/index.js +0 -164
  109. package/dist/tui/mouse/protocol.js +0 -96
  110. package/dist/tui/mouse/source.js +0 -79
  111. package/dist/tui/screens/About.js +0 -172
  112. package/dist/tui/screens/Herdr.js +0 -75
  113. package/dist/tui/screens/Keepalive.js +0 -141
  114. package/dist/tui/screens/Overview.js +0 -36
  115. package/dist/tui/screens/Pair.js +0 -74
  116. package/dist/tui/screens/Relay.js +0 -243
  117. package/dist/tui/screens/Services.js +0 -63
  118. package/dist/tui/screens/Wizard.js +0 -241
  119. package/dist/tui/terminal.js +0 -39
  120. package/dist/tui/theme.js +0 -33
  121. package/dist/update-command.js +0 -118
  122. package/dist/updater/in-place.js +0 -83
  123. package/dist/updater/npm.js +0 -62
  124. package/dist/updater.js +0 -401
  125. package/node_modules/herdr-remote-relay/README.md +0 -76
  126. package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
  127. package/node_modules/herdr-remote-relay/config.example.json +0 -18
  128. package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
  129. package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
  130. package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
  131. package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
  132. package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
  133. package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
  134. package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
  135. package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
  136. package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
  137. package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
  138. package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
  139. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
  140. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
  141. package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
  142. package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
  143. package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
  144. package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
  145. package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
  146. package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
  147. package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
  148. package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
  149. package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
  150. package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
  151. package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
  152. package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
  153. package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
  154. package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
  155. package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
  156. package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
  157. package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
  158. package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
  159. package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
  160. package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
  161. package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
  162. package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
  163. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
  164. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
  165. package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
  166. package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
  167. package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
  168. package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
  169. package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
  170. package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
  171. package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
  172. package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
  173. package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
  174. package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
  175. package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
  176. package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
  177. package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
  178. package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
  179. package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
  180. package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
  181. package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
  182. package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
  183. package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
  184. package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
  185. package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
  186. package/node_modules/herdr-remote-relay/dist/state.js +0 -47
  187. package/node_modules/ws/LICENSE +0 -20
  188. package/node_modules/ws/README.md +0 -548
  189. package/node_modules/ws/browser.js +0 -8
  190. package/node_modules/ws/index.js +0 -22
  191. package/node_modules/ws/lib/buffer-util.js +0 -131
  192. package/node_modules/ws/lib/constants.js +0 -19
  193. package/node_modules/ws/lib/event-target.js +0 -292
  194. package/node_modules/ws/lib/extension.js +0 -203
  195. package/node_modules/ws/lib/limiter.js +0 -55
  196. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  197. package/node_modules/ws/lib/receiver.js +0 -743
  198. package/node_modules/ws/lib/sender.js +0 -607
  199. package/node_modules/ws/lib/stream.js +0 -161
  200. package/node_modules/ws/lib/subprotocol.js +0 -62
  201. package/node_modules/ws/lib/validation.js +0 -152
  202. package/node_modules/ws/lib/websocket-server.js +0 -562
  203. package/node_modules/ws/lib/websocket.js +0 -1407
  204. package/node_modules/ws/package.json +0 -74
  205. package/node_modules/ws/wrapper.mjs +0 -21
@@ -1,150 +0,0 @@
1
- // The workstation's terminal palette and font, as a host reports them and a
2
- // browser receives them. Browser-safe: no Node APIs, so the web app imports it.
3
- /**
4
- * The sixteen ANSI slots a host may report, in index order. A palette is
5
- * all-or-nothing: half the host's colors mixed with half the browser's would
6
- * look worse than either set on its own.
7
- */
8
- export const ANSI_PALETTE_KEYS = [
9
- 'black',
10
- 'red',
11
- 'green',
12
- 'yellow',
13
- 'blue',
14
- 'magenta',
15
- 'cyan',
16
- 'white',
17
- 'brightBlack',
18
- 'brightRed',
19
- 'brightGreen',
20
- 'brightYellow',
21
- 'brightBlue',
22
- 'brightMagenta',
23
- 'brightCyan',
24
- 'brightWhite',
25
- ];
26
- /** The faces a host may offer, in the order a browser registers them. */
27
- export const TERMINAL_FONT_STYLES = ['regular', 'bold', 'italic', 'boldItalic'];
28
- export const TERMINAL_FONT_FORMATS = ['truetype', 'opentype'];
29
- /** `cjk`: the face Hanzi fall back to; `all`: the family itself, too big to send whole. */
30
- export const TERMINAL_FONT_SUBSET_SCOPES = ['cjk', 'all'];
31
- /** One font file; a CJK face is larger than this and is left to the browser. */
32
- export const MAX_TERMINAL_FONT_BYTES = 16 * 1024 * 1024;
33
- /** Raw bytes per `host_font_chunk`; base64 keeps it far below any payload cap. */
34
- export const TERMINAL_FONT_CHUNK_BYTES = 256 * 1024;
35
- const HEX_COLOR = /^#[0-9a-f]{6}$/i;
36
- /**
37
- * A family name ends up inside a CSS `font-family` list in the browser. Quotes,
38
- * separators and escapes are what would let a name break out of its slot, and
39
- * no real family needs them.
40
- */
41
- const FONT_FAMILY_FORBIDDEN = /["'\\;,{}<>@\u0000-\u001f\u007f]/;
42
- const SHA256_HEX = /^[0-9a-f]{64}$/;
43
- function isRecord(value) {
44
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
45
- }
46
- function isOneOf(list, value) {
47
- return list.includes(value);
48
- }
49
- function hexColor(value) {
50
- return typeof value === 'string' && HEX_COLOR.test(value) ? value.toLowerCase() : undefined;
51
- }
52
- /**
53
- * Validates a terminal palette crossing the wire.
54
- *
55
- * The host reports what its own terminal answered to the OSC color queries,
56
- * and the browser paints with it. Anything that is not a plain `#rrggbb`
57
- * string is dropped here, so a compromised or buggy host cannot push arbitrary
58
- * data into a browser's renderer options.
59
- */
60
- export function sanitizeTerminalPalette(value) {
61
- if (!isRecord(value))
62
- return null;
63
- const palette = {};
64
- for (const key of ['background', 'foreground', 'cursor']) {
65
- const color = hexColor(value[key]);
66
- if (color)
67
- palette[key] = color;
68
- }
69
- const ansi = value.ansi;
70
- if (isRecord(ansi)) {
71
- const collected = {};
72
- for (const key of ANSI_PALETTE_KEYS) {
73
- const color = hexColor(ansi[key]);
74
- if (color)
75
- collected[key] = color;
76
- }
77
- if (Object.keys(collected).length === ANSI_PALETTE_KEYS.length) {
78
- palette.ansi = collected;
79
- }
80
- }
81
- return Object.keys(palette).length > 0 ? palette : null;
82
- }
83
- function familyName(value) {
84
- const family = typeof value === 'string' ? value.trim() : '';
85
- if (!family || family.length > 128 || FONT_FAMILY_FORBIDDEN.test(family))
86
- return null;
87
- return family;
88
- }
89
- /**
90
- * Validates the terminal font a host reports.
91
- *
92
- * The browser draws the session in the family the workstation's terminal
93
- * uses, and may fetch the font files by their hash. Every field is optional
94
- * except the family; anything malformed is dropped rather than repaired.
95
- */
96
- export function sanitizeTerminalFont(value) {
97
- if (!isRecord(value))
98
- return null;
99
- const family = familyName(value.family);
100
- if (!family)
101
- return null;
102
- const size = Number(value.sizePx);
103
- const sizePx = Number.isFinite(size) && size >= 4 && size <= 96 ? Math.round(size * 10) / 10 : null;
104
- const source = typeof value.source === 'string' && /^[a-z0-9-]{1,32}$/.test(value.source)
105
- ? value.source
106
- : null;
107
- const faces = [];
108
- const seen = new Set();
109
- for (const face of Array.isArray(value.faces) ? value.faces.slice(0, 16) : []) {
110
- if (!isRecord(face))
111
- continue;
112
- const { style, format, bytes, sha256 } = face;
113
- if (!isOneOf(TERMINAL_FONT_STYLES, style) || seen.has(style))
114
- continue;
115
- if (!isOneOf(TERMINAL_FONT_FORMATS, format))
116
- continue;
117
- if (typeof bytes !== 'number' || !Number.isInteger(bytes))
118
- continue;
119
- if (bytes <= 0 || bytes > MAX_TERMINAL_FONT_BYTES)
120
- continue;
121
- if (typeof sha256 !== 'string' || !SHA256_HEX.test(sha256))
122
- continue;
123
- seen.add(style);
124
- faces.push({ style, format, bytes, sha256 });
125
- }
126
- // Large fonts (CJK above all) are offered a few characters at a time.
127
- const subsets = [];
128
- for (const entry of Array.isArray(value.subsets) ? value.subsets.slice(0, 16) : []) {
129
- if (!isRecord(entry))
130
- continue;
131
- const name = familyName(entry.family);
132
- const { scope, sha256 } = entry;
133
- if (!name || entry.style !== 'regular')
134
- continue;
135
- if (!isOneOf(TERMINAL_FONT_SUBSET_SCOPES, scope))
136
- continue;
137
- if (typeof sha256 !== 'string' || !SHA256_HEX.test(sha256))
138
- continue;
139
- if (subsets.some((known) => known.scope === scope))
140
- continue;
141
- subsets.push({ family: name, style: 'regular', scope, sha256 });
142
- }
143
- return {
144
- family,
145
- ...(sizePx !== null && { sizePx }),
146
- ...(source !== null && { source }),
147
- faces,
148
- subsets,
149
- };
150
- }
@@ -1,74 +0,0 @@
1
- declare const PACKAGE_ROOT: string;
2
- export type RelayMode = 'local' | 'remote';
3
- /** A relay configuration after `validate`: every field present and in range. */
4
- export interface RelayConfig {
5
- relay: {
6
- mode: RelayMode;
7
- host: string;
8
- port: number;
9
- publicUrl: string;
10
- maxPayloadBytes: number;
11
- maxClientsPerHost: number;
12
- maxHosts: number;
13
- maxPendingHandshakes: number;
14
- maxBufferedBytesPerClient: number;
15
- allowedOrigins: string[];
16
- trustProxy: boolean;
17
- hostReconnectGraceMs: number;
18
- /** Development only: simulated round-trip latency. */
19
- devLatencyMs?: number;
20
- };
21
- auth: {
22
- pairingTtlMs: number;
23
- deviceTtlMs: number;
24
- maxDevices: number;
25
- password: string | null;
26
- adminToken: string | null;
27
- stateFile: string | null;
28
- };
29
- cleanup: {
30
- intervalMs: number;
31
- heartbeatIntervalMs: number;
32
- staleAfterMs: number;
33
- };
34
- }
35
- /**
36
- * A configuration while it is being assembled: file, environment and flags
37
- * may each put a string, or anything else, where `validate` expects a number.
38
- */
39
- type RelayConfigDraft = {
40
- [Section in keyof RelayConfig]: Record<string, unknown>;
41
- };
42
- /** Flags given as `--name value` or `--name=value`, by name. */
43
- export type RelayOptions = Record<string, string>;
44
- declare function defaultStateDir(): string;
45
- declare const DEFAULTS: RelayConfig;
46
- /**
47
- * Parse `--flag value` / `--flag=value` pairs plus the standalone flags the
48
- * relay binary understands. Returns { options, help, version, errors }.
49
- */
50
- declare function parseArgv(argv?: readonly string[]): {
51
- options: RelayOptions;
52
- help: boolean;
53
- version: boolean;
54
- errors: string[];
55
- };
56
- declare function validate(config: RelayConfigDraft): RelayConfig;
57
- /**
58
- * Warnings a standalone operator should see at startup. These are advisory —
59
- * the relay still starts, because a loopback-only development run legitimately
60
- * needs neither TLS nor tokens.
61
- */
62
- declare function configWarnings(config: RelayConfig): string[];
63
- declare function loadRelayConfig({ argv, env, }?: {
64
- argv?: readonly string[];
65
- env?: NodeJS.ProcessEnv;
66
- }): {
67
- config: RelayConfig;
68
- help: boolean;
69
- version: boolean;
70
- errors: string[];
71
- configFile: string | null;
72
- warnings: string[];
73
- };
74
- export { PACKAGE_ROOT, DEFAULTS, loadRelayConfig, parseArgv, validate, configWarnings, defaultStateDir, };
@@ -1,316 +0,0 @@
1
- // Standalone relay configuration.
2
- //
3
- // The relay is deliberately decoupled from the herdr-remote plugin: it never
4
- // reads the plugin's config directory and never imports plugin code. Settings
5
- // come from (lowest to highest precedence) built-in defaults, an optional JSON
6
- // config file, environment variables, then command line flags.
7
- import fs from 'node:fs';
8
- import os from 'node:os';
9
- import path from 'node:path';
10
- const PACKAGE_ROOT = path.resolve(import.meta.dirname, '..');
11
- function defaultStateDir() {
12
- if (process.env.RELAY_STATE_DIR)
13
- return process.env.RELAY_STATE_DIR;
14
- const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state');
15
- return path.join(stateHome, 'herdr-remote-relay');
16
- }
17
- const DEFAULTS = {
18
- relay: {
19
- // A relay package is remote/operator-facing by default. The workstation
20
- // service sets this to local for the private relay it starts itself so the
21
- // web client can distinguish its own status page from the operator console.
22
- mode: 'remote',
23
- // Bind to loopback by default: the common production shape is a TLS
24
- // reverse proxy on the same machine. Containers and LAN deployments set
25
- // RELAY_BIND=0.0.0.0 explicitly.
26
- host: '127.0.0.1',
27
- port: 8787,
28
- publicUrl: 'http://127.0.0.1:8787',
29
- // Sized for a pasted image, not for keystrokes. `ws` closes a connection
30
- // that receives an oversized frame (1009) rather than dropping the frame,
31
- // so this ceiling is what a client's own upload cap has to stay under.
32
- maxPayloadBytes: 5 * 1024 * 1024,
33
- maxClientsPerHost: 16,
34
- maxHosts: 1024,
35
- maxPendingHandshakes: 1024,
36
- maxBufferedBytesPerClient: 4 * 1024 * 1024,
37
- allowedOrigins: [],
38
- trustProxy: false,
39
- hostReconnectGraceMs: 30 * 1000,
40
- },
41
- auth: {
42
- pairingTtlMs: 10 * 60 * 1000,
43
- deviceTtlMs: 30 * 24 * 60 * 60 * 1000,
44
- maxDevices: 32,
45
- // Optional shared password. Unset means a public relay: anyone may connect
46
- // a workstation, and each one is still reachable only through its own host
47
- // token.
48
- password: null,
49
- // Optional operator credential for the standalone relay dashboard. This is
50
- // deliberately separate from the join password and from device tokens:
51
- // the former is shared with workstations, while the latter is scoped to a
52
- // single workstation.
53
- adminToken: null,
54
- stateFile: null,
55
- },
56
- cleanup: {
57
- intervalMs: 60 * 1000,
58
- heartbeatIntervalMs: 30 * 1000,
59
- staleAfterMs: 90 * 1000,
60
- },
61
- };
62
- function clone(value) {
63
- return JSON.parse(JSON.stringify(value));
64
- }
65
- function readJsonFile(filePath) {
66
- try {
67
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
68
- }
69
- catch (error) {
70
- const { code, message } = error;
71
- if (code === 'ENOENT')
72
- return null;
73
- throw new Error(`cannot read relay config ${filePath}: ${message}`);
74
- }
75
- }
76
- function parseBoolean(value, fallback) {
77
- if (value === undefined || value === null || value === '')
78
- return fallback;
79
- if (value === true || value === false)
80
- return value;
81
- const normalized = String(value).trim().toLowerCase();
82
- if (['true', '1', 'yes', 'on'].includes(normalized))
83
- return true;
84
- if (['false', '0', 'no', 'off'].includes(normalized))
85
- return false;
86
- return fallback;
87
- }
88
- function parseInteger(value, fallback, min, max) {
89
- const numeric = Number(value);
90
- if (!Number.isInteger(numeric) || numeric < min || numeric > max)
91
- return fallback;
92
- return numeric;
93
- }
94
- function parseOriginList(value) {
95
- if (Array.isArray(value))
96
- return value.map((entry) => String(entry).trim()).filter(Boolean);
97
- if (typeof value !== 'string')
98
- return null;
99
- return value
100
- .split(',')
101
- .map((entry) => entry.trim())
102
- .filter(Boolean);
103
- }
104
- function mergeSection(target, source) {
105
- if (!source || typeof source !== 'object')
106
- return;
107
- const values = source;
108
- for (const key of Object.keys(target)) {
109
- if (values[key] !== undefined)
110
- target[key] = values[key];
111
- }
112
- }
113
- /**
114
- * Parse `--flag value` / `--flag=value` pairs plus the standalone flags the
115
- * relay binary understands. Returns { options, help, version, errors }.
116
- */
117
- function parseArgv(argv = []) {
118
- const options = {};
119
- const errors = [];
120
- let help = false;
121
- let version = false;
122
- for (let index = 0; index < argv.length; index += 1) {
123
- const arg = argv[index];
124
- if (arg === '--help' || arg === '-h') {
125
- help = true;
126
- continue;
127
- }
128
- if (arg === '--version' || arg === '-v') {
129
- version = true;
130
- continue;
131
- }
132
- if (!arg.startsWith('--')) {
133
- errors.push(`unexpected argument: ${arg}`);
134
- continue;
135
- }
136
- const equals = arg.indexOf('=');
137
- const name = equals === -1 ? arg.slice(2) : arg.slice(2, equals);
138
- let value = equals === -1 ? undefined : arg.slice(equals + 1);
139
- if (value === undefined) {
140
- const next = argv[index + 1];
141
- if (next === undefined || next.startsWith('--')) {
142
- // Bare boolean flag.
143
- value = 'true';
144
- }
145
- else {
146
- value = next;
147
- index += 1;
148
- }
149
- }
150
- options[name] = value;
151
- }
152
- return { options, help, version, errors };
153
- }
154
- function applyFile(config, fileConfig) {
155
- if (!fileConfig || typeof fileConfig !== 'object')
156
- return;
157
- const sections = fileConfig;
158
- mergeSection(config.relay, sections.relay);
159
- mergeSection(config.auth, sections.auth);
160
- mergeSection(config.cleanup, sections.cleanup);
161
- }
162
- function applyEnvironment(config, env) {
163
- if (env.RELAY_DEPLOYMENT_MODE === 'local' || env.RELAY_DEPLOYMENT_MODE === 'remote') {
164
- config.relay.mode = env.RELAY_DEPLOYMENT_MODE;
165
- }
166
- if (env.RELAY_BIND)
167
- config.relay.host = env.RELAY_BIND;
168
- if (env.RELAY_PORT)
169
- config.relay.port = env.RELAY_PORT;
170
- if (env.RELAY_PUBLIC_URL)
171
- config.relay.publicUrl = env.RELAY_PUBLIC_URL;
172
- if (env.RELAY_MAX_PAYLOAD_BYTES)
173
- config.relay.maxPayloadBytes = env.RELAY_MAX_PAYLOAD_BYTES;
174
- if (env.RELAY_MAX_CLIENTS_PER_HOST)
175
- config.relay.maxClientsPerHost = env.RELAY_MAX_CLIENTS_PER_HOST;
176
- if (env.RELAY_MAX_HOSTS)
177
- config.relay.maxHosts = env.RELAY_MAX_HOSTS;
178
- if (env.RELAY_MAX_PENDING_HANDSHAKES)
179
- config.relay.maxPendingHandshakes = env.RELAY_MAX_PENDING_HANDSHAKES;
180
- if (env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT)
181
- config.relay.maxBufferedBytesPerClient = env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT;
182
- if (env.RELAY_HOST_RECONNECT_GRACE_MS)
183
- config.relay.hostReconnectGraceMs = env.RELAY_HOST_RECONNECT_GRACE_MS;
184
- if (env.RELAY_ALLOWED_ORIGINS !== undefined) {
185
- const origins = parseOriginList(env.RELAY_ALLOWED_ORIGINS);
186
- if (origins)
187
- config.relay.allowedOrigins = origins;
188
- }
189
- if (env.RELAY_TRUST_PROXY !== undefined) {
190
- config.relay.trustProxy = parseBoolean(env.RELAY_TRUST_PROXY, config.relay.trustProxy);
191
- }
192
- if (env.RELAY_PASSWORD)
193
- config.auth.password = env.RELAY_PASSWORD;
194
- if (env.RELAY_ADMIN_TOKEN)
195
- config.auth.adminToken = env.RELAY_ADMIN_TOKEN;
196
- if (env.RELAY_AUTH_STATE_FILE)
197
- config.auth.stateFile = env.RELAY_AUTH_STATE_FILE;
198
- if (env.RELAY_PAIRING_TTL_MS)
199
- config.auth.pairingTtlMs = env.RELAY_PAIRING_TTL_MS;
200
- if (env.RELAY_DEVICE_TTL_MS)
201
- config.auth.deviceTtlMs = env.RELAY_DEVICE_TTL_MS;
202
- if (env.RELAY_MAX_DEVICES)
203
- config.auth.maxDevices = env.RELAY_MAX_DEVICES;
204
- }
205
- function applyOptions(config, options) {
206
- if (options['deployment-mode'] === 'local' || options['deployment-mode'] === 'remote') {
207
- config.relay.mode = options['deployment-mode'];
208
- }
209
- if (options.bind)
210
- config.relay.host = options.bind;
211
- if (options.port)
212
- config.relay.port = options.port;
213
- if (options['public-url'])
214
- config.relay.publicUrl = options['public-url'];
215
- if (options['allowed-origins'] !== undefined) {
216
- const origins = parseOriginList(options['allowed-origins']);
217
- if (origins)
218
- config.relay.allowedOrigins = origins;
219
- }
220
- if (options['trust-proxy'] !== undefined) {
221
- config.relay.trustProxy = parseBoolean(options['trust-proxy'], config.relay.trustProxy);
222
- }
223
- if (options.password)
224
- config.auth.password = options.password;
225
- if (options['admin-token'])
226
- config.auth.adminToken = options['admin-token'];
227
- if (options['state-file'])
228
- config.auth.stateFile = options['state-file'];
229
- if (options['max-clients'])
230
- config.relay.maxClientsPerHost = options['max-clients'];
231
- if (options['max-hosts'])
232
- config.relay.maxHosts = options['max-hosts'];
233
- if (options['max-pending-handshakes'])
234
- config.relay.maxPendingHandshakes = options['max-pending-handshakes'];
235
- if (options['max-buffered-bytes'])
236
- config.relay.maxBufferedBytesPerClient = options['max-buffered-bytes'];
237
- if (options['host-reconnect-grace-ms'])
238
- config.relay.hostReconnectGraceMs = options['host-reconnect-grace-ms'];
239
- }
240
- function validate(config) {
241
- if (config.relay.mode !== 'local' && config.relay.mode !== 'remote')
242
- config.relay.mode = DEFAULTS.relay.mode;
243
- config.relay.port = parseInteger(config.relay.port, DEFAULTS.relay.port, 0, 65535);
244
- config.relay.maxPayloadBytes = parseInteger(config.relay.maxPayloadBytes, DEFAULTS.relay.maxPayloadBytes, 4096, 16 * 1024 * 1024);
245
- config.relay.maxClientsPerHost = parseInteger(config.relay.maxClientsPerHost, DEFAULTS.relay.maxClientsPerHost, 1, 256);
246
- config.relay.maxHosts = parseInteger(config.relay.maxHosts, DEFAULTS.relay.maxHosts, 1, 100000);
247
- config.relay.maxPendingHandshakes = parseInteger(config.relay.maxPendingHandshakes, DEFAULTS.relay.maxPendingHandshakes, 16, 100000);
248
- config.relay.maxBufferedBytesPerClient = parseInteger(config.relay.maxBufferedBytesPerClient, DEFAULTS.relay.maxBufferedBytesPerClient, 64 * 1024, 256 * 1024 * 1024);
249
- config.relay.hostReconnectGraceMs = parseInteger(config.relay.hostReconnectGraceMs, DEFAULTS.relay.hostReconnectGraceMs, 1000, 24 * 60 * 60 * 1000);
250
- config.auth.pairingTtlMs = parseInteger(config.auth.pairingTtlMs, DEFAULTS.auth.pairingTtlMs, 30 * 1000, 24 * 60 * 60 * 1000);
251
- config.auth.deviceTtlMs = parseInteger(config.auth.deviceTtlMs, DEFAULTS.auth.deviceTtlMs, 60 * 1000, 365 * 24 * 60 * 60 * 1000);
252
- config.auth.maxDevices = parseInteger(config.auth.maxDevices, DEFAULTS.auth.maxDevices, 1, 10000);
253
- config.cleanup.intervalMs = parseInteger(config.cleanup.intervalMs, DEFAULTS.cleanup.intervalMs, 1000, 24 * 60 * 60 * 1000);
254
- config.cleanup.heartbeatIntervalMs = parseInteger(config.cleanup.heartbeatIntervalMs, DEFAULTS.cleanup.heartbeatIntervalMs, 1000, 10 * 60 * 1000);
255
- config.cleanup.staleAfterMs = parseInteger(config.cleanup.staleAfterMs, DEFAULTS.cleanup.staleAfterMs, config.cleanup.heartbeatIntervalMs * 2, 24 * 60 * 60 * 1000);
256
- if (typeof config.relay.host !== 'string' || config.relay.host.length === 0) {
257
- config.relay.host = DEFAULTS.relay.host;
258
- }
259
- if (typeof config.relay.publicUrl !== 'string' || config.relay.publicUrl.length === 0) {
260
- config.relay.publicUrl = `http://${config.relay.host}:${config.relay.port}`;
261
- }
262
- config.relay.publicUrl = String(config.relay.publicUrl).replace(/\/+$/, '');
263
- if (!Array.isArray(config.relay.allowedOrigins))
264
- config.relay.allowedOrigins = [];
265
- config.relay.trustProxy = parseBoolean(config.relay.trustProxy, false);
266
- if (!config.auth.stateFile) {
267
- config.auth.stateFile = path.join(defaultStateDir(), 'relay-auth.json');
268
- }
269
- // Every field `RelayConfig` promises has been normalised above.
270
- return config;
271
- }
272
- /**
273
- * Warnings a standalone operator should see at startup. These are advisory —
274
- * the relay still starts, because a loopback-only development run legitimately
275
- * needs neither TLS nor tokens.
276
- */
277
- function configWarnings(config) {
278
- const warnings = [];
279
- const isLoopbackBind = ['127.0.0.1', 'localhost', '::1'].includes(config.relay.host);
280
- let publicUrl;
281
- try {
282
- publicUrl = new URL(config.relay.publicUrl);
283
- }
284
- catch {
285
- warnings.push(`publicUrl is not a valid URL: ${config.relay.publicUrl}`);
286
- }
287
- if (!config.auth.password) {
288
- warnings.push('no password set (RELAY_PASSWORD): this is a public relay, anyone may connect a workstation to it');
289
- }
290
- if (config.relay.mode === 'remote' && !config.auth.adminToken) {
291
- warnings.push('no admin token set (RELAY_ADMIN_TOKEN): the relay operator dashboard is unavailable');
292
- }
293
- if (!isLoopbackBind &&
294
- publicUrl &&
295
- publicUrl.protocol === 'http:' &&
296
- !['127.0.0.1', 'localhost'].includes(publicUrl.hostname)) {
297
- warnings.push(`publicUrl uses plain http on a non-loopback address (${config.relay.publicUrl}); terminate TLS in front of the relay`);
298
- }
299
- return warnings;
300
- }
301
- function loadRelayConfig({ argv = [], env = process.env, } = {}) {
302
- const { options, help, version, errors } = parseArgv(argv);
303
- const draft = clone(DEFAULTS);
304
- const configFile = options.config || env.HERDR_RELAY_CONFIG || null;
305
- if (configFile) {
306
- const fileConfig = readJsonFile(path.resolve(configFile));
307
- if (fileConfig === null)
308
- throw new Error(`relay config not found: ${configFile}`);
309
- applyFile(draft, fileConfig);
310
- }
311
- applyEnvironment(draft, env);
312
- applyOptions(draft, options);
313
- const config = validate(draft);
314
- return { config, help, version, errors, configFile, warnings: configWarnings(config) };
315
- }
316
- export { PACKAGE_ROOT, DEFAULTS, loadRelayConfig, parseArgv, validate, configWarnings, defaultStateDir, };
@@ -1,67 +0,0 @@
1
- import http from 'node:http';
2
- import type { Duplex } from 'node:stream';
3
- import { WebSocketServer } from 'ws';
4
- import { AuthStore } from './auth-store.js';
5
- import { RelayMetrics } from './metrics.js';
6
- import { PROTOCOL_VERSION } from './protocol/index.js';
7
- import { type RelayConfig } from './relay-config.js';
8
- import type { AttemptWindow, RelayClient, RelayContext, RelayHost, RelaySocket, SendQueue } from './server/types.js';
9
- import { VERSION } from './server/version.js';
10
- /** A configuration with any field left out; the defaults fill the rest. */
11
- export type RelayConfigInput = {
12
- [Section in keyof RelayConfig]?: Partial<RelayConfig[Section]>;
13
- };
14
- /** Overrides for embedding the relay, mostly in tests. */
15
- export interface RelayServerOptions {
16
- stateFile?: string;
17
- password?: string | null;
18
- adminToken?: string | null;
19
- trustProxy?: boolean;
20
- /** Simulated round-trip latency; each direction is delayed by half. */
21
- devLatencyMs?: number | string;
22
- metrics?: RelayMetrics;
23
- auth?: AuthStore;
24
- /** The built web client to serve; defaults to the package's `web/dist`. */
25
- publicDir?: string;
26
- }
27
- export type RelayAddress = {
28
- host: string;
29
- port: number;
30
- } | {
31
- path: string;
32
- };
33
- declare class RelayServer implements RelayContext {
34
- config: RelayConfig;
35
- relayMode: 'local' | 'remote';
36
- hosts: Map<string, RelayHost>;
37
- clients: Map<string, RelayClient>;
38
- streams: Map<string, string>;
39
- pairAttempts: Map<string, AttemptWindow>;
40
- clientHandshakeAttempts: Map<string, AttemptWindow>;
41
- hostHandshakeAttempts: Map<string, AttemptWindow>;
42
- /** Sockets that are upgraded but have not finished their hello. */
43
- pendingHandshakes: Set<RelaySocket>;
44
- startedAt: number;
45
- metrics: RelayMetrics;
46
- stateFile: string;
47
- password: string | null;
48
- adminToken: string | null;
49
- trustProxy: boolean;
50
- devLatencyMs: number;
51
- devDelayMs: number;
52
- publicDir: string;
53
- sendQueues: WeakMap<RelaySocket, SendQueue>;
54
- activeDelayTimers: Set<NodeJS.Timeout>;
55
- auth: AuthStore;
56
- server: http.Server;
57
- wss: WebSocketServer;
58
- heartbeatTimer: NodeJS.Timeout | null;
59
- cleanupTimer: NodeJS.Timeout | null;
60
- constructor(config?: RelayConfigInput, options?: RelayServerOptions);
61
- listen(port?: number, host?: string): Promise<RelayAddress>;
62
- address(): RelayAddress | null;
63
- close(): Promise<void>;
64
- handleUpgrade(req: http.IncomingMessage, socket: Duplex, head: Buffer): void;
65
- finishHandshake(ws: RelaySocket): void;
66
- }
67
- export { RelayServer, PROTOCOL_VERSION, VERSION };