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,41 +0,0 @@
1
- # System-wide unit for a self-hosted relay.
2
- #
3
- # sudo cp herdr-remote-relay.service /etc/systemd/system/
4
- # sudo useradd --system --home /var/lib/herdr-remote-relay --create-home herdr-relay
5
- # sudo install -m 600 -o herdr-relay -g herdr-relay relay.env /etc/herdr-remote-relay.env
6
- # sudo systemctl enable --now herdr-remote-relay
7
-
8
- [Unit]
9
- Description=Herdr Remote relay
10
- Documentation=https://github.com/herdr/herdr-remote
11
- After=network-online.target
12
- Wants=network-online.target
13
-
14
- [Service]
15
- Type=simple
16
- User=herdr-relay
17
- Group=herdr-relay
18
- # Tokens live here, not on the command line, where `ps` would expose them.
19
- EnvironmentFile=/etc/herdr-remote-relay.env
20
- ExecStart=/usr/bin/npx herdr-remote-relay
21
- Restart=always
22
- RestartSec=3
23
- StateDirectory=herdr-remote-relay
24
- WorkingDirectory=/var/lib/herdr-remote-relay
25
-
26
- # The relay reads no user files and runs no subprocesses; it only needs its own
27
- # state directory and a socket.
28
- NoNewPrivileges=true
29
- PrivateTmp=true
30
- ProtectSystem=strict
31
- ProtectHome=true
32
- ProtectKernelTunables=true
33
- ProtectKernelModules=true
34
- ProtectControlGroups=true
35
- RestrictAddressFamilies=AF_INET AF_INET6
36
- RestrictNamespaces=true
37
- LockPersonality=true
38
- MemoryDenyWriteExecute=false
39
-
40
- [Install]
41
- WantedBy=multi-user.target
@@ -1,25 +0,0 @@
1
- # Copy to /etc/herdr-remote-relay.env with mode 0600.
2
-
3
- RELAY_PUBLIC_URL=https://herdr.example.com
4
- # Standalone/self-hosted relay; the workstation-managed relay sets this to local.
5
- RELAY_DEPLOYMENT_MODE=remote
6
- RELAY_BIND=127.0.0.1
7
- RELAY_PORT=8787
8
- RELAY_MAX_HOSTS=1024
9
- RELAY_MAX_PENDING_HANDSHAKES=1024
10
- RELAY_MAX_CLIENTS_PER_HOST=16
11
- RELAY_MAX_BUFFERED_BYTES_PER_CLIENT=4194304
12
- RELAY_HOST_RECONNECT_GRACE_MS=30000
13
-
14
- # Password a workstation must present. Leave empty for a public relay.
15
- RELAY_PASSWORD=change-me
16
-
17
- # Credential for the relay operator dashboard at /admin. Keep this separate
18
- # from RELAY_PASSWORD; it is sent only in X-Relay-Admin-Token.
19
- RELAY_ADMIN_TOKEN=replace-with-a-long-random-operator-token
20
-
21
- # Set when a reverse proxy sits in front, so pairing rate limits key on the
22
- # real client address instead of the proxy's.
23
- RELAY_TRUST_PROXY=1
24
-
25
- RELAY_AUTH_STATE_FILE=/var/lib/herdr-remote-relay/relay-auth.json
@@ -1,114 +0,0 @@
1
- import type { PairedDeviceInfo } from './protocol/http.js';
2
- interface HostRecord {
3
- tokenHash: string;
4
- createdAt: string;
5
- lastSeenAt: string;
6
- }
7
- export interface DeviceRecord {
8
- deviceId: string;
9
- hostId: string;
10
- tokenHash: string;
11
- createdAt: string;
12
- lastSeenAt: string;
13
- expiresAt: number;
14
- userAgent?: string;
15
- lastIp?: string;
16
- }
17
- interface AuthState {
18
- version: 1;
19
- hosts: Record<string, HostRecord>;
20
- devices: Record<string, DeviceRecord>;
21
- }
22
- interface AuthStoreOptions {
23
- stateFile?: string;
24
- pairingTtlMs?: number;
25
- deviceTtlMs?: number;
26
- maxDevices?: number;
27
- password?: string | null;
28
- }
29
- type RegisterHostResult = {
30
- ok: true;
31
- hostId: string;
32
- firstSeen: boolean;
33
- } | {
34
- ok: false;
35
- code: 'invalid_host_credentials' | 'relay_password_required' | 'host_auth_failed';
36
- message: string;
37
- };
38
- interface PairingStart {
39
- code: string;
40
- hostId: string;
41
- publicUrl: string;
42
- expiresAt: number;
43
- expiresAtIso: string;
44
- }
45
- interface PairingResult {
46
- deviceId: string;
47
- hostId: string;
48
- token: string;
49
- expiresAt: number;
50
- expiresAtIso: string;
51
- publicUrl: string;
52
- }
53
- interface RevokedDevice {
54
- deviceId: string;
55
- hostId: string;
56
- userAgent: string | null;
57
- lastIp: string | null;
58
- }
59
- declare class AuthStore {
60
- readonly stateFile: string;
61
- readonly pairingTtlMs: number;
62
- readonly deviceTtlMs: number;
63
- readonly maxDevices: number;
64
- readonly password: string | null;
65
- private readonly pairings;
66
- private readonly lastDeviceSaveAt;
67
- state: AuthState;
68
- constructor({ stateFile, pairingTtlMs, deviceTtlMs, maxDevices, password, }?: AuthStoreOptions);
69
- save(): void;
70
- /** Does this request carry the relay password, if one is required at all? */
71
- checkPassword(supplied: unknown): boolean;
72
- /**
73
- * Enrol a workstation, or re-authenticate one that is already enrolled.
74
- *
75
- * The password (when the relay has one) decides who may *join*. The host
76
- * token decides who may act *as a given workstation*: it is generated on the
77
- * workstation and only its hash is ever stored here, so even on a public
78
- * relay nobody else can impersonate an enrolled host or pair a device to it.
79
- */
80
- registerHost(hostId: unknown, token: unknown, password?: unknown, now?: number): RegisterHostResult;
81
- /** Verify a host token without enrolling anything. */
82
- authenticateHost(hostId: unknown, token: unknown): boolean;
83
- hostCount(): number;
84
- startPairing(hostId: unknown, publicUrl: string, now?: number): PairingStart;
85
- completePairing(code: unknown, now?: number): PairingResult | null;
86
- /**
87
- * Record how a device presented itself, so an operator revoking a device can
88
- * tell which phone or laptop they are about to cut off. Only the coarse
89
- * user agent string and the address are kept — never terminal content.
90
- */
91
- noteDeviceSeen(deviceId: string, { userAgent, ip }?: {
92
- userAgent?: string | null;
93
- ip?: string | null;
94
- }, now?: number): DeviceRecord | null;
95
- /**
96
- * Devices an operator may act on, newest first. Token hashes are never
97
- * included: the dashboard has no use for them and they must not leave the
98
- * process.
99
- */
100
- listDevices(now?: number): PairedDeviceInfo[];
101
- /**
102
- * Permanently invalidate a paired device. The stored hash is dropped, so the
103
- * token it was derived from can never authenticate again. Returns the removed
104
- * record so the caller can also close whatever sockets it still holds.
105
- */
106
- revokeDevice(deviceId: unknown): RevokedDevice | null;
107
- authenticateDevice(token: unknown, now?: number): DeviceRecord | null;
108
- cleanup(now?: number): {
109
- removedDevices: number;
110
- removedPairings: number;
111
- };
112
- deviceCount(): number;
113
- }
114
- export { AuthStore };
@@ -1,289 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import path from 'node:path';
3
- import { randomToken, readJson, writeJsonAtomic, ensureDir } from './state.js';
4
- function hash(value) {
5
- return crypto.createHash('sha256').update(String(value), 'utf8').digest('hex');
6
- }
7
- function equalHash(left, right) {
8
- if (typeof left !== 'string' || typeof right !== 'string')
9
- return false;
10
- const a = Buffer.from(left, 'hex');
11
- const b = Buffer.from(right, 'hex');
12
- return a.length === b.length && a.length > 0 && crypto.timingSafeEqual(a, b);
13
- }
14
- function nowIso(now = Date.now()) {
15
- return new Date(now).toISOString();
16
- }
17
- function validHostId(value) {
18
- return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
19
- }
20
- /**
21
- * What a person reads off one screen and types on another: six characters,
22
- * without 0/O or 1/I/L to confuse. Cutting base64 and dropping its `-` and `_`
23
- * made one code in seven shorter than six, and some too short to pair at all.
24
- */
25
- const PAIR_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
26
- const PAIR_CODE_LENGTH = 6;
27
- function newPairCode() {
28
- let code = '';
29
- for (let i = 0; i < PAIR_CODE_LENGTH; i += 1)
30
- code += PAIR_CODE_ALPHABET[crypto.randomInt(PAIR_CODE_ALPHABET.length)];
31
- return code;
32
- }
33
- class AuthStore {
34
- stateFile;
35
- pairingTtlMs;
36
- deviceTtlMs;
37
- maxDevices;
38
- password;
39
- pairings = new Map();
40
- lastDeviceSaveAt = new Map();
41
- state;
42
- constructor({ stateFile, pairingTtlMs = 10 * 60 * 1000, deviceTtlMs = 30 * 24 * 60 * 60 * 1000, maxDevices = 32, password = null, } = {}) {
43
- if (!stateFile)
44
- throw new TypeError('stateFile is required');
45
- this.stateFile = stateFile;
46
- this.pairingTtlMs = pairingTtlMs;
47
- this.deviceTtlMs = deviceTtlMs;
48
- this.maxDevices = maxDevices;
49
- // Optional shared password. When unset the relay is public: anyone may
50
- // enrol a workstation. That is safe because a workstation is only ever
51
- // reachable through its own host token, which the relay never hands out.
52
- this.password = password || null;
53
- this.state = readJson(stateFile, { version: 1, hosts: {}, devices: {} });
54
- this.state.version = 1;
55
- this.state.hosts =
56
- this.state.hosts && typeof this.state.hosts === 'object' ? this.state.hosts : {};
57
- this.state.devices =
58
- this.state.devices && typeof this.state.devices === 'object' ? this.state.devices : {};
59
- ensureDir(path.dirname(stateFile));
60
- }
61
- save() {
62
- writeJsonAtomic(this.stateFile, this.state);
63
- }
64
- /** Does this request carry the relay password, if one is required at all? */
65
- checkPassword(supplied) {
66
- if (!this.password)
67
- return true;
68
- return typeof supplied === 'string' && equalHash(hash(supplied), hash(this.password));
69
- }
70
- /**
71
- * Enrol a workstation, or re-authenticate one that is already enrolled.
72
- *
73
- * The password (when the relay has one) decides who may *join*. The host
74
- * token decides who may act *as a given workstation*: it is generated on the
75
- * workstation and only its hash is ever stored here, so even on a public
76
- * relay nobody else can impersonate an enrolled host or pair a device to it.
77
- */
78
- registerHost(hostId, token, password = null, now = Date.now()) {
79
- if (!validHostId(hostId) ||
80
- typeof token !== 'string' ||
81
- token.length < 16 ||
82
- token.length > 4096) {
83
- return {
84
- ok: false,
85
- code: 'invalid_host_credentials',
86
- message: 'hostId and token are required',
87
- };
88
- }
89
- if (!this.checkPassword(password)) {
90
- return {
91
- ok: false,
92
- code: 'relay_password_required',
93
- message: 'the relay password is missing or wrong',
94
- };
95
- }
96
- const existing = this.state.hosts[hostId];
97
- if (existing) {
98
- if (!equalHash(existing.tokenHash, hash(token))) {
99
- return { ok: false, code: 'host_auth_failed', message: 'host token is invalid' };
100
- }
101
- existing.lastSeenAt = nowIso(now);
102
- this.save();
103
- return { ok: true, hostId, firstSeen: false };
104
- }
105
- this.state.hosts[hostId] = {
106
- tokenHash: hash(token),
107
- createdAt: nowIso(now),
108
- lastSeenAt: nowIso(now),
109
- };
110
- this.save();
111
- return { ok: true, hostId, firstSeen: true };
112
- }
113
- /** Verify a host token without enrolling anything. */
114
- authenticateHost(hostId, token) {
115
- if (!validHostId(hostId) ||
116
- typeof token !== 'string' ||
117
- token.length < 16 ||
118
- token.length > 4096)
119
- return false;
120
- const existing = this.state.hosts[hostId];
121
- return Boolean(existing) && equalHash(existing.tokenHash, hash(token));
122
- }
123
- hostCount() {
124
- return Object.keys(this.state.hosts).length;
125
- }
126
- startPairing(hostId, publicUrl, now = Date.now()) {
127
- if (!validHostId(hostId) || !this.state.hosts[hostId]) {
128
- throw Object.assign(new Error('host is not connected or enrolled'), {
129
- code: 'host_not_found',
130
- });
131
- }
132
- this.cleanup(now);
133
- let code;
134
- do {
135
- code = newPairCode();
136
- } while ([...this.pairings.values()].some((pairing) => equalHash(pairing.codeHash, hash(code))));
137
- const expiresAt = now + this.pairingTtlMs;
138
- this.pairings.set(code, {
139
- codeHash: hash(code),
140
- hostId,
141
- expiresAt,
142
- publicUrl,
143
- });
144
- return { code, hostId, publicUrl, expiresAt, expiresAtIso: nowIso(expiresAt) };
145
- }
146
- completePairing(code, now = Date.now()) {
147
- if (typeof code !== 'string' || code.length < 4 || code.length > 32)
148
- return null;
149
- this.cleanup(now);
150
- const normalized = code.trim().toUpperCase();
151
- const pairing = this.pairings.get(normalized);
152
- if (!pairing || pairing.expiresAt <= now || !equalHash(pairing.codeHash, hash(normalized)))
153
- return null;
154
- this.pairings.delete(normalized);
155
- const devices = Object.values(this.state.devices);
156
- if (devices.length >= this.maxDevices) {
157
- devices.sort((a, b) => (a.lastSeenAt || '').localeCompare(b.lastSeenAt || ''));
158
- delete this.state.devices[devices[0].deviceId];
159
- }
160
- const deviceId = `device-${randomToken(9)}`;
161
- const token = randomToken(32);
162
- const expiresAt = now + this.deviceTtlMs;
163
- this.state.devices[deviceId] = {
164
- deviceId,
165
- hostId: pairing.hostId,
166
- tokenHash: hash(token),
167
- createdAt: nowIso(now),
168
- lastSeenAt: nowIso(now),
169
- expiresAt,
170
- };
171
- this.save();
172
- return {
173
- deviceId,
174
- hostId: pairing.hostId,
175
- token,
176
- expiresAt,
177
- expiresAtIso: nowIso(expiresAt),
178
- publicUrl: pairing.publicUrl,
179
- };
180
- }
181
- /**
182
- * Record how a device presented itself, so an operator revoking a device can
183
- * tell which phone or laptop they are about to cut off. Only the coarse
184
- * user agent string and the address are kept — never terminal content.
185
- */
186
- noteDeviceSeen(deviceId, { userAgent, ip } = {}, now = Date.now()) {
187
- const device = this.state.devices[deviceId];
188
- if (!device)
189
- return null;
190
- let changed = false;
191
- if (userAgent && device.userAgent !== userAgent) {
192
- device.userAgent = String(userAgent).slice(0, 256);
193
- changed = true;
194
- }
195
- if (ip && device.lastIp !== ip) {
196
- device.lastIp = String(ip).slice(0, 64);
197
- changed = true;
198
- }
199
- device.lastSeenAt = nowIso(now);
200
- if (changed)
201
- this.save();
202
- return { ...device };
203
- }
204
- /**
205
- * Devices an operator may act on, newest first. Token hashes are never
206
- * included: the dashboard has no use for them and they must not leave the
207
- * process.
208
- */
209
- listDevices(now = Date.now()) {
210
- return Object.values(this.state.devices)
211
- .filter((device) => device && device.expiresAt > now)
212
- .map((device) => ({
213
- deviceId: device.deviceId,
214
- hostId: device.hostId,
215
- createdAt: device.createdAt,
216
- lastSeenAt: device.lastSeenAt,
217
- expiresAt: device.expiresAt,
218
- expiresAtIso: nowIso(device.expiresAt),
219
- userAgent: device.userAgent || null,
220
- lastIp: device.lastIp || null,
221
- }))
222
- .sort((a, b) => String(b.lastSeenAt || '').localeCompare(String(a.lastSeenAt || '')));
223
- }
224
- /**
225
- * Permanently invalidate a paired device. The stored hash is dropped, so the
226
- * token it was derived from can never authenticate again. Returns the removed
227
- * record so the caller can also close whatever sockets it still holds.
228
- */
229
- revokeDevice(deviceId) {
230
- if (typeof deviceId !== 'string' || !deviceId)
231
- return null;
232
- const device = this.state.devices[deviceId];
233
- if (!device)
234
- return null;
235
- delete this.state.devices[deviceId];
236
- this.lastDeviceSaveAt.delete(deviceId);
237
- this.save();
238
- return {
239
- deviceId: device.deviceId,
240
- hostId: device.hostId,
241
- userAgent: device.userAgent || null,
242
- lastIp: device.lastIp || null,
243
- };
244
- }
245
- authenticateDevice(token, now = Date.now()) {
246
- if (typeof token !== 'string' || token.length < 16)
247
- return null;
248
- const tokenHash = hash(token);
249
- for (const device of Object.values(this.state.devices)) {
250
- if (device.expiresAt <= now)
251
- continue;
252
- if (!equalHash(device.tokenHash, tokenHash))
253
- continue;
254
- device.lastSeenAt = nowIso(now);
255
- device.expiresAt = now + this.deviceTtlMs;
256
- const lastSaveAt = this.lastDeviceSaveAt.get(device.deviceId) || 0;
257
- if (now - lastSaveAt >= 60 * 1000) {
258
- this.lastDeviceSaveAt.set(device.deviceId, now);
259
- this.save();
260
- }
261
- return { ...device };
262
- }
263
- return null;
264
- }
265
- cleanup(now = Date.now()) {
266
- let removedDevices = 0;
267
- for (const [deviceId, device] of Object.entries(this.state.devices)) {
268
- if (!device || device.expiresAt <= now) {
269
- delete this.state.devices[deviceId];
270
- this.lastDeviceSaveAt.delete(deviceId);
271
- removedDevices += 1;
272
- }
273
- }
274
- let removedPairings = 0;
275
- for (const [code, pairing] of this.pairings.entries()) {
276
- if (pairing.expiresAt <= now) {
277
- this.pairings.delete(code);
278
- removedPairings += 1;
279
- }
280
- }
281
- if (removedDevices)
282
- this.save();
283
- return { removedDevices, removedPairings };
284
- }
285
- deviceCount() {
286
- return Object.keys(this.state.devices).length;
287
- }
288
- }
289
- export { AuthStore };
@@ -1,76 +0,0 @@
1
- import type { AdminStatusResponse, CleanupCounters, ConnectedClientInfo, EventLoopDelayMetrics, HostInfo, PtyInfo, ThroughputMetrics } from './protocol/http.js';
2
- /** Cleanup counters that `recordCleanup` may increment. */
3
- type CleanupCounterName = Exclude<keyof CleanupCounters, 'lastCleanupAt'>;
4
- interface HostTrafficCounter {
5
- bytesIn: number;
6
- bytesOut: number;
7
- framesIn: number;
8
- framesOut: number;
9
- sampleAt: number;
10
- sampleBytesIn: number;
11
- sampleBytesOut: number;
12
- sampleFramesIn: number;
13
- sampleFramesOut: number;
14
- }
15
- /** A connection as `countActiveUsers` sees it. */
16
- interface ClientIdentity {
17
- id: string;
18
- deviceId?: string | null;
19
- }
20
- interface SnapshotOptions {
21
- clients?: ConnectedClientInfo[];
22
- hosts?: HostInfo[];
23
- ptys?: PtyInfo[];
24
- sample?: boolean;
25
- scopeHostId?: string | null;
26
- activeUserCount?: number | null;
27
- }
28
- /**
29
- * How many people are attached, rather than how many sockets are open.
30
- *
31
- * A phone and a laptop belonging to one person are two connections but one
32
- * paired device each, and one person opening three browser windows is three
33
- * connections and one device — so the distinct paired device is the closest
34
- * thing the relay has to a user. Connections that predate device pairing carry
35
- * no `deviceId`; they fall back to their own connection id so they still count
36
- * once instead of collapsing into a single anonymous user.
37
- */
38
- declare function countActiveUsers(clients?: readonly (ClientIdentity | null | undefined)[]): number;
39
- declare class RelayMetrics {
40
- readonly version: string;
41
- readonly protocolVersion: number;
42
- readonly startedAt: number;
43
- bytesIn: number;
44
- bytesOut: number;
45
- framesIn: number;
46
- framesOut: number;
47
- cleanup: Required<Omit<CleanupCounters, 'lastCleanupAt'>> & {
48
- lastCleanupAt: string | null;
49
- };
50
- private lastSample;
51
- private readonly hostTraffic;
52
- private cpuSampleAt;
53
- private cpuSample;
54
- private readonly eventLoop;
55
- constructor({ version, protocolVersion }?: {
56
- version?: string | undefined;
57
- protocolVersion?: number | undefined;
58
- });
59
- hostCounter(hostId: string | null | undefined): HostTrafficCounter | null;
60
- recordIn(bytes: number, hostId?: string | null): void;
61
- recordOut(bytes: number, hostId?: string | null): void;
62
- forgetHost(hostId: string | null | undefined): void;
63
- hostThroughput(hostId: string | null | undefined, now?: number): ThroughputMetrics;
64
- recordCleanup(name: CleanupCounterName, amount?: number): void;
65
- cpuPercent(): number;
66
- eventLoopDelay(): EventLoopDelayMetrics;
67
- /**
68
- * `activeUserCount` is passed in rather than derived from `clients`: the
69
- * public `/api/status` rows deliberately omit `deviceId`, so only the caller
70
- * still holding the full connection records can count devices correctly.
71
- * Omitting it falls back to counting whatever the given rows identify.
72
- */
73
- snapshot({ clients, hosts, ptys, sample, scopeHostId, activeUserCount, }?: SnapshotOptions): AdminStatusResponse;
74
- close(): void;
75
- }
76
- export { RelayMetrics, countActiveUsers };