@tapflowio/relay 0.22.0 → 0.23.0

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 (158) hide show
  1. package/dist/RelayServer.d.ts +18 -0
  2. package/dist/RelayServer.d.ts.map +1 -1
  3. package/dist/RelayServer.js +110 -12
  4. package/dist/RelayServer.js.map +1 -1
  5. package/dist/SessionManager.d.ts +4 -1
  6. package/dist/SessionManager.d.ts.map +1 -1
  7. package/dist/SessionManager.js +7 -0
  8. package/dist/SessionManager.js.map +1 -1
  9. package/dist/api/auth.d.ts.map +1 -1
  10. package/dist/api/auth.js +2 -1
  11. package/dist/api/auth.js.map +1 -1
  12. package/dist/api/passwordReset.d.ts +6 -2
  13. package/dist/api/passwordReset.d.ts.map +1 -1
  14. package/dist/api/passwordReset.js +21 -6
  15. package/dist/api/passwordReset.js.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/clientAddress.d.ts +15 -0
  21. package/dist/lib/clientAddress.d.ts.map +1 -1
  22. package/dist/lib/clientAddress.js +21 -0
  23. package/dist/lib/clientAddress.js.map +1 -1
  24. package/dist/lib/config.d.ts +2 -0
  25. package/dist/lib/config.d.ts.map +1 -1
  26. package/dist/lib/config.js +6 -0
  27. package/dist/lib/config.js.map +1 -1
  28. package/dist/lib/proxyConfig.d.ts +10 -0
  29. package/dist/lib/proxyConfig.d.ts.map +1 -1
  30. package/dist/lib/proxyConfig.js +14 -0
  31. package/dist/lib/proxyConfig.js.map +1 -1
  32. package/dist/server.js +3 -1
  33. package/dist/server.js.map +1 -1
  34. package/package.json +5 -5
  35. package/public/assets/AppCenter-OTDuqwSf.js +21 -0
  36. package/public/assets/AppCenter-OTDuqwSf.js.br +0 -0
  37. package/public/assets/AppCenter-OTDuqwSf.js.gz +0 -0
  38. package/public/assets/{Default-2rFTkbo9.js → Default-D9GLpWlN.js} +1 -1
  39. package/public/assets/Default-D9GLpWlN.js.br +0 -0
  40. package/public/assets/Default-D9GLpWlN.js.gz +0 -0
  41. package/public/assets/{Invite-aN4DXJyM.js → Invite-Bhiam8En.js} +1 -1
  42. package/public/assets/Invite-Bhiam8En.js.br +0 -0
  43. package/public/assets/Invite-Bhiam8En.js.gz +0 -0
  44. package/public/assets/{MacResources-D3IlesXU.js → MacResources-DObzVJxQ.js} +1 -1
  45. package/public/assets/MacResources-DObzVJxQ.js.br +0 -0
  46. package/public/assets/MacResources-DObzVJxQ.js.gz +0 -0
  47. package/public/assets/{NotFound-BoDDivK5.js → NotFound-Dz_k_Mzf.js} +1 -1
  48. package/public/assets/NotFound-Dz_k_Mzf.js.br +0 -0
  49. package/public/assets/NotFound-Dz_k_Mzf.js.gz +0 -0
  50. package/public/assets/QASession-2crubOLY.js +150 -0
  51. package/public/assets/QASession-2crubOLY.js.br +0 -0
  52. package/public/assets/QASession-2crubOLY.js.gz +0 -0
  53. package/public/assets/{ResetPassword-DPc5kwQU.js → ResetPassword-EUm12ojo.js} +1 -1
  54. package/public/assets/ResetPassword-EUm12ojo.js.br +0 -0
  55. package/public/assets/ResetPassword-EUm12ojo.js.gz +0 -0
  56. package/public/assets/{Setup-CwPx_e_O.js → Setup-Fc_0RW0a.js} +1 -1
  57. package/public/assets/Setup-Fc_0RW0a.js.br +0 -0
  58. package/public/assets/Setup-Fc_0RW0a.js.gz +0 -0
  59. package/public/assets/Team-Ac5aOGgx.js +6 -0
  60. package/public/assets/Team-Ac5aOGgx.js.br +0 -0
  61. package/public/assets/Team-Ac5aOGgx.js.gz +0 -0
  62. package/public/assets/{Tokens-DdMMke73.js → Tokens-pEnCy-7l.js} +1 -1
  63. package/public/assets/Tokens-pEnCy-7l.js.br +0 -0
  64. package/public/assets/Tokens-pEnCy-7l.js.gz +0 -0
  65. package/public/assets/alert-dialog-DhOTocMh.js +1 -0
  66. package/public/assets/alert-dialog-DhOTocMh.js.br +0 -0
  67. package/public/assets/alert-dialog-DhOTocMh.js.gz +0 -0
  68. package/public/assets/{badge-CMf8d5RJ.js → badge-B4gbViMs.js} +1 -1
  69. package/public/assets/badge-B4gbViMs.js.br +0 -0
  70. package/public/assets/badge-B4gbViMs.js.gz +0 -0
  71. package/public/assets/build-format-DKGajT96.js +11 -0
  72. package/public/assets/build-format-DKGajT96.js.br +0 -0
  73. package/public/assets/build-format-DKGajT96.js.gz +0 -0
  74. package/public/assets/dialog-DEL1xxKJ.js +11 -0
  75. package/public/assets/dialog-DEL1xxKJ.js.br +0 -0
  76. package/public/assets/dialog-DEL1xxKJ.js.gz +0 -0
  77. package/public/assets/index-COIlCLCW.js +218 -0
  78. package/public/assets/index-COIlCLCW.js.br +0 -0
  79. package/public/assets/index-COIlCLCW.js.gz +0 -0
  80. package/public/assets/pencil-CPaoEKFm.js +6 -0
  81. package/public/assets/pencil-CPaoEKFm.js.br +0 -0
  82. package/public/assets/pencil-CPaoEKFm.js.gz +0 -0
  83. package/public/assets/resource-health-Ca4-5Rde.js +1 -0
  84. package/public/assets/resource-health-Ca4-5Rde.js.br +0 -0
  85. package/public/assets/resource-health-Ca4-5Rde.js.gz +0 -0
  86. package/public/assets/{table-CL9AvoTp.js → table-CSeGVnii.js} +1 -1
  87. package/public/assets/table-CSeGVnii.js.br +0 -0
  88. package/public/assets/table-CSeGVnii.js.gz +0 -0
  89. package/public/assets/trash-CD9Y9G-i.js +11 -0
  90. package/public/assets/trash-CD9Y9G-i.js.br +0 -0
  91. package/public/assets/trash-CD9Y9G-i.js.gz +0 -0
  92. package/public/assets/{vendor-charts-8kVeHLwY.js → vendor-charts-YPhPkjLw.js} +1 -1
  93. package/public/assets/vendor-charts-YPhPkjLw.js.br +0 -0
  94. package/public/assets/vendor-charts-YPhPkjLw.js.gz +0 -0
  95. package/public/assets/vendor-react-C5H_1fOn.js +49 -0
  96. package/public/assets/vendor-react-C5H_1fOn.js.br +0 -0
  97. package/public/assets/vendor-react-C5H_1fOn.js.gz +0 -0
  98. package/public/assets/{vendor-router-BFibuab1.js → vendor-router-CnmrRwJF.js} +1 -1
  99. package/public/assets/vendor-router-CnmrRwJF.js.br +0 -0
  100. package/public/assets/vendor-router-CnmrRwJF.js.gz +0 -0
  101. package/public/index.html +3 -3
  102. package/public/index.html.br +0 -0
  103. package/public/index.html.gz +0 -0
  104. package/public/assets/AppCenter-gbsJscjW.js +0 -21
  105. package/public/assets/AppCenter-gbsJscjW.js.br +0 -0
  106. package/public/assets/AppCenter-gbsJscjW.js.gz +0 -0
  107. package/public/assets/Default-2rFTkbo9.js.br +0 -0
  108. package/public/assets/Default-2rFTkbo9.js.gz +0 -0
  109. package/public/assets/Invite-aN4DXJyM.js.br +0 -0
  110. package/public/assets/Invite-aN4DXJyM.js.gz +0 -0
  111. package/public/assets/MacResources-D3IlesXU.js.br +0 -0
  112. package/public/assets/MacResources-D3IlesXU.js.gz +0 -0
  113. package/public/assets/NotFound-BoDDivK5.js.br +0 -0
  114. package/public/assets/NotFound-BoDDivK5.js.gz +0 -0
  115. package/public/assets/QASession-U29t9j0E.js +0 -140
  116. package/public/assets/QASession-U29t9j0E.js.br +0 -0
  117. package/public/assets/QASession-U29t9j0E.js.gz +0 -0
  118. package/public/assets/ResetPassword-DPc5kwQU.js.br +0 -0
  119. package/public/assets/ResetPassword-DPc5kwQU.js.gz +0 -0
  120. package/public/assets/Setup-CwPx_e_O.js.br +0 -0
  121. package/public/assets/Setup-CwPx_e_O.js.gz +0 -0
  122. package/public/assets/Team-DD9li-7M.js +0 -6
  123. package/public/assets/Team-DD9li-7M.js.br +0 -0
  124. package/public/assets/Team-DD9li-7M.js.gz +0 -0
  125. package/public/assets/Tokens-DdMMke73.js.br +0 -0
  126. package/public/assets/Tokens-DdMMke73.js.gz +0 -0
  127. package/public/assets/alert-dialog-DADd1jHN.js +0 -1
  128. package/public/assets/alert-dialog-DADd1jHN.js.br +0 -0
  129. package/public/assets/alert-dialog-DADd1jHN.js.gz +0 -0
  130. package/public/assets/badge-CMf8d5RJ.js.br +0 -0
  131. package/public/assets/badge-CMf8d5RJ.js.gz +0 -0
  132. package/public/assets/build-format-lp_8kSvq.js +0 -11
  133. package/public/assets/build-format-lp_8kSvq.js.br +0 -0
  134. package/public/assets/build-format-lp_8kSvq.js.gz +0 -0
  135. package/public/assets/dialog-pS8vHXXv.js +0 -11
  136. package/public/assets/dialog-pS8vHXXv.js.br +0 -0
  137. package/public/assets/dialog-pS8vHXXv.js.gz +0 -0
  138. package/public/assets/index-Cajf0T-5.js +0 -198
  139. package/public/assets/index-Cajf0T-5.js.br +0 -0
  140. package/public/assets/index-Cajf0T-5.js.gz +0 -0
  141. package/public/assets/pencil-LD1ga-Kk.js +0 -6
  142. package/public/assets/pencil-LD1ga-Kk.js.br +0 -0
  143. package/public/assets/pencil-LD1ga-Kk.js.gz +0 -0
  144. package/public/assets/resource-health-BScurILF.js +0 -1
  145. package/public/assets/resource-health-BScurILF.js.br +0 -0
  146. package/public/assets/resource-health-BScurILF.js.gz +0 -0
  147. package/public/assets/table-CL9AvoTp.js.br +0 -0
  148. package/public/assets/table-CL9AvoTp.js.gz +0 -0
  149. package/public/assets/trash-mvVp4nwq.js +0 -11
  150. package/public/assets/trash-mvVp4nwq.js.br +0 -0
  151. package/public/assets/trash-mvVp4nwq.js.gz +0 -0
  152. package/public/assets/vendor-charts-8kVeHLwY.js.br +0 -0
  153. package/public/assets/vendor-charts-8kVeHLwY.js.gz +0 -0
  154. package/public/assets/vendor-react-AJpf4YZ0.js +0 -49
  155. package/public/assets/vendor-react-AJpf4YZ0.js.br +0 -0
  156. package/public/assets/vendor-react-AJpf4YZ0.js.gz +0 -0
  157. package/public/assets/vendor-router-BFibuab1.js.br +0 -0
  158. package/public/assets/vendor-router-BFibuab1.js.gz +0 -0
@@ -30,6 +30,20 @@ export declare function ownerKeyFor(userId: number | string | undefined, claimed
30
30
  export declare class RelayServer {
31
31
  private readonly options;
32
32
  private httpServer;
33
+ /**
34
+ * **The port tunnel clients are pointed at, and the only thing that tells their traffic apart.**
35
+ * rathole's client and `tailscale serve` run on this machine and connect from loopback on behalf of
36
+ * someone on the internet or the tailnet; rathole forwards raw TCP, so no header marks it either. On
37
+ * the main port such a socket is indistinguishable from `tapflow start`'s own agent, which is
38
+ * unauthenticated by design — so every tunnel visitor was too. Everything accepted here is remote
39
+ * (`resolveClientAddress`'s `viaTunnel`), whatever its address.
40
+ *
41
+ * Loopback-only, because only a tunnel client on this machine has any business here; a remote client
42
+ * belongs on the main port, where it is classified by address as before. Same handler, same
43
+ * `WebSocketServer`, same TLS material — rathole carried a TLS relay's own handshake through before
44
+ * this existed, and still does.
45
+ */
46
+ private tunnelServer;
33
47
  private wss;
34
48
  private sessions;
35
49
  private publicDir;
@@ -114,12 +128,16 @@ export declare class RelayServer {
114
128
  };
115
129
  agentGraceMs?: number;
116
130
  tunnel?: TunnelRuntime;
131
+ tunnelPort?: number;
117
132
  });
118
133
  private registerRoutes;
119
134
  pushLog(msg: string): void;
120
135
  start(): Promise<void>;
136
+ private listenTunnel;
121
137
  stop(): Promise<void>;
122
138
  address(): string | import("net").AddressInfo | null;
139
+ /** The tunnel listener's address, or null when this relay was given no `tunnelPort`. */
140
+ tunnelAddress(): string | import("net").AddressInfo | null;
123
141
  private runHeartbeat;
124
142
  /**
125
143
  * Has this socket answered recently enough to be treated as present?
@@ -1 +1 @@
1
- {"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"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"}
@@ -10,7 +10,7 @@ import { directionOf, parseInbound } from '@tapflowio/protocol/validate';
10
10
  import { Router, json } from './router.js';
11
11
  import { requireViewAuth, requireAuth, getAuth, verifyPat } from './middleware/auth.js';
12
12
  import { classifyConnection } from './lib/connectionAuth.js';
13
- import { resolveClientAddress } from './lib/clientAddress.js';
13
+ import { isTunnelIngress, markTunnelIngress, resolveClientAddress } from './lib/clientAddress.js';
14
14
  import { BuildTicketStore } from './lib/buildTickets.js';
15
15
  import { resolveCorsHeaders } from './lib/cors.js';
16
16
  import { isCsrfBlocked } from './lib/csrf.js';
@@ -156,6 +156,20 @@ function describeRejection(failure, suppressed) {
156
156
  export class RelayServer {
157
157
  options;
158
158
  httpServer;
159
+ /**
160
+ * **The port tunnel clients are pointed at, and the only thing that tells their traffic apart.**
161
+ * rathole's client and `tailscale serve` run on this machine and connect from loopback on behalf of
162
+ * someone on the internet or the tailnet; rathole forwards raw TCP, so no header marks it either. On
163
+ * the main port such a socket is indistinguishable from `tapflow start`'s own agent, which is
164
+ * unauthenticated by design — so every tunnel visitor was too. Everything accepted here is remote
165
+ * (`resolveClientAddress`'s `viaTunnel`), whatever its address.
166
+ *
167
+ * Loopback-only, because only a tunnel client on this machine has any business here; a remote client
168
+ * belongs on the main port, where it is classified by address as before. Same handler, same
169
+ * `WebSocketServer`, same TLS material — rathole carried a TLS relay's own handshake through before
170
+ * this existed, and still does.
171
+ */
172
+ tunnelServer = null;
159
173
  wss;
160
174
  sessions;
161
175
  publicDir;
@@ -240,6 +254,10 @@ export class RelayServer {
240
254
  pendingUITrees = new Map();
241
255
  constructor(options) {
242
256
  this.options = options;
257
+ // 0 on both sides is two ephemeral ports, which is what the tests ask for.
258
+ if (options.tunnelPort !== undefined && options.tunnelPort !== 0 && options.tunnelPort === options.port) {
259
+ throw new Error(`The tunnel port (${options.tunnelPort}) must differ from the relay port. Set TAPFLOW_TUNNEL_PORT to another port.`);
260
+ }
243
261
  this.backpressureBytes = options.wsBackpressureBytes ?? DEFAULT_BACKPRESSURE_BYTES;
244
262
  this.screenshotTimeoutMs = options.screenshotTimeoutMs ?? 10_000;
245
263
  // Longer than the screenshot default: the Android agent's device-side dump
@@ -280,6 +298,24 @@ export class RelayServer {
280
298
  this.wss = new WebSocketServer({ server: this.httpServer });
281
299
  this.wss.on('connection', (ws, request) => this.handleConnection(ws, request));
282
300
  this.wss.on('error', () => { });
301
+ if (options.tunnelPort !== undefined) {
302
+ // Marked before anything reads it: the router hands this same request object to `handleInit`, and
303
+ // `handleUpgrade` passes it on to the `connection` event.
304
+ const tunnelHandler = (req, res) => {
305
+ markTunnelIngress(req);
306
+ return handler(req, res);
307
+ };
308
+ const tunnelServer = options.tls
309
+ ? https.createServer({ cert: options.tls.cert, key: options.tls.key }, tunnelHandler)
310
+ : http.createServer(tunnelHandler);
311
+ tunnelServer.on('connection', (socket) => socket.setNoDelay(true));
312
+ // `handleUpgrade` adds the socket to `wss.clients`, so the heartbeat and `stop()` cover it.
313
+ tunnelServer.on('upgrade', (req, socket, head) => {
314
+ markTunnelIngress(req);
315
+ this.wss.handleUpgrade(req, socket, head, (ws) => this.wss.emit('connection', ws, req));
316
+ });
317
+ this.tunnelServer = tunnelServer;
318
+ }
283
319
  }
284
320
  registerRoutes() {
285
321
  const u = this.uploadsDir;
@@ -327,7 +363,7 @@ export class RelayServer {
327
363
  this.router.post('/api/v1/team/invite', (req, res) => handleInvite(req, res, this.options.tunnel));
328
364
  this.router.patch('/api/v1/team/members/:id', handleUpdateMember);
329
365
  this.router.delete('/api/v1/team/members/:id', handleDeleteMember);
330
- this.router.post('/api/v1/team/members/:id/send-reset', handleSendMemberReset);
366
+ this.router.post('/api/v1/team/members/:id/send-reset', (req, res, params) => handleSendMemberReset(req, res, params, this.options.tunnel));
331
367
  // tokens
332
368
  this.router.get('/api/v1/tokens', handleListTokens);
333
369
  this.router.post('/api/v1/tokens', handleCreateToken);
@@ -407,7 +443,30 @@ export class RelayServer {
407
443
  });
408
444
  // Bind dual-stack (IPv4 + IPv6). A bare listen(port) binds IPv6-only on some
409
445
  // macOS/node setups, so LAN agents connecting over IPv4 (ws://<ipv4>:port) time out.
410
- this.httpServer.listen({ port: this.options.port, host: '::', ipv6Only: false }, resolve);
446
+ this.httpServer.listen({ port: this.options.port, host: '::', ipv6Only: false }, () => resolve());
447
+ }).then(() => this.listenTunnel());
448
+ }
449
+ listenTunnel() {
450
+ const tunnelServer = this.tunnelServer;
451
+ if (!tunnelServer)
452
+ return Promise.resolve();
453
+ const port = this.options.tunnelPort ?? 0;
454
+ return new Promise((resolve, reject) => {
455
+ tunnelServer.once('error', (err) => reject(err.code === 'EADDRINUSE'
456
+ ? new Error(`Tunnel port ${port} is already in use. Stop the process holding it, or set TAPFLOW_TUNNEL_PORT to a free port.`)
457
+ : err));
458
+ // 127.0.0.1, not `localhost`: rathole's client dials that address literally, and `tailscale serve
459
+ // <port>` proxies to it.
460
+ tunnelServer.listen({ port, host: '127.0.0.1' }, () => {
461
+ // The main server's later errors reach `wss`, which swallows them; nothing forwards this one's.
462
+ tunnelServer.on('error', (err) => logger.error(`tunnel listener error: ${String(err)}`));
463
+ resolve();
464
+ });
465
+ }).catch(async (err) => {
466
+ // A relay whose tunnel port failed must not keep serving on the main one: the caller treats this
467
+ // as a failed start and moves on, and the port would stay held by a server nobody stops.
468
+ await this.stop().catch(() => { });
469
+ throw err;
411
470
  });
412
471
  }
413
472
  stop() {
@@ -445,16 +504,27 @@ export class RelayServer {
445
504
  for (const requester of this.networkStateRequesters.values())
446
505
  requester.dispose();
447
506
  this.networkStateRequesters.clear();
448
- return new Promise((resolve, reject) => {
507
+ const closeTunnel = new Promise((resolve, reject) => {
508
+ // Not listening means `start()` never reached it, or its listen failed; `close()` would reject.
509
+ if (!this.tunnelServer?.listening)
510
+ return resolve();
511
+ this.tunnelServer.close((err) => (err ? reject(err) : resolve()));
512
+ });
513
+ const closeMain = new Promise((resolve, reject) => {
449
514
  this.wss.clients.forEach((ws) => ws.terminate());
450
515
  this.wss.close(() => {
451
516
  this.httpServer.close((err) => (err ? reject(err) : resolve()));
452
517
  });
453
518
  });
519
+ return Promise.all([closeMain, closeTunnel]).then(() => undefined);
454
520
  }
455
521
  address() {
456
522
  return this.httpServer.address();
457
523
  }
524
+ /** The tunnel listener's address, or null when this relay was given no `tunnelPort`. */
525
+ tunnelAddress() {
526
+ return this.tunnelServer?.address() ?? null;
527
+ }
458
528
  // Terminate sockets that missed the previous pong; ping the rest. Covers all roles via wss.clients.
459
529
  runHeartbeat(clients = this.wss.clients) {
460
530
  for (const ws of clients) {
@@ -488,8 +558,10 @@ export class RelayServer {
488
558
  }
489
559
  // 갱신된 cert를 재시작 없이 핫스왑한다(https 종단일 때만 의미 있음).
490
560
  updateTlsContext(material) {
491
- if (this.httpServer instanceof https.Server) {
492
- this.httpServer.setSecureContext({ cert: material.cert, key: material.key });
561
+ for (const server of [this.httpServer, this.tunnelServer]) {
562
+ if (server instanceof https.Server) {
563
+ server.setSecureContext({ cert: material.cert, key: material.key });
564
+ }
493
565
  }
494
566
  }
495
567
  /**
@@ -604,8 +676,9 @@ export class RelayServer {
604
676
  const a = socketAddr.replace(/^::ffff:/, '');
605
677
  if (a === '::1' || a.startsWith('127.')) {
606
678
  logger.warn('Received X-Forwarded-For from a loopback connection but TAPFLOW_TRUSTED_PROXIES is unset. ' +
607
- 'If the relay runs behind a same-host reverse proxy, set TAPFLOW_TRUSTED_PROXIES so the real ' +
608
- 'client IP is used — otherwise every proxied client is treated as localhost (unauthenticated).');
679
+ 'If a same-host reverse proxy or `tailscale serve` forwards to this port, every client it forwards is ' +
680
+ 'treated as localhost (unauthenticated). Point it at the tunnel port instead (TAPFLOW_TUNNEL_PORT, ' +
681
+ 'default 4001), or set TAPFLOW_TRUSTED_PROXIES so the real client IP is used.');
609
682
  this.warnedProxyMisconfig = true;
610
683
  }
611
684
  }
@@ -613,11 +686,15 @@ export class RelayServer {
613
686
  const socketAddr = this.remoteAddressOf(request);
614
687
  const xff = request.headers['x-forwarded-for'];
615
688
  const forwardedFor = Array.isArray(xff) ? xff[0] : xff;
616
- this.warnProxyMisconfigOnce(socketAddr, forwardedFor);
689
+ const viaTunnel = isTunnelIngress(request);
690
+ // A proxy in front of the tunnel port adds the header too, and there it changes nothing about auth.
691
+ if (!viaTunnel)
692
+ this.warnProxyMisconfigOnce(socketAddr, forwardedFor);
617
693
  const { addr, isLocal } = resolveClientAddress({
618
694
  socketAddr,
619
695
  forwardedFor,
620
696
  trustedProxies: this.options.trustedProxies ?? [],
697
+ viaTunnel,
621
698
  });
622
699
  const hasCookieAuth = getAuth(request) !== null;
623
700
  // DB lookup — only when the connection can't be classified without it (remote, no cookie).
@@ -964,15 +1041,32 @@ export class RelayServer {
964
1041
  // two-member union** while `AgentRegister.platform` is `string`, open so that a third-party
965
1042
  // platform can register through `AgentRegistry.register()` (root AGENTS.md, OCP). Validating
966
1043
  // this would refuse a platform the repo promises to support — and refusing costs more than a
967
- // dropped frame, because the message arrives once per boot and skipping `setChromeData` also
968
- // empties what the re-join replay reads. The relay never looks inside; the viewer does, and it
969
- // is where the variants are already told apart.
1044
+ // dropped frame: skipping `setChromeData` also
1045
+ // empties what the re-join replay reads. It is no longer true that this arrives once per boot,
1046
+ // either — a foldable re-sends it when the screen changes under the session — so the cost of
1047
+ // a refusal is a viewer stuck with the previous screen's bezel rather than one frame.
1048
+ // The relay never looks inside; the viewer does, and it is where the variants are already
1049
+ // told apart.
970
1050
  this.sessions.setChromeData(session.id, raw['payload']);
971
1051
  if (session.browserSocket?.readyState === WebSocket.OPEN) {
972
1052
  session.browserSocket.send(JSON.stringify(raw));
973
1053
  }
974
1054
  break;
975
1055
  }
1056
+ case 'device:postures': {
1057
+ const session = this.sessions.get(msg.sessionId);
1058
+ if (!session)
1059
+ break;
1060
+ // Cached for the same reason `session:chrome` is: a viewer that re-joins (a tab reload, a
1061
+ // socket blip) gets the session's state replayed, and without this the posture control would
1062
+ // simply be missing until the device was rebooted — with the picture still fine, which is
1063
+ // the hardest kind of gap to diagnose.
1064
+ this.sessions.setPostures(session.id, raw['payload']);
1065
+ if (session.browserSocket?.readyState === WebSocket.OPEN) {
1066
+ session.browserSocket.send(JSON.stringify(raw));
1067
+ }
1068
+ break;
1069
+ }
976
1070
  case 'session:deviceInfo': {
977
1071
  const session = this.sessions.get(msg.sessionId);
978
1072
  if (!session)
@@ -1228,6 +1322,7 @@ export class RelayServer {
1228
1322
  case 'input:pinch:start':
1229
1323
  case 'input:pinch:move':
1230
1324
  case 'input:rotate':
1325
+ case 'input:posture':
1231
1326
  case 'input:keyboard:toggle': {
1232
1327
  // No ack, so no correlator to check and nothing to answer. An unowned frame is dropped rather
1233
1328
  // than refused for the same reason: there is no waiter to tell.
@@ -1627,6 +1722,9 @@ export class RelayServer {
1627
1722
  if (session.deviceInfo) {
1628
1723
  this.sendTo(ws, { type: 'session:deviceInfo', sessionId: session.id, payload: session.deviceInfo });
1629
1724
  }
1725
+ if (session.postures) {
1726
+ this.sendTo(ws, { type: 'device:postures', sessionId: session.id, payload: session.postures });
1727
+ }
1630
1728
  // Replay device:ready only if this session actually announced one (browser WS blip reconnect).
1631
1729
  // Not `deviceStatus`: that starts from the agent's `simctl list` snapshot, so a session for a
1632
1730
  // simulator that was already running would fire this before the agent had done anything —