@blue-soda/dsh-remote 0.5.6 → 0.5.8

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 (232) hide show
  1. package/README.md +14 -3
  2. package/cordis.patch.yml +30 -30
  3. package/dist/client.github.js +1 -1
  4. package/dist/client.js +1 -1
  5. package/dist/index.js +1216 -396
  6. package/dist/index.js.map +4 -4
  7. package/dsh-plugin.json +41 -41
  8. package/package.json +165 -165
  9. package/dist/acp.d.ts +0 -75
  10. package/dist/acp.d.ts.map +0 -1
  11. package/dist/acp.js +0 -61
  12. package/dist/acp.js.map +0 -1
  13. package/dist/api-proxy-switch.d.ts +0 -33
  14. package/dist/api-proxy-switch.d.ts.map +0 -1
  15. package/dist/api-proxy-switch.js +0 -109
  16. package/dist/api-proxy-switch.js.map +0 -1
  17. package/dist/atomic-file.d.ts +0 -16
  18. package/dist/atomic-file.d.ts.map +0 -1
  19. package/dist/atomic-file.js +0 -81
  20. package/dist/atomic-file.js.map +0 -1
  21. package/dist/bin/doctor.d.ts +0 -3
  22. package/dist/bin/doctor.d.ts.map +0 -1
  23. package/dist/bin/doctor.js +0 -13
  24. package/dist/bin/doctor.js.map +0 -1
  25. package/dist/cli.d.ts +0 -32
  26. package/dist/cli.d.ts.map +0 -1
  27. package/dist/cli.js +0 -289
  28. package/dist/cli.js.map +0 -1
  29. package/dist/client-runtime.d.ts +0 -375
  30. package/dist/client-runtime.d.ts.map +0 -1
  31. package/dist/client-runtime.js +0 -1768
  32. package/dist/client-runtime.js.map +0 -1
  33. package/dist/client-secure-transport.d.ts +0 -23
  34. package/dist/client-secure-transport.d.ts.map +0 -1
  35. package/dist/client-secure-transport.js +0 -144
  36. package/dist/client-secure-transport.js.map +0 -1
  37. package/dist/client-target-store.d.ts +0 -40
  38. package/dist/client-target-store.d.ts.map +0 -1
  39. package/dist/client-target-store.js +0 -79
  40. package/dist/client-target-store.js.map +0 -1
  41. package/dist/client.d.ts +0 -13
  42. package/dist/client.d.ts.map +0 -1
  43. package/dist/codex/app-server.d.ts +0 -71
  44. package/dist/codex/app-server.d.ts.map +0 -1
  45. package/dist/codex/app-server.js +0 -268
  46. package/dist/codex/app-server.js.map +0 -1
  47. package/dist/codex/codex-image-codec.d.ts +0 -7
  48. package/dist/codex/codex-image-codec.d.ts.map +0 -1
  49. package/dist/codex/codex-image-codec.js +0 -288
  50. package/dist/codex/codex-image-codec.js.map +0 -1
  51. package/dist/codex/domain.d.ts +0 -102
  52. package/dist/codex/domain.d.ts.map +0 -1
  53. package/dist/codex/domain.js +0 -1323
  54. package/dist/codex/domain.js.map +0 -1
  55. package/dist/codex/method-policy.d.ts +0 -338
  56. package/dist/codex/method-policy.d.ts.map +0 -1
  57. package/dist/codex/method-policy.js +0 -141
  58. package/dist/codex/method-policy.js.map +0 -1
  59. package/dist/codex/peer-bridge.d.ts +0 -57
  60. package/dist/codex/peer-bridge.d.ts.map +0 -1
  61. package/dist/codex/peer-bridge.js +0 -299
  62. package/dist/codex/peer-bridge.js.map +0 -1
  63. package/dist/codex/permissions.d.ts +0 -4
  64. package/dist/codex/permissions.d.ts.map +0 -1
  65. package/dist/codex/permissions.js +0 -21
  66. package/dist/codex/permissions.js.map +0 -1
  67. package/dist/codex/session-id.d.ts +0 -10
  68. package/dist/codex/session-id.d.ts.map +0 -1
  69. package/dist/codex/session-id.js +0 -15
  70. package/dist/codex/session-id.js.map +0 -1
  71. package/dist/codex/virtual-harness.d.ts +0 -209
  72. package/dist/codex/virtual-harness.d.ts.map +0 -1
  73. package/dist/codex/virtual-harness.js +0 -2650
  74. package/dist/codex/virtual-harness.js.map +0 -1
  75. package/dist/codex-workspace-bridge.d.ts +0 -108
  76. package/dist/codex-workspace-bridge.d.ts.map +0 -1
  77. package/dist/codex-workspace-bridge.js +0 -502
  78. package/dist/codex-workspace-bridge.js.map +0 -1
  79. package/dist/config.d.ts +0 -99
  80. package/dist/config.d.ts.map +0 -1
  81. package/dist/config.js +0 -127
  82. package/dist/config.js.map +0 -1
  83. package/dist/connection-controller.d.ts +0 -44
  84. package/dist/connection-controller.d.ts.map +0 -1
  85. package/dist/connection-controller.js +0 -185
  86. package/dist/connection-controller.js.map +0 -1
  87. package/dist/control-route.d.ts +0 -34
  88. package/dist/control-route.d.ts.map +0 -1
  89. package/dist/control-route.js +0 -136
  90. package/dist/control-route.js.map +0 -1
  91. package/dist/control-runtime.d.ts +0 -138
  92. package/dist/control-runtime.d.ts.map +0 -1
  93. package/dist/control-runtime.js +0 -543
  94. package/dist/control-runtime.js.map +0 -1
  95. package/dist/control-stream.d.ts +0 -67
  96. package/dist/control-stream.d.ts.map +0 -1
  97. package/dist/control-stream.js +0 -179
  98. package/dist/control-stream.js.map +0 -1
  99. package/dist/defaults.d.ts +0 -9
  100. package/dist/defaults.d.ts.map +0 -1
  101. package/dist/defaults.js +0 -9
  102. package/dist/defaults.js.map +0 -1
  103. package/dist/file-viewer-bridge.d.ts +0 -30
  104. package/dist/file-viewer-bridge.d.ts.map +0 -1
  105. package/dist/file-viewer-bridge.js +0 -128
  106. package/dist/file-viewer-bridge.js.map +0 -1
  107. package/dist/file-viewer-contract.d.ts +0 -5
  108. package/dist/file-viewer-contract.d.ts.map +0 -1
  109. package/dist/file-viewer-contract.js +0 -4
  110. package/dist/file-viewer-contract.js.map +0 -1
  111. package/dist/harness-api-bridge.d.ts +0 -81
  112. package/dist/harness-api-bridge.d.ts.map +0 -1
  113. package/dist/harness-api-bridge.js +0 -862
  114. package/dist/harness-api-bridge.js.map +0 -1
  115. package/dist/harness-api-history.d.ts +0 -3
  116. package/dist/harness-api-history.d.ts.map +0 -1
  117. package/dist/harness-api-history.js +0 -59
  118. package/dist/harness-api-history.js.map +0 -1
  119. package/dist/harness-remote-bridge.d.ts +0 -60
  120. package/dist/harness-remote-bridge.d.ts.map +0 -1
  121. package/dist/harness-remote-bridge.js +0 -540
  122. package/dist/harness-remote-bridge.js.map +0 -1
  123. package/dist/harness-version.d.ts +0 -23
  124. package/dist/harness-version.d.ts.map +0 -1
  125. package/dist/harness-version.js +0 -95
  126. package/dist/harness-version.js.map +0 -1
  127. package/dist/identity-store.d.ts +0 -45
  128. package/dist/identity-store.d.ts.map +0 -1
  129. package/dist/identity-store.js +0 -186
  130. package/dist/identity-store.js.map +0 -1
  131. package/dist/ids.d.ts +0 -2
  132. package/dist/ids.d.ts.map +0 -1
  133. package/dist/ids.js +0 -17
  134. package/dist/ids.js.map +0 -1
  135. package/dist/index.d.ts +0 -78
  136. package/dist/index.d.ts.map +0 -1
  137. package/dist/logging.d.ts +0 -18
  138. package/dist/logging.d.ts.map +0 -1
  139. package/dist/logging.js +0 -30
  140. package/dist/logging.js.map +0 -1
  141. package/dist/loopback-host.d.ts +0 -20
  142. package/dist/loopback-host.d.ts.map +0 -1
  143. package/dist/loopback-host.js +0 -245
  144. package/dist/loopback-host.js.map +0 -1
  145. package/dist/loopback-preview.d.ts +0 -19
  146. package/dist/loopback-preview.d.ts.map +0 -1
  147. package/dist/loopback-preview.js +0 -232
  148. package/dist/loopback-preview.js.map +0 -1
  149. package/dist/native-rtc-helper.d.ts +0 -14
  150. package/dist/native-rtc-helper.d.ts.map +0 -1
  151. package/dist/native-rtc-helper.js +0 -575
  152. package/dist/native-rtc-helper.js.map +0 -1
  153. package/dist/remote-api-proxy.d.ts +0 -17
  154. package/dist/remote-api-proxy.d.ts.map +0 -1
  155. package/dist/remote-api-proxy.js +0 -451
  156. package/dist/remote-api-proxy.js.map +0 -1
  157. package/dist/remote-directory-browser.d.ts +0 -16
  158. package/dist/remote-directory-browser.d.ts.map +0 -1
  159. package/dist/remote-directory-browser.js +0 -48
  160. package/dist/remote-directory-browser.js.map +0 -1
  161. package/dist/remote-file-content-provider.d.ts +0 -48
  162. package/dist/remote-file-content-provider.d.ts.map +0 -1
  163. package/dist/remote-file-content-provider.js +0 -93
  164. package/dist/remote-file-content-provider.js.map +0 -1
  165. package/dist/remote-typert-gateway.d.ts +0 -20
  166. package/dist/remote-typert-gateway.d.ts.map +0 -1
  167. package/dist/remote-typert-gateway.js +0 -334
  168. package/dist/remote-typert-gateway.js.map +0 -1
  169. package/dist/rpc-binary-attachments.d.ts +0 -33
  170. package/dist/rpc-binary-attachments.d.ts.map +0 -1
  171. package/dist/rpc-binary-attachments.js +0 -142
  172. package/dist/rpc-binary-attachments.js.map +0 -1
  173. package/dist/rpc-router.d.ts +0 -31
  174. package/dist/rpc-router.d.ts.map +0 -1
  175. package/dist/rpc-router.js +0 -201
  176. package/dist/rpc-router.js.map +0 -1
  177. package/dist/safe-error.d.ts +0 -8
  178. package/dist/safe-error.d.ts.map +0 -1
  179. package/dist/safe-error.js +0 -20
  180. package/dist/safe-error.js.map +0 -1
  181. package/dist/server-api.d.ts +0 -109
  182. package/dist/server-api.d.ts.map +0 -1
  183. package/dist/server-api.js +0 -520
  184. package/dist/server-api.js.map +0 -1
  185. package/dist/server-connection.d.ts +0 -79
  186. package/dist/server-connection.d.ts.map +0 -1
  187. package/dist/server-connection.js +0 -1023
  188. package/dist/server-connection.js.map +0 -1
  189. package/dist/server-credentials.d.ts +0 -55
  190. package/dist/server-credentials.d.ts.map +0 -1
  191. package/dist/server-credentials.js +0 -108
  192. package/dist/server-credentials.js.map +0 -1
  193. package/dist/service.d.ts +0 -151
  194. package/dist/service.d.ts.map +0 -1
  195. package/dist/service.js +0 -448
  196. package/dist/service.js.map +0 -1
  197. package/dist/session-format-compat.d.ts +0 -3
  198. package/dist/session-format-compat.d.ts.map +0 -1
  199. package/dist/session-format-compat.js +0 -183
  200. package/dist/session-format-compat.js.map +0 -1
  201. package/dist/status-stream.d.ts +0 -48
  202. package/dist/status-stream.d.ts.map +0 -1
  203. package/dist/status-stream.js +0 -133
  204. package/dist/status-stream.js.map +0 -1
  205. package/dist/terminal-policy.d.ts +0 -21
  206. package/dist/terminal-policy.d.ts.map +0 -1
  207. package/dist/terminal-policy.js +0 -68
  208. package/dist/terminal-policy.js.map +0 -1
  209. package/dist/tui-command.d.ts +0 -13
  210. package/dist/tui-command.d.ts.map +0 -1
  211. package/dist/tui-command.js +0 -392
  212. package/dist/tui-command.js.map +0 -1
  213. package/dist/typert-gateway-contract.d.ts +0 -59
  214. package/dist/typert-gateway-contract.d.ts.map +0 -1
  215. package/dist/typert-gateway-contract.js +0 -2
  216. package/dist/typert-gateway-contract.js.map +0 -1
  217. package/dist/typert-gateway-switch.d.ts +0 -85
  218. package/dist/typert-gateway-switch.d.ts.map +0 -1
  219. package/dist/typert-gateway-switch.js +0 -448
  220. package/dist/typert-gateway-switch.js.map +0 -1
  221. package/dist/types.d.ts +0 -20
  222. package/dist/types.d.ts.map +0 -1
  223. package/dist/types.js +0 -2
  224. package/dist/types.js.map +0 -1
  225. package/dist/version.d.ts +0 -12
  226. package/dist/version.d.ts.map +0 -1
  227. package/dist/version.js +0 -12
  228. package/dist/version.js.map +0 -1
  229. package/dist/werift-rtc.d.ts +0 -121
  230. package/dist/werift-rtc.d.ts.map +0 -1
  231. package/dist/werift-rtc.js +0 -490
  232. package/dist/werift-rtc.js.map +0 -1
@@ -1,1023 +0,0 @@
1
- import { waitForRelayCapacity, SerialSend } from '@dsh-remote/webrtc';
2
- import { NoiseIkSession, createNoisePrologue, fromBase64Url, toBase64Url, } from '@dsh-remote/crypto';
3
- import { PROTOCOL_VERSION, SecureMessageCodec, acceptNegotiatedCapabilities, createControlFrame, decodeControlFrame, decodeMessage, encodeControlFrame, encodeMessage, } from '@dsh-remote/protocol';
4
- import { RtcDataChannelTransport, stunOnlyIceServers, } from '@dsh-remote/webrtc';
5
- import { ServerApiError } from './server-api.js';
6
- import { PLUGIN_VERSION } from './version.js';
7
- const DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12_000;
8
- export class HostServerConnection {
9
- config;
10
- identity;
11
- identities;
12
- api;
13
- connections;
14
- logger;
15
- createWebSocket;
16
- rtcFactoryProvider;
17
- hostCapabilities;
18
- harnessVersion;
19
- socket;
20
- running;
21
- stopped = true;
22
- online = false;
23
- retryWake;
24
- tunnels = new Map();
25
- terminalError;
26
- lastActiveAt;
27
- reconnectRequested = false;
28
- resumeQueued = false;
29
- authRecoveryAttempted = false;
30
- rtcFactory;
31
- negotiatedCapabilities = ['transport.relay'];
32
- controlFrameLimits = {};
33
- constructor(config, identity, identities, api, connections, logger, createWebSocket = url => new WebSocket(url), rtcFactoryProvider, hostCapabilities = () => ['harness.api.v1'], harnessVersion) {
34
- this.config = config;
35
- this.identity = identity;
36
- this.identities = identities;
37
- this.api = api;
38
- this.connections = connections;
39
- this.logger = logger;
40
- this.createWebSocket = createWebSocket;
41
- this.rtcFactoryProvider = rtcFactoryProvider;
42
- this.hostCapabilities = hostCapabilities;
43
- this.harnessVersion = harnessVersion;
44
- }
45
- start() {
46
- if (this.running !== undefined)
47
- return;
48
- this.stopped = false;
49
- this.running = this.run().finally(() => { this.running = undefined; });
50
- }
51
- resume() {
52
- this.terminalError = undefined;
53
- this.stopped = false;
54
- if (this.running === undefined) {
55
- this.start();
56
- return;
57
- }
58
- if (this.resumeQueued)
59
- return;
60
- this.resumeQueued = true;
61
- void this.running.finally(() => {
62
- this.resumeQueued = false;
63
- if (!this.stopped)
64
- this.start();
65
- });
66
- }
67
- async stop() {
68
- this.stopped = true;
69
- this.reconnectRequested = false;
70
- this.retryWake?.();
71
- this.retryWake = undefined;
72
- this.socket?.close(1000, 'plugin stopped');
73
- await this.running;
74
- await this.dropTunnels();
75
- }
76
- isOnline() { return this.online; }
77
- lastError() { return this.terminalError; }
78
- lastActivity() { return this.lastActiveAt; }
79
- isReconnecting() { return !this.online && !this.stopped && this.running !== undefined; }
80
- reconnect() {
81
- this.terminalError = undefined;
82
- this.stopped = false;
83
- if (this.running === undefined) {
84
- this.start();
85
- return;
86
- }
87
- this.reconnectRequested = true;
88
- this.retryWake?.();
89
- this.socket?.close(4000, 'manual reconnect');
90
- void this.running.finally(() => {
91
- if (!this.reconnectRequested || this.stopped)
92
- return;
93
- this.reconnectRequested = false;
94
- this.start();
95
- });
96
- }
97
- async run() {
98
- let delayMs = this.config.reconnect.initialDelayMs;
99
- this.authRecoveryAttempted = false;
100
- while (!this.stopped) {
101
- try {
102
- await this.connectOnce();
103
- delayMs = this.config.reconnect.initialDelayMs;
104
- }
105
- catch (error) {
106
- const code = errorCode(error);
107
- if (code === 'CREDENTIALS_REFRESHED')
108
- continue;
109
- this.terminalError = code;
110
- this.logger.warn('server control connection failed', {
111
- code, retryable: isRetryable(error),
112
- ...(error instanceof ServerApiError && error.phase !== undefined ? { phase: error.phase } : {}),
113
- });
114
- if (TERMINAL_AUTH_ERRORS.has(code)) {
115
- this.logger.warn(code === 'CONNECTION_REPLACED'
116
- ? 'Another instance is using this Host identity. Stop it or use a separate DSH_HOME; automatic reconnect is paused.'
117
- : code === 'SERVER_CREDENTIALS_BUSY'
118
- ? 'Credential refresh is locked. Stop other instances; after a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again.'
119
- : 'Host authorization failed. Run /remote login or authorize this Host again in Remote settings.');
120
- }
121
- if (code === 'DEVICE_REVOKED') {
122
- try {
123
- await this.api.clearAuthorization();
124
- }
125
- catch (clearError) {
126
- this.logger.error('failed to clear revoked Host authorization', { code: errorCode(clearError) });
127
- }
128
- }
129
- if (TERMINAL_AUTH_ERRORS.has(code) || !this.config.reconnect.enabled)
130
- return;
131
- }
132
- if (this.stopped)
133
- return;
134
- if (this.reconnectRequested) {
135
- this.reconnectRequested = false;
136
- delayMs = this.config.reconnect.initialDelayMs;
137
- continue;
138
- }
139
- if (!this.config.reconnect.enabled)
140
- return;
141
- await this.waitBeforeRetry(delayMs);
142
- if (this.reconnectRequested) {
143
- this.reconnectRequested = false;
144
- delayMs = this.config.reconnect.initialDelayMs;
145
- continue;
146
- }
147
- delayMs = Math.min(this.config.reconnect.maxDelayMs, delayMs * 2);
148
- }
149
- }
150
- async connectOnce() {
151
- const credentials = await this.api.authenticate(this.identity);
152
- if (this.stopped)
153
- return;
154
- const offeredCapabilities = this.rtcFactoryProvider === undefined || this.config.forceRelay
155
- ? ['transport.relay', ...this.hostCapabilities()]
156
- : ['transport.lan', 'transport.p2p', 'transport.turn', 'transport.relay', ...this.hostCapabilities()];
157
- const socket = this.createWebSocket(websocketUrl(this.api.baseUrl));
158
- this.socket = socket;
159
- this.controlFrameLimits = {};
160
- let acknowledged = false;
161
- let messageQueue = Promise.resolve();
162
- await new Promise((resolve, reject) => {
163
- let settled = false;
164
- const helloTimer = setTimeout(() => socket.close(4001, 'hello timeout'), 10_000);
165
- const finish = (error) => {
166
- if (settled)
167
- return;
168
- settled = true;
169
- clearTimeout(helloTimer);
170
- this.online = false;
171
- if (this.socket === socket)
172
- this.socket = undefined;
173
- void this.dropTunnels().finally(() => error === undefined ? resolve() : reject(error));
174
- };
175
- socket.onopen = () => {
176
- this.sendControl('hello', {
177
- role: 'host',
178
- deviceId: this.identity.deviceId,
179
- accessToken: credentials.accessToken,
180
- protocols: [PROTOCOL_VERSION],
181
- clientVersion: PLUGIN_VERSION,
182
- ...(this.harnessVersion === undefined ? {} : { harnessVersion: this.harnessVersion }),
183
- capabilities: offeredCapabilities,
184
- });
185
- };
186
- socket.onmessage = event => {
187
- messageQueue = messageQueue.then(async () => {
188
- const frame = decodeControl(event.data, this.controlFrameLimits);
189
- this.lastActiveAt = Date.now();
190
- if (frame.type === 'hello.ack') {
191
- const payload = requireHelloAck(frame.payload);
192
- this.authRecoveryAttempted = false;
193
- this.controlFrameLimits = {
194
- maxControlFrameBytes: payload.maxControlFrameBytes,
195
- maxRelayFrameBytes: payload.maxRelayFrameBytes,
196
- };
197
- this.negotiatedCapabilities = acceptNegotiatedCapabilities(offeredCapabilities, payload.capabilities);
198
- if (!this.negotiatedCapabilities.some(capability => capability === 'transport.relay'
199
- || capability === 'transport.lan'
200
- || capability === 'transport.p2p'
201
- || capability === 'transport.turn')) {
202
- throw new ControlConnectionError('INVALID_MESSAGE', 'Server did not negotiate a transport capability.');
203
- }
204
- acknowledged = true;
205
- clearTimeout(helloTimer);
206
- this.online = true;
207
- this.terminalError = undefined;
208
- this.logger.info('server control connection online', {
209
- serverVersion: payload.serverVersion,
210
- connectionSessionId: shortId(payload.connectionSessionId),
211
- });
212
- return;
213
- }
214
- if (!acknowledged)
215
- throw new ControlConnectionError('INVALID_MESSAGE', 'Server sent a frame before hello.ack.');
216
- await this.handleFrame(frame);
217
- }).catch(error => {
218
- const code = errorCode(error);
219
- this.terminalError = code;
220
- this.logger.error('server control frame failed', {
221
- code,
222
- reason: diagnosticReason(error),
223
- });
224
- socket.close(4008, 'invalid control frame');
225
- });
226
- };
227
- socket.onerror = () => {
228
- if (!acknowledged)
229
- finish(new ControlConnectionError('CONNECTION_FAILED', 'Unable to open the Server WebSocket.'));
230
- };
231
- socket.onclose = event => {
232
- const close = async () => {
233
- await messageQueue.catch(() => undefined);
234
- if (this.stopped) {
235
- finish();
236
- return;
237
- }
238
- if (event.code === 4003) {
239
- finish(new ControlConnectionError('CONNECTION_REPLACED', 'Another instance connected with this Host identity.'));
240
- return;
241
- }
242
- if (event.code === 4002) {
243
- if (this.authRecoveryAttempted) {
244
- finish(new ControlConnectionError('AUTH_INVALID', 'Server rejected refreshed credentials.'));
245
- return;
246
- }
247
- try {
248
- await this.api.refreshCredentials(credentials.accessToken);
249
- }
250
- catch (error) {
251
- finish(asError(error));
252
- return;
253
- }
254
- // Only a successfully refreshed credential consumes the hello retry.
255
- // Transient refresh errors must remain eligible for normal backoff.
256
- this.authRecoveryAttempted = true;
257
- finish(new ControlConnectionError('CREDENTIALS_REFRESHED', 'Retry hello with refreshed credentials.'));
258
- return;
259
- }
260
- if (event.code === 4004) {
261
- finish(new ControlConnectionError('DEVICE_REVOKED', 'The Server revoked this Host device.'));
262
- return;
263
- }
264
- if (acknowledged)
265
- this.terminalError = closeCode(event.code);
266
- finish(acknowledged ? undefined : new ControlConnectionError(closeCode(event.code), event.reason || 'Server control connection closed.'));
267
- };
268
- void close();
269
- };
270
- });
271
- }
272
- async handleFrame(frame) {
273
- if (frame.type === 'ping') {
274
- const nonce = objectValue(frame.payload, 'nonce');
275
- if (typeof nonce !== 'string')
276
- throw new ControlConnectionError('INVALID_MESSAGE', 'Control ping has no nonce.');
277
- this.sendControl('pong', { nonce });
278
- return;
279
- }
280
- if (frame.type === 'pong')
281
- return;
282
- if (frame.type === 'connect.incoming') {
283
- await this.handleConnectIncoming(requireConnectIncoming(frame.payload));
284
- return;
285
- }
286
- if (frame.type === 'secure.handshake') {
287
- await this.handleHandshake(requireHandshake(frame.payload));
288
- return;
289
- }
290
- if (frame.type === 'relay') {
291
- await this.handleRelay(requireRelay(frame.payload));
292
- return;
293
- }
294
- if (frame.type === 'signal.offer') {
295
- await this.handleSignalOffer(requireSignal(frame.payload));
296
- return;
297
- }
298
- if (frame.type === 'signal.ice') {
299
- this.handleSignalIce(requireSignalIce(frame.payload));
300
- return;
301
- }
302
- if (frame.type === 'transport.selected') {
303
- await this.handleTransportSelected(requireTransportSelected(frame.payload));
304
- return;
305
- }
306
- if (frame.type === 'signal.answer')
307
- return;
308
- if (frame.type === 'error') {
309
- const payload = requireControlError(frame.payload);
310
- if (payload.code === 'DEVICE_REVOKED') {
311
- this.terminalError = payload.code;
312
- this.socket?.close(4004, 'device revoked');
313
- }
314
- else if (payload.connectionId !== undefined) {
315
- await this.dropTunnel(payload.connectionId, payload.code);
316
- const fields = {
317
- code: payload.code,
318
- connectionId: shortId(payload.connectionId),
319
- retryable: payload.retryable,
320
- };
321
- if (payload.retryable)
322
- this.logger.debug('server closed a remote connection', fields);
323
- else
324
- this.logger.warn('server closed a remote connection', fields);
325
- }
326
- else {
327
- this.terminalError = payload.code;
328
- this.logger.warn('server returned a control error', { code: payload.code, retryable: payload.retryable });
329
- }
330
- return;
331
- }
332
- throw new ControlConnectionError('INVALID_MESSAGE', `Unexpected Server control frame: ${frame.type}`);
333
- }
334
- async handleConnectIncoming(payload) {
335
- let descriptor;
336
- try {
337
- descriptor = await this.api.deviceFor(payload.clientDeviceId);
338
- }
339
- catch (error) {
340
- this.sendControl('connect.rejected', { connectionId: payload.connectionId });
341
- this.logger.warn('connection rejected by account authorization', {
342
- clientDeviceId: shortId(payload.clientDeviceId),
343
- code: errorCode(error),
344
- });
345
- return;
346
- }
347
- if (descriptor.role !== 'client' || descriptor.deviceId !== payload.clientDeviceId
348
- || descriptor.identityKey !== payload.clientIdentityKey) {
349
- this.sendControl('connect.rejected', { connectionId: payload.connectionId });
350
- this.logger.warn('connection rejected by peer identity validation', {
351
- clientDeviceId: shortId(payload.clientDeviceId),
352
- });
353
- return;
354
- }
355
- const existing = this.identities.trustedPeer(descriptor.deviceId);
356
- if (existing !== undefined && existing.publicKey !== descriptor.identityKey) {
357
- this.sendControl('connect.rejected', { connectionId: payload.connectionId });
358
- this.logger.warn('connection rejected by pinned peer identity', {
359
- clientDeviceId: shortId(payload.clientDeviceId),
360
- });
361
- return;
362
- }
363
- const peer = existing !== undefined
364
- && existing.membershipId === descriptor.membershipId
365
- && existing.name === descriptor.name
366
- && existing.platform === descriptor.platform
367
- ? existing
368
- : await this.identities.trustPeer({
369
- deviceId: descriptor.deviceId,
370
- name: descriptor.name,
371
- platform: descriptor.platform,
372
- publicKey: descriptor.identityKey,
373
- membershipId: descriptor.membershipId,
374
- });
375
- const previous = this.tunnels.get(payload.connectionId);
376
- previous?.noise.destroy();
377
- const noise = new NoiseIkSession({
378
- role: 'responder',
379
- localPrivateKey: this.identity.privateKey,
380
- localPublicKey: this.identity.publicKey,
381
- remotePublicKey: peer.publicKey,
382
- prologue: createNoisePrologue(payload.connectionId, this.identity.deviceId, peer.deviceId),
383
- });
384
- this.tunnels.set(payload.connectionId, {
385
- connectionId: payload.connectionId,
386
- membershipId: descriptor.membershipId,
387
- peer,
388
- preferredTransports: payload.preferredTransports,
389
- noise,
390
- transport: 'negotiating',
391
- });
392
- this.sendControl('connect.accepted', { connectionId: payload.connectionId });
393
- }
394
- async handleHandshake(payload) {
395
- const tunnel = this.tunnels.get(payload.connectionId);
396
- if (tunnel !== undefined
397
- && tunnel.channel !== undefined
398
- && payload.targetDeviceId === this.identity.deviceId
399
- && payload.step === 1) {
400
- // A queued Relay/WebRTC fallback can deliver the initiator's first
401
- // handshake again after the authenticated channel is already ready.
402
- // It belongs to this exact tunnel, so ignore it without taking the
403
- // Host's shared control connection offline.
404
- this.logger.warn('duplicate secure handshake ignored', {
405
- connectionId: shortId(tunnel.connectionId),
406
- peerDeviceId: shortId(tunnel.peer.deviceId),
407
- });
408
- return;
409
- }
410
- if (tunnel === undefined || payload.targetDeviceId !== this.identity.deviceId || payload.step !== 1) {
411
- throw new ControlConnectionError('SECURE_CHANNEL_FAILED', 'Noise IK handshake is not valid for this connection.');
412
- }
413
- if (tunnel.transport === 'negotiating' && tunnel.rtc !== undefined) {
414
- // transport.selected and secure.handshake traverse the Server control
415
- // plane separately. Do not guess Relay if forwarding or the responder's
416
- // RTC-ready callback delivers the handshake first; resume as soon as
417
- // either side confirms the selected data plane.
418
- if (tunnel.pendingHandshake === undefined)
419
- tunnel.pendingHandshake = payload;
420
- else
421
- this.logger.warn('duplicate pending secure handshake ignored', {
422
- connectionId: shortId(tunnel.connectionId),
423
- peerDeviceId: shortId(tunnel.peer.deviceId),
424
- });
425
- return;
426
- }
427
- await this.completeHandshake(tunnel, payload);
428
- }
429
- async completeHandshake(tunnel, payload) {
430
- tunnel.noise.readHandshake(fromBase64Url(payload.data));
431
- const reply = tunnel.noise.writeHandshake();
432
- if (!tunnel.noise.complete)
433
- throw new ControlConnectionError('SECURE_CHANNEL_FAILED', 'Noise IK handshake did not complete.');
434
- const viaWebRtc = tunnel.rtc !== undefined
435
- && (tunnel.transport === 'lan' || tunnel.transport === 'p2p' || tunnel.transport === 'turn');
436
- if (!viaWebRtc && tunnel.transport === 'negotiating')
437
- tunnel.transport = 'relay';
438
- const mode = viaWebRtc
439
- ? tunnel.transportMode ?? (tunnel.transport === 'turn' ? 'TURN' : tunnel.transport === 'lan' ? 'LAN' : 'P2P')
440
- : 'Relay';
441
- const transmit = viaWebRtc
442
- ? (ciphertext) => tunnel.rtc.send(ciphertext)
443
- : (ciphertext) => this.sendRelay(tunnel, ciphertext);
444
- const channel = new ServerNoiseChannel(tunnel, transmit, () => {
445
- if (this.tunnels.get(tunnel.connectionId) === tunnel)
446
- this.tunnels.delete(tunnel.connectionId);
447
- }, mode);
448
- tunnel.channel = channel;
449
- await this.connections.accept(channel);
450
- // Publish the responder handshake only after the channel is registered.
451
- // The Client starts its first RPC as soon as step 2 arrives; sending the
452
- // reply earlier lets WebRTC deliver that RPC while tunnel.channel is still
453
- // undefined, which silently drops the request.
454
- this.sendControl('secure.handshake', {
455
- connectionId: tunnel.connectionId,
456
- targetDeviceId: tunnel.peer.deviceId,
457
- step: 2,
458
- data: toBase64Url(reply),
459
- });
460
- this.logger.info('authenticated peer channel ready', {
461
- connectionId: shortId(tunnel.connectionId),
462
- peerDeviceId: shortId(tunnel.peer.deviceId),
463
- transport: mode,
464
- });
465
- }
466
- async resumePendingHandshake(tunnel) {
467
- const payload = tunnel.pendingHandshake;
468
- if (payload === undefined || tunnel.channel !== undefined || tunnel.transport === 'negotiating')
469
- return;
470
- tunnel.pendingHandshake = undefined;
471
- await this.completeHandshake(tunnel, payload);
472
- }
473
- async handleRelay(payload) {
474
- if (!this.negotiatedCapabilities.includes('transport.relay')) {
475
- throw new ControlConnectionError('INVALID_MESSAGE', 'Server forwarded Relay without negotiating it.');
476
- }
477
- if (payload.targetDeviceId !== this.identity.deviceId) {
478
- throw new ControlConnectionError('INVALID_MESSAGE', 'Relay frame target does not match this Host.');
479
- }
480
- const tunnel = this.tunnels.get(payload.connectionId);
481
- if (tunnel?.channel === undefined) {
482
- this.logger.warn('stale relay frame ignored', {
483
- connectionId: shortId(payload.connectionId),
484
- });
485
- return;
486
- }
487
- try {
488
- tunnel.channel.receive(payload.counter, fromBase64Url(payload.ciphertext));
489
- }
490
- catch (error) {
491
- await tunnel.channel.close();
492
- throw new ControlConnectionError('SECURE_CHANNEL_FAILED', asError(error).message);
493
- }
494
- }
495
- async handleSignalOffer(payload) {
496
- if (!this.canUseWebRtc()) {
497
- throw new ControlConnectionError('INVALID_MESSAGE', 'Server forwarded WebRTC signaling without negotiating it.');
498
- }
499
- const tunnel = this.tunnels.get(payload.connectionId);
500
- if (tunnel === undefined || payload.targetDeviceId !== this.identity.deviceId) {
501
- this.logger.warn('stale webrtc offer ignored', { connectionId: shortId(payload.connectionId) });
502
- return;
503
- }
504
- if (tunnel.channel !== undefined || tunnel.rtc !== undefined) {
505
- // Late or duplicate offer after relay establishment / RTC start: ignore.
506
- this.logger.warn('duplicate webrtc offer ignored', { connectionId: shortId(tunnel.connectionId) });
507
- return;
508
- }
509
- if (this.config.forceRelay) {
510
- // Device-level forced degradation (webrtc plan §11).
511
- this.logger.warn('webrtc offer ignored: forceRelay is enabled', { connectionId: shortId(tunnel.connectionId) });
512
- return;
513
- }
514
- if (this.rtcFactory === undefined && this.rtcFactoryProvider !== undefined) {
515
- this.rtcFactory = await this.rtcFactoryProvider().catch(() => undefined);
516
- }
517
- if (this.rtcFactory === undefined) {
518
- // No Node RTC backend on this Host (webrtc plan §6.2): drop the offer so
519
- // the initiator times out and falls back to the Relay data plane.
520
- this.logger.warn('webrtc offer ignored: no RTC backend available', { connectionId: shortId(tunnel.connectionId) });
521
- return;
522
- }
523
- let iceServers = [];
524
- try {
525
- iceServers = await this.api.turnCredentials(tunnel.connectionId);
526
- }
527
- catch (error) {
528
- this.logger.warn('TURN credentials unavailable; trying direct candidates', {
529
- connectionId: shortId(tunnel.connectionId),
530
- code: errorCode(error),
531
- });
532
- }
533
- if (!tunnel.preferredTransports.includes('turn'))
534
- iceServers = stunOnlyIceServers(iceServers);
535
- const rtc = new RtcDataChannelTransport({
536
- role: 'responder',
537
- factory: this.rtcFactory,
538
- iceServers,
539
- onSignal: signal => this.sendRtcSignal(tunnel, signal),
540
- negotiateTimeoutMs: DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS,
541
- label: `host<-${tunnel.peer.deviceId}`,
542
- onDiagnostic: event => {
543
- if (event.type !== 'local-candidate-filtered' && event.type !== 'candidate-pair-filtered')
544
- return;
545
- this.logger.debug('webrtc candidate filtered', {
546
- connectionId: shortId(tunnel.connectionId),
547
- peerDeviceId: shortId(tunnel.peer.deviceId),
548
- type: event.type,
549
- reason: event.reason,
550
- candidate: event.type === 'local-candidate-filtered' ? event.candidate : event.localCandidate,
551
- });
552
- },
553
- });
554
- tunnel.rtc = rtc;
555
- rtc.onMessage(data => tunnel.channel?.receive(undefined, data));
556
- rtc.onClose(() => {
557
- void this.handleRtcFailed(tunnel, rtc, new Error('WebRTC data channel closed.'));
558
- });
559
- void rtc.connect().then(() => {
560
- this.handleRtcOpened(tunnel, rtc.selectedTransport() ?? 'p2p');
561
- }).catch(error => {
562
- void this.handleRtcFailed(tunnel, rtc, asError(error));
563
- });
564
- rtc.handleSignal({ type: 'offer', sdp: payload.sdp });
565
- }
566
- handleSignalIce(payload) {
567
- if (!this.canUseWebRtc()) {
568
- throw new ControlConnectionError('INVALID_MESSAGE', 'Server forwarded WebRTC signaling without negotiating it.');
569
- }
570
- const tunnel = this.tunnels.get(payload.connectionId);
571
- if (tunnel === undefined || payload.targetDeviceId !== this.identity.deviceId)
572
- return;
573
- tunnel.rtc?.handleSignal({ type: 'ice', candidate: payload.candidate });
574
- }
575
- async handleTransportSelected(payload) {
576
- const tunnel = this.tunnels.get(payload.connectionId);
577
- if (tunnel === undefined || payload.targetDeviceId !== this.identity.deviceId) {
578
- this.logger.warn('stale transport selection ignored', { connectionId: shortId(payload.connectionId) });
579
- return;
580
- }
581
- if (tunnel.channel !== undefined) {
582
- // Selection is immutable once Noise has bound the business channel to a
583
- // data plane. Late duplicate control frames must not switch it beneath
584
- // an authenticated connection.
585
- if (tunnel.transport !== payload.transport) {
586
- this.logger.warn('late transport selection ignored', {
587
- connectionId: shortId(tunnel.connectionId),
588
- selected: payload.transport,
589
- });
590
- }
591
- return;
592
- }
593
- const requiredCapability = `transport.${payload.transport}`;
594
- if (!this.negotiatedCapabilities.includes(requiredCapability)) {
595
- throw new ControlConnectionError('INVALID_MESSAGE', `Client selected unnegotiated transport: ${payload.transport}`);
596
- }
597
- if (payload.transport === 'relay') {
598
- const rtc = tunnel.rtc;
599
- tunnel.rtc = undefined;
600
- tunnel.transport = 'relay';
601
- tunnel.transportMode = undefined;
602
- await rtc?.close();
603
- await this.resumePendingHandshake(tunnel);
604
- return;
605
- }
606
- if (tunnel.rtc === undefined) {
607
- throw new ControlConnectionError('INVALID_MESSAGE', 'Client selected WebRTC before creating a data channel.');
608
- }
609
- // The initiator emits transport.selected only after its DataChannel is
610
- // open, then immediately starts Noise. Apply that ordered control frame
611
- // before the handshake so a slower responder-side RTC ready callback
612
- // cannot bind replies to Relay while requests arrive over WebRTC.
613
- tunnel.transport = payload.transport;
614
- tunnel.transportMode = tunnel.rtc.selectedPathMode();
615
- await this.resumePendingHandshake(tunnel);
616
- }
617
- canUseWebRtc() {
618
- return this.negotiatedCapabilities.includes('transport.lan')
619
- || this.negotiatedCapabilities.includes('transport.p2p')
620
- || this.negotiatedCapabilities.includes('transport.turn');
621
- }
622
- sendRtcSignal(tunnel, signal) {
623
- if (signal.type === 'answer') {
624
- this.sendControl('signal.answer', {
625
- connectionId: tunnel.connectionId,
626
- targetDeviceId: tunnel.peer.deviceId,
627
- sdp: signal.sdp,
628
- });
629
- }
630
- else if (signal.type === 'ice') {
631
- this.sendControl('signal.ice', {
632
- connectionId: tunnel.connectionId,
633
- targetDeviceId: tunnel.peer.deviceId,
634
- candidate: signal.candidate,
635
- });
636
- }
637
- }
638
- handleRtcOpened(tunnel, selected) {
639
- if (this.tunnels.get(tunnel.connectionId) !== tunnel || tunnel.rtc === undefined)
640
- return;
641
- const wireSelected = selected === 'lan'
642
- && !this.negotiatedCapabilities.includes('transport.lan')
643
- && this.negotiatedCapabilities.includes('transport.p2p')
644
- ? 'p2p'
645
- : selected;
646
- const requiredCapability = wireSelected === 'lan'
647
- ? 'transport.lan'
648
- : wireSelected === 'turn' ? 'transport.turn' : 'transport.p2p';
649
- if (!this.negotiatedCapabilities.includes(requiredCapability)) {
650
- const error = new Error(`WebRTC selected unnegotiated transport: ${selected}`);
651
- if (!this.negotiatedCapabilities.includes('transport.relay')) {
652
- void this.dropTunnel(tunnel.connectionId, 'CONNECTION_FAILED');
653
- return;
654
- }
655
- void this.handleRtcFailed(tunnel, tunnel.rtc, error);
656
- return;
657
- }
658
- tunnel.transport = wireSelected;
659
- tunnel.transportMode = tunnel.rtc.selectedPathMode();
660
- this.sendTransportSelected(tunnel, wireSelected);
661
- const diagnostics = rtcDiagnostics(tunnel.rtc);
662
- this.logger.info('webrtc data channel ready', {
663
- connectionId: shortId(tunnel.connectionId),
664
- peerDeviceId: shortId(tunnel.peer.deviceId),
665
- transport: tunnel.transportMode ?? wireSelected,
666
- });
667
- if (diagnostics !== undefined) {
668
- this.logger.debug('webrtc data channel diagnostics', {
669
- connectionId: shortId(tunnel.connectionId),
670
- ...webrtcDiagnosticsLogFields(diagnostics),
671
- });
672
- }
673
- void this.resumePendingHandshake(tunnel).catch(error => {
674
- this.logger.warn('pending secure handshake failed', {
675
- connectionId: shortId(tunnel.connectionId),
676
- reason: diagnosticReason(error),
677
- });
678
- void this.dropTunnel(tunnel.connectionId, 'SECURE_CHANNEL_FAILED');
679
- });
680
- }
681
- async handleRtcFailed(tunnel, rtc, error) {
682
- if (this.tunnels.get(tunnel.connectionId) !== tunnel || tunnel.rtc !== rtc)
683
- return;
684
- const diagnostics = rtcDiagnostics(rtc);
685
- if (tunnel.transport === 'lan' || tunnel.transport === 'p2p' || tunnel.transport === 'turn') {
686
- this.logger.warn('webrtc data channel failed; disconnecting peer', {
687
- connectionId: shortId(tunnel.connectionId),
688
- reason: diagnosticReason(error),
689
- });
690
- if (diagnostics !== undefined) {
691
- this.logger.debug('webrtc data channel failure diagnostics', {
692
- connectionId: shortId(tunnel.connectionId),
693
- ...webrtcDiagnosticsLogFields(diagnostics),
694
- });
695
- }
696
- await this.dropTunnel(tunnel.connectionId, 'CONNECTION_FAILED');
697
- return;
698
- }
699
- tunnel.rtc = undefined;
700
- tunnel.transport = 'relay';
701
- await rtc.close();
702
- this.logger.warn('webrtc negotiation failed; falling back to relay', {
703
- connectionId: shortId(tunnel.connectionId),
704
- reason: diagnosticReason(error),
705
- });
706
- if (diagnostics !== undefined) {
707
- this.logger.debug('webrtc negotiation failure diagnostics', {
708
- connectionId: shortId(tunnel.connectionId),
709
- ...webrtcDiagnosticsLogFields(diagnostics),
710
- });
711
- }
712
- await this.resumePendingHandshake(tunnel);
713
- }
714
- sendTransportSelected(tunnel, transport) {
715
- this.sendControl('transport.selected', {
716
- connectionId: tunnel.connectionId,
717
- targetDeviceId: tunnel.peer.deviceId,
718
- transport,
719
- });
720
- }
721
- async sendRelay(tunnel, ciphertext) {
722
- const counter = Number(tunnel.noise.sendingCounter() - 1n);
723
- if (!Number.isSafeInteger(counter) || counter < 0)
724
- throw new ControlConnectionError('FRAME_TOO_LARGE', 'Noise transport counter overflowed.');
725
- const socket = this.socket;
726
- if (socket === undefined)
727
- throw new Error('Relay transport closed');
728
- await waitForRelayCapacity(socket);
729
- if (this.socket !== socket)
730
- throw new Error('Relay transport replaced');
731
- this.sendControl('relay', {
732
- connectionId: tunnel.connectionId,
733
- targetDeviceId: tunnel.peer.deviceId,
734
- counter,
735
- ciphertext: toBase64Url(ciphertext),
736
- });
737
- }
738
- sendControl(type, payload) {
739
- const socket = this.socket;
740
- if (socket === undefined || socket.readyState !== 1)
741
- throw new ControlConnectionError('CONNECTION_FAILED', 'Server control socket is not open.');
742
- socket.send(encodeControlFrame(createControlFrame(type, payload), this.controlFrameLimits));
743
- }
744
- async dropTunnels() {
745
- const tunnels = [...this.tunnels.values()];
746
- this.tunnels.clear();
747
- await Promise.all(tunnels.map(async (tunnel) => {
748
- if (tunnel.rtc !== undefined)
749
- await tunnel.rtc.close();
750
- if (tunnel.channel !== undefined)
751
- await tunnel.channel.close();
752
- else
753
- tunnel.noise.destroy();
754
- }));
755
- await this.connections.close();
756
- }
757
- async dropTunnel(connectionId, code) {
758
- const tunnel = this.tunnels.get(connectionId);
759
- if (tunnel === undefined)
760
- return;
761
- this.tunnels.delete(connectionId);
762
- try {
763
- await tunnel.rtc?.close();
764
- }
765
- catch (error) {
766
- this.logger.warn('remote connection RTC cleanup failed', {
767
- connectionId: shortId(connectionId),
768
- reason: diagnosticReason(error),
769
- });
770
- }
771
- if (tunnel.channel !== undefined) {
772
- try {
773
- const closed = await this.connections.closeConnection(connectionId, code);
774
- if (!closed)
775
- await tunnel.channel.close(code);
776
- }
777
- catch (error) {
778
- await tunnel.channel.close(code).catch(() => undefined);
779
- this.logger.warn('remote connection channel cleanup failed', {
780
- connectionId: shortId(connectionId),
781
- reason: diagnosticReason(error),
782
- });
783
- }
784
- }
785
- else {
786
- tunnel.noise.destroy();
787
- }
788
- }
789
- waitBeforeRetry(baseDelay) {
790
- const spread = baseDelay * this.config.reconnect.jitter;
791
- const delay = Math.max(0, Math.round(baseDelay - spread + Math.random() * spread * 2));
792
- return new Promise(resolve => {
793
- const timer = setTimeout(() => { this.retryWake = undefined; resolve(); }, delay);
794
- this.retryWake = () => { clearTimeout(timer); resolve(); };
795
- });
796
- }
797
- }
798
- const TERMINAL_AUTH_ERRORS = new Set([
799
- 'CONNECTION_REPLACED',
800
- 'SERVER_CREDENTIALS_BUSY',
801
- 'ACCOUNT_AUTH_REQUIRED',
802
- 'AUTH_INVALID',
803
- 'DEVICE_OWNERSHIP_REQUIRED',
804
- 'DEVICE_REVOKED',
805
- 'TOKEN_EXPIRED',
806
- ]);
807
- class ServerNoiseChannel {
808
- tunnel;
809
- transmit;
810
- onClose;
811
- sends = new SerialSend();
812
- security;
813
- peerDeviceId;
814
- peerIdentityKey;
815
- mode;
816
- handlers = new Set();
817
- incoming = new SecureMessageCodec();
818
- outgoing = new SecureMessageCodec();
819
- closed = false;
820
- constructor(tunnel, transmit, onClose, mode) {
821
- this.tunnel = tunnel;
822
- this.transmit = transmit;
823
- this.onClose = onClose;
824
- this.mode = mode;
825
- this.security = {
826
- protocol: 'Noise_IK_25519_ChaChaPoly_SHA256',
827
- connectionId: tunnel.connectionId,
828
- membershipId: tunnel.membershipId,
829
- };
830
- this.peerDeviceId = tunnel.peer.deviceId;
831
- this.peerIdentityKey = tunnel.peer.publicKey;
832
- }
833
- async send(message) {
834
- if (this.closed)
835
- throw new Error('secure channel is closed');
836
- const encoded = encodeMessage(message);
837
- try {
838
- await this.sends.run(encoded.byteLength, async () => {
839
- if (this.closed)
840
- throw new Error('Secure channel closed');
841
- for (const plaintext of this.outgoing.encode(encoded))
842
- await this.transmit(this.tunnel.noise.encrypt(plaintext));
843
- });
844
- }
845
- catch (error) {
846
- await this.close().catch(() => undefined);
847
- throw error;
848
- }
849
- }
850
- onMessage(handler) {
851
- this.handlers.add(handler);
852
- return () => this.handlers.delete(handler);
853
- }
854
- receive(counter, ciphertext) {
855
- if (this.closed)
856
- return;
857
- if (counter !== undefined) {
858
- const expected = Number(this.tunnel.noise.receivingCounter());
859
- if (!Number.isSafeInteger(counter) || counter !== expected) {
860
- throw new ControlConnectionError('INVALID_MESSAGE', 'Relay counter is duplicated or out of order.');
861
- }
862
- }
863
- const plaintext = this.incoming.decode(this.tunnel.noise.decrypt(ciphertext));
864
- if (plaintext === undefined)
865
- return;
866
- const message = decodeMessage(plaintext);
867
- for (const handler of this.handlers)
868
- handler(message);
869
- }
870
- async close(_code) {
871
- if (this.closed)
872
- return;
873
- this.closed = true;
874
- this.handlers.clear();
875
- this.incoming.reset();
876
- this.outgoing.reset();
877
- this.tunnel.noise.destroy();
878
- this.onClose();
879
- }
880
- }
881
- class ControlConnectionError extends Error {
882
- code;
883
- constructor(code, message) {
884
- super(message);
885
- this.code = code;
886
- }
887
- }
888
- function websocketUrl(baseUrl) {
889
- const url = new URL(baseUrl);
890
- url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
891
- url.pathname = `${url.pathname.replace(/\/$/, '')}/ws/v1/connect`;
892
- return url.toString();
893
- }
894
- function decodeControl(data, limits) {
895
- if (typeof data !== 'string')
896
- throw new ControlConnectionError('INVALID_MESSAGE', 'Server control frames must be text JSON.');
897
- try {
898
- return decodeControlFrame(data, limits);
899
- }
900
- catch {
901
- throw new ControlConnectionError('INVALID_MESSAGE', 'Server sent an invalid control frame.');
902
- }
903
- }
904
- function requireHelloAck(value) {
905
- const payload = requireObject(value);
906
- if (payload.protocol !== PROTOCOL_VERSION || typeof payload.serverVersion !== 'string'
907
- || typeof payload.connectionSessionId !== 'string' || !Number.isSafeInteger(payload.heartbeatIntervalMs)
908
- || !Number.isSafeInteger(payload.maxControlFrameBytes) || !Number.isSafeInteger(payload.maxRelayFrameBytes)) {
909
- throw new ControlConnectionError('INVALID_MESSAGE', 'hello.ack payload is invalid.');
910
- }
911
- return payload;
912
- }
913
- function requireConnectIncoming(value) {
914
- const payload = requireObject(value);
915
- if (typeof payload.connectionId !== 'string' || typeof payload.clientDeviceId !== 'string'
916
- || typeof payload.clientIdentityKey !== 'string' || payload.authorization !== 'account'
917
- || !Array.isArray(payload.preferredTransports)) {
918
- throw new ControlConnectionError('INVALID_MESSAGE', 'connect.incoming payload is invalid.');
919
- }
920
- return payload;
921
- }
922
- function requireHandshake(value) {
923
- const payload = requireObject(value);
924
- if (typeof payload.connectionId !== 'string' || typeof payload.targetDeviceId !== 'string'
925
- || !Number.isSafeInteger(payload.step) || typeof payload.data !== 'string') {
926
- throw new ControlConnectionError('INVALID_MESSAGE', 'secure.handshake payload is invalid.');
927
- }
928
- return payload;
929
- }
930
- function requireRelay(value) {
931
- const payload = requireObject(value);
932
- if (typeof payload.connectionId !== 'string' || typeof payload.targetDeviceId !== 'string'
933
- || !Number.isSafeInteger(payload.counter) || typeof payload.ciphertext !== 'string') {
934
- throw new ControlConnectionError('INVALID_MESSAGE', 'relay payload is invalid.');
935
- }
936
- return payload;
937
- }
938
- function requireSignal(value) {
939
- const payload = requireObject(value);
940
- if (typeof payload.connectionId !== 'string' || typeof payload.targetDeviceId !== 'string'
941
- || typeof payload.sdp !== 'string' || payload.sdp.length === 0) {
942
- throw new ControlConnectionError('INVALID_MESSAGE', 'signal offer/answer payload is invalid.');
943
- }
944
- return payload;
945
- }
946
- function requireSignalIce(value) {
947
- const payload = requireObject(value);
948
- if (typeof payload.connectionId !== 'string' || typeof payload.targetDeviceId !== 'string'
949
- || typeof payload.candidate !== 'object' || payload.candidate === null || Array.isArray(payload.candidate)) {
950
- throw new ControlConnectionError('INVALID_MESSAGE', 'signal.ice payload is invalid.');
951
- }
952
- return payload;
953
- }
954
- function requireTransportSelected(value) {
955
- const payload = requireObject(value);
956
- if (typeof payload.connectionId !== 'string' || typeof payload.targetDeviceId !== 'string'
957
- || (payload.transport !== 'lan' && payload.transport !== 'p2p'
958
- && payload.transport !== 'turn' && payload.transport !== 'relay')) {
959
- throw new ControlConnectionError('INVALID_MESSAGE', 'transport.selected payload is invalid.');
960
- }
961
- return payload;
962
- }
963
- function requireControlError(value) {
964
- const payload = requireObject(value);
965
- if (typeof payload.code !== 'string' || typeof payload.message !== 'string'
966
- || (payload.connectionId !== undefined && (typeof payload.connectionId !== 'string' || payload.connectionId === ''))) {
967
- throw new ControlConnectionError('INVALID_MESSAGE', 'error payload is invalid.');
968
- }
969
- return payload;
970
- }
971
- function requireObject(value) {
972
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
973
- throw new ControlConnectionError('INVALID_MESSAGE', 'Control payload must be an object.');
974
- }
975
- return value;
976
- }
977
- function objectValue(value, key) { return requireObject(value)[key]; }
978
- function shortId(value) { return value.length <= 12 ? value : `${value.slice(0, 8)}…${value.slice(-4)}`; }
979
- function asError(error) { return error instanceof Error ? error : new Error('Unknown Server connection error.'); }
980
- function diagnosticReason(error) {
981
- const message = asError(error).message.replace(/[\r\n]+/g, ' ').slice(0, 160);
982
- return message || 'Unknown Server connection error.';
983
- }
984
- function webrtcDiagnosticsLogFields(diagnostics) {
985
- if (diagnostics === undefined)
986
- return {};
987
- return {
988
- rtcConnectionState: diagnostics.connectionState,
989
- rtcIceConnectionState: diagnostics.iceConnectionState,
990
- rtcIceGatheringState: diagnostics.iceGatheringState,
991
- rtcLocalCandidates: diagnostics.localCandidates,
992
- rtcRemoteCandidates: diagnostics.remoteCandidates,
993
- rtcCandidatePairs: diagnostics.candidatePairs,
994
- rtcFilteredLocalCandidates: diagnostics.filteredLocalCandidates,
995
- rtcFilteredCandidatePairs: diagnostics.filteredCandidatePairs,
996
- ...(diagnostics.selectedPath === undefined ? {} : { rtcSelectedPath: diagnostics.selectedPath }),
997
- };
998
- }
999
- function rtcDiagnostics(rtc) {
1000
- try {
1001
- const candidate = rtc;
1002
- return typeof candidate.diagnostics === 'function' ? candidate.diagnostics() : undefined;
1003
- }
1004
- catch {
1005
- return undefined;
1006
- }
1007
- }
1008
- function errorCode(error) { return error instanceof ServerApiError || error instanceof ControlConnectionError ? error.code : 'CONNECTION_FAILED'; }
1009
- function isRetryable(error) { return !TERMINAL_AUTH_ERRORS.has(errorCode(error)) && (!(error instanceof ServerApiError) || error.retryable); }
1010
- function closeCode(code) {
1011
- if (code === 4002)
1012
- return 'AUTH_INVALID';
1013
- if (code === 4003)
1014
- return 'CONNECTION_REPLACED';
1015
- if (code === 4004)
1016
- return 'DEVICE_REVOKED';
1017
- if (code === 4007)
1018
- return 'RATE_LIMITED';
1019
- if (code === 4011)
1020
- return 'UNSUPPORTED_VERSION';
1021
- return 'CONNECTION_FAILED';
1022
- }
1023
- //# sourceMappingURL=server-connection.js.map