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,142 +0,0 @@
1
- // Who is asking, and whether to let them: origins, credentials and rate limits
2
- // for HTTP requests and WebSocket handshakes.
3
- import crypto from 'node:crypto';
4
- function bearerToken(req) {
5
- const value = req.headers.authorization;
6
- if (typeof value !== 'string')
7
- return null;
8
- const match = /^Bearer\s+(.+)$/i.exec(value.trim());
9
- return match ? match[1] : null;
10
- }
11
- function tokenMatches(candidate, expected) {
12
- if (typeof candidate !== 'string' ||
13
- candidate.length === 0 ||
14
- typeof expected !== 'string' ||
15
- expected.length === 0)
16
- return false;
17
- // Hashing first gives timingSafeEqual fixed-size buffers, without leaking a
18
- // length mismatch through the comparison itself.
19
- const candidateHash = crypto.createHash('sha256').update(candidate, 'utf8').digest();
20
- const expectedHash = crypto.createHash('sha256').update(expected, 'utf8').digest();
21
- return crypto.timingSafeEqual(candidateHash, expectedHash);
22
- }
23
- export function isAllowedOrigin(relay, origin, req) {
24
- if (!origin)
25
- return true;
26
- const allowed = relay.config.relay.allowedOrigins || [];
27
- if (allowed.includes(origin))
28
- return true;
29
- try {
30
- return new URL(origin).host === req.headers.host;
31
- }
32
- catch {
33
- return false;
34
- }
35
- }
36
- export function requestOriginAllowed(relay, req) {
37
- const origin = req.headers.origin;
38
- if (!origin)
39
- return true;
40
- return isAllowedOrigin(relay, origin, req);
41
- }
42
- /**
43
- * Identify the workstation making a request by its own host token.
44
- * Ownership of a workstation is exactly what the host token proves, so a
45
- * public relay serving many workstations stays safe: nobody can mint a pairing
46
- * code for a host whose token they do not hold.
47
- */
48
- export function authorizedHost(relay, req) {
49
- const hostId = req.headers['x-herdr-host-id'];
50
- const token = req.headers['x-herdr-host-token'];
51
- if (typeof hostId !== 'string' || typeof token !== 'string')
52
- return null;
53
- return relay.auth.authenticateHost(hostId, token) ? hostId : null;
54
- }
55
- function authorizedDevice(relay, req) {
56
- const token = bearerToken(req);
57
- return token ? relay.auth.authenticateDevice(token) : null;
58
- }
59
- /**
60
- * Resolve the request to one tenant. Supplying both authentication schemes is
61
- * allowed only when they identify the same host; otherwise a caller could
62
- * accidentally combine credentials from two workstations and receive the
63
- * result selected by whichever branch happened to run first.
64
- */
65
- export function authorizedSubject(relay, req) {
66
- const hostIdHeader = req.headers['x-herdr-host-id'];
67
- const hostTokenHeader = req.headers['x-herdr-host-token'];
68
- const hasHostCredentials = hostIdHeader !== undefined || hostTokenHeader !== undefined;
69
- const hasBearer = req.headers.authorization !== undefined;
70
- const hostId = authorizedHost(relay, req);
71
- const device = authorizedDevice(relay, req);
72
- if (hasHostCredentials && !hostId)
73
- return null;
74
- if (hasBearer && !device)
75
- return null;
76
- if (hostId && device && hostId !== device.hostId)
77
- return null;
78
- if (hostId)
79
- return { kind: 'host', hostId };
80
- if (device)
81
- return { kind: 'device', hostId: device.hostId, deviceId: device.deviceId };
82
- return null;
83
- }
84
- /** Authenticate the operator of this relay, not a workstation or device. */
85
- export function authorizedAdmin(relay, req) {
86
- return tokenMatches(req.headers['x-relay-admin-token'], relay.adminToken);
87
- }
88
- /**
89
- * The left-most X-Forwarded-For entry, but only when `trustProxy` says a proxy
90
- * sits in front of the relay: otherwise any caller could write the header.
91
- */
92
- function forwardedFor(relay, req) {
93
- if (!relay.trustProxy)
94
- return null;
95
- const forwarded = req.headers['x-forwarded-for'];
96
- if (typeof forwarded !== 'string' || forwarded.length === 0)
97
- return null;
98
- return forwarded.split(',')[0].trim() || null;
99
- }
100
- /**
101
- * Rate-limit key for a request. Behind a TLS reverse proxy every connection
102
- * arrives from the proxy itself, so keying on the socket address would put
103
- * every device in the world into one bucket and let a single attacker lock
104
- * everyone out.
105
- */
106
- function rateLimitKey(relay, req) {
107
- return forwardedFor(relay, req) || req.socket.remoteAddress || 'unknown';
108
- }
109
- /** The address a window connected from, as the operator dashboard shows it. */
110
- export function clientAddress(relay, req) {
111
- return forwardedFor(relay, req) || req.socket.remoteAddress || null;
112
- }
113
- function allowAttempt(relay, store, req, limit = 20) {
114
- const key = rateLimitKey(relay, req);
115
- const now = Date.now();
116
- const current = store.get(key);
117
- if (!current || now - current.startedAt >= 60_000) {
118
- // Bound the map even when an attacker rotates source addresses. Expired
119
- // entries are removed by sweep; the oldest live entry is the least
120
- // useful one to retain when the cap is reached.
121
- if (store.size >= 4096) {
122
- const oldest = store.keys().next().value;
123
- if (oldest !== undefined)
124
- store.delete(oldest);
125
- }
126
- store.set(key, { startedAt: now, count: 1 });
127
- return true;
128
- }
129
- if (current.count >= limit)
130
- return false;
131
- current.count += 1;
132
- return true;
133
- }
134
- export function allowPairAttempt(relay, req) {
135
- return allowAttempt(relay, relay.pairAttempts, req, 20);
136
- }
137
- export function allowClientHandshake(relay, req) {
138
- return allowAttempt(relay, relay.clientHandshakeAttempts, req, 60);
139
- }
140
- export function allowHostHandshake(relay, req) {
141
- return allowAttempt(relay, relay.hostHandshakeAttempts, req, 60);
142
- }
@@ -1,36 +0,0 @@
1
- import type { RelayClient, RelayContext, RelayHost } from './types.js';
2
- /** Never shrink an individual session grid below something a program can still draw in. */
3
- export declare const MIN_SESSION_COLS = 20;
4
- export declare const MIN_SESSION_ROWS = 6;
5
- export interface DetachOptions {
6
- /** Tell the window why, before closing it. */
7
- notify?: boolean;
8
- reason?: string;
9
- closeCode?: number;
10
- /** Drop the socket at once instead of closing it politely. */
11
- terminate?: boolean;
12
- }
13
- export declare function allocateStreamIndex(host: RelayHost | null | undefined): number | null;
14
- /** Forget `client`'s stream: its id, its v2 index on `host`, and the session itself. */
15
- export declare function releaseSession(relay: RelayContext, host: RelayHost | null | undefined, client: RelayClient): void;
16
- /** Start a dedicated PTY session for one attached client. */
17
- export declare function startSession(relay: RelayContext, host: RelayHost, client: RelayClient | null | undefined, { restarted }?: {
18
- restarted?: boolean | undefined;
19
- }): void;
20
- /**
21
- * Tell every window who is attached.
22
- *
23
- * There is no controller to announce any more, so this carries the one fact
24
- * that changed: how many windows now share this terminal.
25
- */
26
- export declare function broadcastControlState(relay: RelayContext, host: RelayHost): void;
27
- /**
28
- * Close every window a revoked device still holds, on any host, so revoking
29
- * takes effect now rather than at its next reconnect. Returns how many.
30
- */
31
- export declare function detachDeviceSessions(relay: RelayContext, deviceId: string): number;
32
- export declare function detachClient(relay: RelayContext, client: RelayClient | null | undefined, { notify, reason, closeCode, terminate, }?: DetachOptions): void;
33
- export declare function detachHost(relay: RelayContext, host: RelayHost | null | undefined, { notify, reason, terminate }?: DetachOptions): void;
34
- /** Whether `host`'s windows can keep their sessions across a host reconnect. */
35
- export declare function canHandoffHost(relay: RelayContext, host: RelayHost | null | undefined): boolean;
36
- export declare function beginHostReconnect(relay: RelayContext, host: RelayHost | null | undefined, reason?: string): void;
@@ -1,230 +0,0 @@
1
- // A window's PTY session on its host, and taking windows and hosts off the
2
- // relay: detaching, host hand-off and the reconnect grace period.
3
- import { WebSocket } from 'ws';
4
- import { closeSocket, isOpen, jsonSend, randomId, terminateSocket } from './sockets.js';
5
- import { broadcastToClients, notifyHostClientCount } from './transport.js';
6
- /** Never shrink an individual session grid below something a program can still draw in. */
7
- export const MIN_SESSION_COLS = 20;
8
- export const MIN_SESSION_ROWS = 6;
9
- export function allocateStreamIndex(host) {
10
- if (!host)
11
- return null;
12
- const totalPossible = 65536;
13
- for (let i = 0; i < totalPossible; i++) {
14
- const candidate = host.nextStreamIndex;
15
- host.nextStreamIndex = (host.nextStreamIndex + 1) & 0xffff;
16
- if (!host.streamIndices.has(candidate)) {
17
- return candidate;
18
- }
19
- }
20
- return null;
21
- }
22
- /** Forget `client`'s stream: its id, its v2 index on `host`, and the session itself. */
23
- export function releaseSession(relay, host, client) {
24
- const session = client.session;
25
- if (!session)
26
- return;
27
- if (session.streamIndex !== null && session.streamIndex !== undefined && host) {
28
- host.streamIndices.delete(session.streamIndex);
29
- }
30
- relay.streams.delete(session.streamId);
31
- client.session = null;
32
- }
33
- /** Start a dedicated PTY session for one attached client. */
34
- export function startSession(relay, host, client, { restarted = false } = {}) {
35
- if (!client || !isOpen(host.ws))
36
- return;
37
- const streamId = randomId('session');
38
- let streamIndex = null;
39
- if (host.binaryFrameV2) {
40
- streamIndex = allocateStreamIndex(host);
41
- if (streamIndex !== null) {
42
- host.streamIndices.set(streamIndex, client.id);
43
- }
44
- }
45
- client.session = {
46
- streamId,
47
- streamIndex,
48
- cols: client.cols,
49
- rows: client.rows,
50
- ready: false,
51
- };
52
- relay.streams.set(streamId, client.id);
53
- jsonSend(host.ws, {
54
- type: 'session_start',
55
- clientId: streamId,
56
- streamId,
57
- cols: Math.max(MIN_SESSION_COLS, client.cols),
58
- rows: Math.max(MIN_SESSION_ROWS, client.rows),
59
- role: 'controller',
60
- ...(streamIndex !== null ? { streamIndex } : {}),
61
- });
62
- if (restarted) {
63
- jsonSend(client.ws, {
64
- type: 'session_restarted',
65
- streamId,
66
- cols: client.cols,
67
- rows: client.rows,
68
- hostname: host.hostname,
69
- platform: host.platform,
70
- ...(host.shellProfile ? { shellProfile: host.shellProfile } : {}),
71
- adminTerminalSupported: host.adminTabsAvailable,
72
- terminalPalette: host.terminalPalette || null,
73
- terminalFont: host.terminalFont || null,
74
- hostVersion: host.version,
75
- ...(host.herdrPrefixKeys ? { herdrPrefixKeys: host.herdrPrefixKeys } : {}),
76
- });
77
- }
78
- }
79
- /**
80
- * Tell every window who is attached.
81
- *
82
- * There is no controller to announce any more, so this carries the one fact
83
- * that changed: how many windows now share this terminal.
84
- */
85
- export function broadcastControlState(relay, host) {
86
- for (const clientId of host.clients) {
87
- const client = relay.clients.get(clientId);
88
- if (!client)
89
- continue;
90
- client.role = 'controller';
91
- client.controllerId = null;
92
- jsonSend(client.ws, {
93
- type: 'control_state',
94
- role: 'controller',
95
- controllerId: null,
96
- clientCount: host.clients.size,
97
- });
98
- }
99
- }
100
- /**
101
- * Close every window a revoked device still holds, on any host, so revoking
102
- * takes effect now rather than at its next reconnect. Returns how many.
103
- */
104
- export function detachDeviceSessions(relay, deviceId) {
105
- if (!deviceId)
106
- return 0;
107
- let closed = 0;
108
- for (const existing of [...relay.clients.values()]) {
109
- if (!existing || existing.deviceId !== deviceId)
110
- continue;
111
- detachClient(relay, existing, { notify: false, reason: 'device_revoked' });
112
- closeSocket(existing.ws, 1000, 'this device has been revoked by the relay operator');
113
- closed += 1;
114
- }
115
- return closed;
116
- }
117
- export function detachClient(relay, client, { notify = true, reason = 'client_disconnected', closeCode = 1000, terminate = false, } = {}) {
118
- if (!client || !relay.clients.has(client.id))
119
- return;
120
- relay.clients.delete(client.id);
121
- const host = relay.hosts.get(client.hostId);
122
- // Each window has its own PTY session. Tearing down the client immediately
123
- // stops its backing session on the host and cleans up its stream mapping.
124
- if (client.session) {
125
- const streamId = client.session.streamId;
126
- releaseSession(relay, host, client);
127
- if (host && isOpen(host.ws)) {
128
- jsonSend(host.ws, { type: 'session_stop', clientId: streamId, streamId });
129
- }
130
- }
131
- if (host) {
132
- host.clients.delete(client.id);
133
- notifyHostClientCount(host);
134
- broadcastControlState(relay, host);
135
- if (host.clients.size === 0) {
136
- host.controllerId = null;
137
- if (host.reconnecting)
138
- detachHost(relay, host, { notify: false, reason: 'no_clients' });
139
- }
140
- }
141
- const clientSocket = client.ws;
142
- client.ws = null;
143
- if (notify)
144
- jsonSend(clientSocket, {
145
- type: 'error',
146
- code: reason,
147
- message: reason === 'host_offline' ? 'Herdr host is offline' : 'connection closed',
148
- });
149
- if (terminate || clientSocket?.readyState === WebSocket.CLOSING) {
150
- terminateSocket(clientSocket);
151
- }
152
- else {
153
- closeSocket(clientSocket, closeCode, reason);
154
- }
155
- relay.metrics.recordCleanup('closedPtysCleaned');
156
- }
157
- export function detachHost(relay, host, { notify = true, reason = 'host_disconnected', terminate = false } = {}) {
158
- if (!host || relay.hosts.get(host.id) !== host)
159
- return;
160
- if (host.reconnectTimer)
161
- clearTimeout(host.reconnectTimer);
162
- host.reconnectTimer = null;
163
- relay.hosts.delete(host.id);
164
- for (const clientId of [...host.clients]) {
165
- const client = relay.clients.get(clientId);
166
- if (!client)
167
- continue;
168
- relay.clients.delete(client.id);
169
- releaseSession(relay, host, client);
170
- const clientSocket = client.ws;
171
- client.ws = null;
172
- if (notify)
173
- jsonSend(clientSocket, { type: 'error', code: reason, message: 'Herdr host disconnected' });
174
- if (terminate || clientSocket?.readyState === WebSocket.CLOSING) {
175
- terminateSocket(clientSocket);
176
- }
177
- else {
178
- closeSocket(clientSocket, 1012, reason);
179
- }
180
- }
181
- host.clients.clear();
182
- relay.metrics.forgetHost(host.id);
183
- const hostSocket = host.ws;
184
- host.ws = null;
185
- if (terminate || hostSocket?.readyState === WebSocket.CLOSING) {
186
- terminateSocket(hostSocket);
187
- }
188
- else {
189
- closeSocket(hostSocket, 1000, reason);
190
- }
191
- }
192
- /** Whether `host`'s windows can keep their sessions across a host reconnect. */
193
- export function canHandoffHost(relay, host) {
194
- if (!host?.handoffCapable || host.clients.size === 0)
195
- return false;
196
- for (const clientId of host.clients) {
197
- const client = relay.clients.get(clientId);
198
- if (!client?.handoffCapable)
199
- return false;
200
- }
201
- return true;
202
- }
203
- export function beginHostReconnect(relay, host, reason = 'host_disconnected') {
204
- if (!host || relay.hosts.get(host.id) !== host || host.reconnecting)
205
- return;
206
- if (!canHandoffHost(relay, host)) {
207
- detachHost(relay, host, { notify: true, reason });
208
- return;
209
- }
210
- host.ws = null;
211
- host.reconnecting = true;
212
- host.reconnectStartedAt = Date.now();
213
- host.lastSeenAt = Date.now();
214
- host.load = {};
215
- host.ptys = [];
216
- for (const clientId of host.clients) {
217
- const client = relay.clients.get(clientId);
218
- if (client)
219
- releaseSession(relay, host, client);
220
- }
221
- broadcastToClients(relay, host, () => ({ type: 'host_reconnecting', code: reason }));
222
- const timer = setTimeout(() => {
223
- host.reconnectTimer = null;
224
- if (relay.hosts.get(host.id) === host && host.reconnecting) {
225
- detachHost(relay, host, { notify: true, reason: 'host_reconnect_timeout' });
226
- }
227
- }, relay.config.relay.hostReconnectGraceMs);
228
- host.reconnectTimer = timer;
229
- timer.unref?.();
230
- }
@@ -1,12 +0,0 @@
1
- import type { RelaySocket } from './types.js';
2
- export declare function randomId(prefix: string): string;
3
- /** A bounded string, or null. For fields whose contents a pane decided. */
4
- export declare function text(value: unknown, limit: number): string | null;
5
- export declare function clampDimension(value: unknown, fallback: number): number;
6
- export declare function isOpen(socket: RelaySocket | null | undefined): socket is RelaySocket;
7
- export declare function jsonSend(socket: RelaySocket | null | undefined, payload: unknown): void;
8
- export declare function closeSocket(socket: RelaySocket | null | undefined, code?: number, reason?: string): void;
9
- export declare function terminateSocket(socket: RelaySocket | null | undefined): void;
10
- /** Parsed JSON, or null for anything that is not a string of valid JSON. */
11
- export declare function parseJson<T = Record<string, unknown>>(data: unknown): T | null;
12
- export declare function rejectHandshake(ws: RelaySocket, message: string, code?: string): void;
@@ -1,64 +0,0 @@
1
- // Small helpers for the relay's WebSockets and the untrusted data read from them.
2
- import crypto from 'node:crypto';
3
- import { WebSocket } from 'ws';
4
- const MAX_DIMENSION = 500;
5
- export function randomId(prefix) {
6
- return `${prefix}-${crypto.randomBytes(9).toString('base64url')}`;
7
- }
8
- /** A bounded string, or null. For fields whose contents a pane decided. */
9
- export function text(value, limit) {
10
- return typeof value === 'string' && value.length > 0 ? value.slice(0, limit) : null;
11
- }
12
- export function clampDimension(value, fallback) {
13
- const numeric = Number(value);
14
- if (!Number.isInteger(numeric))
15
- return fallback;
16
- return Math.min(MAX_DIMENSION, Math.max(2, numeric));
17
- }
18
- export function isOpen(socket) {
19
- return Boolean(socket) && socket?.readyState === WebSocket.OPEN;
20
- }
21
- export function jsonSend(socket, payload) {
22
- if (isOpen(socket))
23
- socket.send(JSON.stringify(payload));
24
- }
25
- export function closeSocket(socket, code = 1000, reason = '') {
26
- if (!socket || socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING)
27
- return;
28
- try {
29
- socket.close(code, reason.slice(0, 120));
30
- }
31
- catch {
32
- // Closing a socket that is already failing needs no further handling.
33
- }
34
- }
35
- export function terminateSocket(socket) {
36
- if (!socket || socket.readyState === WebSocket.CLOSED)
37
- return;
38
- try {
39
- if (typeof socket.terminate === 'function') {
40
- socket.terminate();
41
- }
42
- else if (typeof socket.destroy === 'function') {
43
- socket.destroy();
44
- }
45
- }
46
- catch {
47
- // Tearing down a dead socket; there is nothing left to recover.
48
- }
49
- }
50
- /** Parsed JSON, or null for anything that is not a string of valid JSON. */
51
- export function parseJson(data) {
52
- if (typeof data !== 'string')
53
- return null;
54
- try {
55
- return JSON.parse(data);
56
- }
57
- catch {
58
- return null;
59
- }
60
- }
61
- export function rejectHandshake(ws, message, code = 'invalid_handshake') {
62
- jsonSend(ws, { type: 'error', code, message });
63
- closeSocket(ws, 1008, message);
64
- }
@@ -1,11 +0,0 @@
1
- import type { AdminStatusResponse } from '../protocol/http.js';
2
- import type { RelayContext } from './types.js';
3
- export interface StatusOptions {
4
- /** Advance the throughput sample; off for reads that must not skew the rates. */
5
- sample?: boolean;
6
- /** Include the paired-device roster, which only the relay operator may see. */
7
- includeDevices?: boolean;
8
- /** Report only this workstation and its windows. */
9
- scopeHostId?: string | null;
10
- }
11
- export declare function statusSnapshot(relay: RelayContext, { sample, includeDevices, scopeHostId }?: StatusOptions): AdminStatusResponse;
@@ -1,99 +0,0 @@
1
- // What `/api/status` and `/api/admin/status` report: the relay's metrics plus
2
- // the hosts, windows and PTYs it can see, scoped to one tenant when asked.
3
- import { countActiveUsers } from '../metrics.js';
4
- export function statusSnapshot(relay, { sample = true, includeDevices = false, scopeHostId = null } = {}) {
5
- const scopedClients = scopeHostId
6
- ? [...relay.clients.values()].filter((client) => client.hostId === scopeHostId)
7
- : [...relay.clients.values()];
8
- const scopedHosts = scopeHostId
9
- ? [...relay.hosts.values()].filter((host) => host.id === scopeHostId)
10
- : [...relay.hosts.values()];
11
- const clients = scopedClients.map((client) => ({
12
- id: client.id,
13
- role: client.role,
14
- ...(scopeHostId ? {} : { hostId: client.hostId }),
15
- ...(includeDevices ? { deviceId: client.deviceId } : {}),
16
- userAgent: client.userAgent,
17
- connectedAt: client.connectedAt,
18
- lastPingAt: client.lastPingAt,
19
- bytesReceived: client.bytesReceived,
20
- bytesSent: client.bytesSent,
21
- ip: client.ip,
22
- }));
23
- // The roster is read once and used twice: the operator response carries it
24
- // whole, and every response carries the per-host tally derived from it. A
25
- // public caller learns how many devices a workstation has paired, never
26
- // which ones.
27
- const pairedDevices = relay.auth.listDevices();
28
- const pairedPerHost = new Map();
29
- for (const device of pairedDevices) {
30
- pairedPerHost.set(device.hostId, (pairedPerHost.get(device.hostId) || 0) + 1);
31
- }
32
- const hosts = scopedHosts.map((host) => ({
33
- id: host.id,
34
- hostname: host.hostname,
35
- platform: host.platform,
36
- arch: host.arch,
37
- status: host.reconnecting ? 'reconnecting' : host.clients.size ? 'busy' : 'online',
38
- connectedAt: host.connectedAt,
39
- activePtyCount: host.ptys.length,
40
- // Distinct devices attached to this workstation, not open sockets: a
41
- // phone with two tabs open is one device on the operator's board.
42
- connectedDeviceCount: countActiveUsers([...host.clients]
43
- .map((id) => relay.clients.get(id))
44
- .filter((client) => Boolean(client))),
45
- pairedDeviceCount: pairedPerHost.get(host.id) || 0,
46
- // A connector too old to name itself in its hello may still have said
47
- // which release it runs when it checked npm.
48
- version: host.version ?? host.updateStatus?.current ?? null,
49
- latestVersion: host.updateStatus?.updateAvailable ? host.updateStatus.latest : null,
50
- load: host.load,
51
- }));
52
- // The workstation counts one PTY per stream and cannot know how many
53
- // windows are watching it; the relay does, and that is the number an
54
- // operator needs when the session is shared. Scoped callers only receive
55
- // the PTYs belonging to their authenticated host.
56
- const ptys = scopedHosts.flatMap((host) => host.ptys.map((pty) => ({
57
- ...pty,
58
- ...(scopeHostId ? {} : { hostId: host.id }),
59
- activeClients: host.clients.size,
60
- })));
61
- // The paired-device roster identifies people's hardware, so it is served to
62
- // the relay operator only — never on /api/status, which any paired device
63
- // may read.
64
- const devices = includeDevices ? pairedDevices : undefined;
65
- const address = relay.address();
66
- return {
67
- ...relay.metrics.snapshot({
68
- clients,
69
- hosts,
70
- ptys,
71
- sample,
72
- scopeHostId,
73
- // Counted from the unredacted records: the mapped rows above only carry
74
- // `deviceId` for the operator, and a public caller must still see the
75
- // same number of users the operator does.
76
- activeUserCount: countActiveUsers(scopedClients),
77
- }),
78
- ...(devices ? { devices } : {}),
79
- relayMode: relay.relayMode,
80
- isRemoteRelay: relay.relayMode === 'remote',
81
- remoteAdminUrl: relay.relayMode === 'remote'
82
- ? `${String(relay.config.relay.publicUrl || '').replace(/\/+$/, '')}/admin`
83
- : undefined,
84
- relay: {
85
- mode: relay.relayMode,
86
- publicUrl: relay.config.relay.publicUrl,
87
- bind: relay.config.relay.host,
88
- port: (address && 'port' in address ? address.port : undefined) || relay.config.relay.port,
89
- maxClientsPerHost: relay.config.relay.maxClientsPerHost,
90
- maxHosts: relay.config.relay.maxHosts,
91
- maxPendingHandshakes: relay.config.relay.maxPendingHandshakes,
92
- maxBufferedBytesPerClient: relay.config.relay.maxBufferedBytesPerClient,
93
- hostReconnectGraceMs: relay.config.relay.hostReconnectGraceMs,
94
- adminConfigured: Boolean(relay.adminToken),
95
- adminStatusPath: '/api/admin/status',
96
- dashboardPath: '/admin',
97
- },
98
- };
99
- }
@@ -1,16 +0,0 @@
1
- import type { RelayClient, RelayContext, RelayHost, RelaySocket } from './types.js';
2
- /** Notify the host whether any browser currently needs business telemetry. */
3
- export declare function notifyHostClientCount(host: RelayHost | null | undefined): void;
4
- /**
5
- * Forward output without allowing one slow browser to grow an unbounded ws
6
- * queue. Closing only that browser preserves low latency for the other views.
7
- */
8
- export declare function sendClientBinary(relay: RelayContext, host: RelayHost, client: RelayClient | null | undefined, payload: Buffer): boolean;
9
- /**
10
- * Queue artificial delay per-socket rather than using naked setTimeout calls.
11
- * Concurrent timers experience event loop jitter that can deliver frames out of order;
12
- * a single FIFO queue per socket guarantees strict in-order delivery of terminal frames.
13
- */
14
- export declare function enqueueDelayedSend(relay: RelayContext, socket: RelaySocket, task: () => void): void;
15
- /** Send one JSON message to every browser attached to `host`. */
16
- export declare function broadcastToClients(relay: RelayContext, host: RelayHost, build: (client: RelayClient) => unknown): void;