@tapflowio/relay 0.21.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 (182) hide show
  1. package/dist/RelayServer.d.ts +20 -0
  2. package/dist/RelayServer.d.ts.map +1 -1
  3. package/dist/RelayServer.js +124 -16
  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/api/team.d.ts +2 -1
  17. package/dist/api/team.d.ts.map +1 -1
  18. package/dist/api/team.js +7 -5
  19. package/dist/api/team.js.map +1 -1
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/clientAddress.d.ts +15 -0
  25. package/dist/lib/clientAddress.d.ts.map +1 -1
  26. package/dist/lib/clientAddress.js +21 -0
  27. package/dist/lib/clientAddress.js.map +1 -1
  28. package/dist/lib/config.d.ts +2 -0
  29. package/dist/lib/config.d.ts.map +1 -1
  30. package/dist/lib/config.js +6 -0
  31. package/dist/lib/config.js.map +1 -1
  32. package/dist/lib/lanAddress.d.ts +10 -0
  33. package/dist/lib/lanAddress.d.ts.map +1 -1
  34. package/dist/lib/lanAddress.js +13 -0
  35. package/dist/lib/lanAddress.js.map +1 -1
  36. package/dist/lib/proxyConfig.d.ts +14 -2
  37. package/dist/lib/proxyConfig.d.ts.map +1 -1
  38. package/dist/lib/proxyConfig.js +36 -13
  39. package/dist/lib/proxyConfig.js.map +1 -1
  40. package/dist/lib/publicUrl.d.ts +31 -1
  41. package/dist/lib/publicUrl.d.ts.map +1 -1
  42. package/dist/lib/publicUrl.js +51 -9
  43. package/dist/lib/publicUrl.js.map +1 -1
  44. package/dist/server.js +10 -2
  45. package/dist/server.js.map +1 -1
  46. package/package.json +5 -5
  47. package/public/assets/AppCenter-OTDuqwSf.js +21 -0
  48. package/public/assets/AppCenter-OTDuqwSf.js.br +0 -0
  49. package/public/assets/AppCenter-OTDuqwSf.js.gz +0 -0
  50. package/public/assets/{Default-D1inA8sU.js → Default-D9GLpWlN.js} +1 -1
  51. package/public/assets/Default-D9GLpWlN.js.br +0 -0
  52. package/public/assets/Default-D9GLpWlN.js.gz +0 -0
  53. package/public/assets/{Invite-ChaVEWvP.js → Invite-Bhiam8En.js} +1 -1
  54. package/public/assets/Invite-Bhiam8En.js.br +0 -0
  55. package/public/assets/Invite-Bhiam8En.js.gz +0 -0
  56. package/public/assets/MacResources-DObzVJxQ.js +3 -0
  57. package/public/assets/MacResources-DObzVJxQ.js.br +0 -0
  58. package/public/assets/MacResources-DObzVJxQ.js.gz +0 -0
  59. package/public/assets/{NotFound-BoDDivK5.js → NotFound-Dz_k_Mzf.js} +1 -1
  60. package/public/assets/NotFound-Dz_k_Mzf.js.br +0 -0
  61. package/public/assets/NotFound-Dz_k_Mzf.js.gz +0 -0
  62. package/public/assets/QASession-2crubOLY.js +150 -0
  63. package/public/assets/QASession-2crubOLY.js.br +0 -0
  64. package/public/assets/QASession-2crubOLY.js.gz +0 -0
  65. package/public/assets/{ResetPassword-Ut9BFums.js → ResetPassword-EUm12ojo.js} +1 -1
  66. package/public/assets/ResetPassword-EUm12ojo.js.br +0 -0
  67. package/public/assets/ResetPassword-EUm12ojo.js.gz +0 -0
  68. package/public/assets/{Setup-CoHwaxo7.js → Setup-Fc_0RW0a.js} +1 -1
  69. package/public/assets/Setup-Fc_0RW0a.js.br +0 -0
  70. package/public/assets/Setup-Fc_0RW0a.js.gz +0 -0
  71. package/public/assets/Team-Ac5aOGgx.js +6 -0
  72. package/public/assets/Team-Ac5aOGgx.js.br +0 -0
  73. package/public/assets/Team-Ac5aOGgx.js.gz +0 -0
  74. package/public/assets/Tokens-pEnCy-7l.js +1 -0
  75. package/public/assets/Tokens-pEnCy-7l.js.br +0 -0
  76. package/public/assets/Tokens-pEnCy-7l.js.gz +0 -0
  77. package/public/assets/alert-dialog-DhOTocMh.js +1 -0
  78. package/public/assets/alert-dialog-DhOTocMh.js.br +0 -0
  79. package/public/assets/alert-dialog-DhOTocMh.js.gz +0 -0
  80. package/public/assets/{badge-CikXk90N.js → badge-B4gbViMs.js} +1 -1
  81. package/public/assets/badge-B4gbViMs.js.br +0 -0
  82. package/public/assets/badge-B4gbViMs.js.gz +0 -0
  83. package/public/assets/build-format-DKGajT96.js +11 -0
  84. package/public/assets/build-format-DKGajT96.js.br +0 -0
  85. package/public/assets/build-format-DKGajT96.js.gz +0 -0
  86. package/public/assets/dialog-DEL1xxKJ.js +11 -0
  87. package/public/assets/dialog-DEL1xxKJ.js.br +0 -0
  88. package/public/assets/dialog-DEL1xxKJ.js.gz +0 -0
  89. package/public/assets/index-Bxs7CdEi.css +1 -0
  90. package/public/assets/index-Bxs7CdEi.css.br +0 -0
  91. package/public/assets/index-Bxs7CdEi.css.gz +0 -0
  92. package/public/assets/index-COIlCLCW.js +218 -0
  93. package/public/assets/index-COIlCLCW.js.br +0 -0
  94. package/public/assets/index-COIlCLCW.js.gz +0 -0
  95. package/public/assets/pencil-CPaoEKFm.js +6 -0
  96. package/public/assets/pencil-CPaoEKFm.js.br +0 -0
  97. package/public/assets/pencil-CPaoEKFm.js.gz +0 -0
  98. package/public/assets/publicLink-D6zKS_wu.js +1 -0
  99. package/public/assets/publicLink-D6zKS_wu.js.br +0 -0
  100. package/public/assets/publicLink-D6zKS_wu.js.gz +0 -0
  101. package/public/assets/resource-health-Ca4-5Rde.js +1 -0
  102. package/public/assets/resource-health-Ca4-5Rde.js.br +0 -0
  103. package/public/assets/resource-health-Ca4-5Rde.js.gz +0 -0
  104. package/public/assets/{table-dbLmBL_w.js → table-CSeGVnii.js} +1 -1
  105. package/public/assets/table-CSeGVnii.js.br +0 -0
  106. package/public/assets/table-CSeGVnii.js.gz +0 -0
  107. package/public/assets/trash-CD9Y9G-i.js +11 -0
  108. package/public/assets/trash-CD9Y9G-i.js.br +0 -0
  109. package/public/assets/trash-CD9Y9G-i.js.gz +0 -0
  110. package/public/assets/vendor-charts-YPhPkjLw.js +5 -0
  111. package/public/assets/vendor-charts-YPhPkjLw.js.br +0 -0
  112. package/public/assets/vendor-charts-YPhPkjLw.js.gz +0 -0
  113. package/public/assets/vendor-react-C5H_1fOn.js +49 -0
  114. package/public/assets/vendor-react-C5H_1fOn.js.br +0 -0
  115. package/public/assets/vendor-react-C5H_1fOn.js.gz +0 -0
  116. package/public/assets/{vendor-router-BFibuab1.js → vendor-router-CnmrRwJF.js} +1 -1
  117. package/public/assets/vendor-router-CnmrRwJF.js.br +0 -0
  118. package/public/assets/vendor-router-CnmrRwJF.js.gz +0 -0
  119. package/public/index.html +4 -4
  120. package/public/index.html.br +0 -0
  121. package/public/index.html.gz +0 -0
  122. package/public/assets/AppCenter-BkSQFKRv.js +0 -21
  123. package/public/assets/AppCenter-BkSQFKRv.js.br +0 -0
  124. package/public/assets/AppCenter-BkSQFKRv.js.gz +0 -0
  125. package/public/assets/Default-D1inA8sU.js.br +0 -0
  126. package/public/assets/Default-D1inA8sU.js.gz +0 -0
  127. package/public/assets/Invite-ChaVEWvP.js.br +0 -0
  128. package/public/assets/Invite-ChaVEWvP.js.gz +0 -0
  129. package/public/assets/MacResources-CLChFyJW.js +0 -1
  130. package/public/assets/MacResources-CLChFyJW.js.br +0 -0
  131. package/public/assets/MacResources-CLChFyJW.js.gz +0 -0
  132. package/public/assets/NotFound-BoDDivK5.js.br +0 -0
  133. package/public/assets/NotFound-BoDDivK5.js.gz +0 -0
  134. package/public/assets/QASession-DICynZa0.js +0 -140
  135. package/public/assets/QASession-DICynZa0.js.br +0 -0
  136. package/public/assets/QASession-DICynZa0.js.gz +0 -0
  137. package/public/assets/ResetPassword-Ut9BFums.js.br +0 -0
  138. package/public/assets/ResetPassword-Ut9BFums.js.gz +0 -0
  139. package/public/assets/Setup-CoHwaxo7.js.br +0 -0
  140. package/public/assets/Setup-CoHwaxo7.js.gz +0 -0
  141. package/public/assets/Team-DaVY1DGm.js +0 -6
  142. package/public/assets/Team-DaVY1DGm.js.br +0 -0
  143. package/public/assets/Team-DaVY1DGm.js.gz +0 -0
  144. package/public/assets/Tokens-_KZJ7AZ9.js +0 -1
  145. package/public/assets/Tokens-_KZJ7AZ9.js.br +0 -0
  146. package/public/assets/Tokens-_KZJ7AZ9.js.gz +0 -0
  147. package/public/assets/alert-dialog-Cc_pQpe6.js +0 -1
  148. package/public/assets/alert-dialog-Cc_pQpe6.js.br +0 -0
  149. package/public/assets/alert-dialog-Cc_pQpe6.js.gz +0 -0
  150. package/public/assets/badge-CikXk90N.js.br +0 -0
  151. package/public/assets/badge-CikXk90N.js.gz +0 -0
  152. package/public/assets/build-format-DsApFPHy.js +0 -11
  153. package/public/assets/build-format-DsApFPHy.js.br +0 -0
  154. package/public/assets/build-format-DsApFPHy.js.gz +0 -0
  155. package/public/assets/dialog-B4Wg7TCg.js +0 -11
  156. package/public/assets/dialog-B4Wg7TCg.js.br +0 -0
  157. package/public/assets/dialog-B4Wg7TCg.js.gz +0 -0
  158. package/public/assets/index-CBfjqtE-.js +0 -198
  159. package/public/assets/index-CBfjqtE-.js.br +0 -0
  160. package/public/assets/index-CBfjqtE-.js.gz +0 -0
  161. package/public/assets/index-ClHUQm-F.css +0 -1
  162. package/public/assets/index-ClHUQm-F.css.br +0 -0
  163. package/public/assets/index-ClHUQm-F.css.gz +0 -0
  164. package/public/assets/pencil-DMswV31e.js +0 -6
  165. package/public/assets/pencil-DMswV31e.js.br +0 -0
  166. package/public/assets/pencil-DMswV31e.js.gz +0 -0
  167. package/public/assets/table-dbLmBL_w.js.br +0 -0
  168. package/public/assets/table-dbLmBL_w.js.gz +0 -0
  169. package/public/assets/tabs-pPYCRfmT.js +0 -1
  170. package/public/assets/tabs-pPYCRfmT.js.br +0 -0
  171. package/public/assets/tabs-pPYCRfmT.js.gz +0 -0
  172. package/public/assets/trash-VgeHmI8N.js +0 -11
  173. package/public/assets/trash-VgeHmI8N.js.br +0 -0
  174. package/public/assets/trash-VgeHmI8N.js.gz +0 -0
  175. package/public/assets/vendor-charts-DyX5CBcf.js +0 -5
  176. package/public/assets/vendor-charts-DyX5CBcf.js.br +0 -0
  177. package/public/assets/vendor-charts-DyX5CBcf.js.gz +0 -0
  178. package/public/assets/vendor-react-AJpf4YZ0.js +0 -49
  179. package/public/assets/vendor-react-AJpf4YZ0.js.br +0 -0
  180. package/public/assets/vendor-react-AJpf4YZ0.js.gz +0 -0
  181. package/public/assets/vendor-router-BFibuab1.js.br +0 -0
  182. package/public/assets/vendor-router-BFibuab1.js.gz +0 -0
@@ -1,3 +1,4 @@
1
+ import { type TunnelRuntime } from './lib/publicUrl.js';
1
2
  export declare function isExternalAddress(addr: string): boolean;
2
3
  /** Exported for the re-join test, which pins both edges of this window rather than hardcoding 500 —
3
4
  * a test carrying its own copy of the number passes when the constant moves, which is the drift the
@@ -29,6 +30,20 @@ export declare function ownerKeyFor(userId: number | string | undefined, claimed
29
30
  export declare class RelayServer {
30
31
  private readonly options;
31
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;
32
47
  private wss;
33
48
  private sessions;
34
49
  private publicDir;
@@ -112,12 +127,17 @@ export declare class RelayServer {
112
127
  key: string;
113
128
  };
114
129
  agentGraceMs?: number;
130
+ tunnel?: TunnelRuntime;
131
+ tunnelPort?: number;
115
132
  });
116
133
  private registerRoutes;
117
134
  pushLog(msg: string): void;
118
135
  start(): Promise<void>;
136
+ private listenTunnel;
119
137
  stop(): Promise<void>;
120
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;
121
141
  private runHeartbeat;
122
142
  /**
123
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,11 +10,13 @@ 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';
17
- import { pickLanAddress } from './lib/lanAddress.js';
17
+ import { pickLanAddress, runningInContainer } from './lib/lanAddress.js';
18
+ import { config } from './lib/config.js';
19
+ import { forTeammates, resolveAgentRelayUrl, resolvePublicBaseUrl } from './lib/publicUrl.js';
18
20
  import { createTrailingRequester, systemTimerScheduler } from './lib/trailingRequester.js';
19
21
  import { getDb } from './db.js';
20
22
  import { handleLogin, handleLogout, handleMe, handleChangePassword, handleInit, handleAuthStatus } from './api/auth.js';
@@ -154,6 +156,20 @@ function describeRejection(failure, suppressed) {
154
156
  export class RelayServer {
155
157
  options;
156
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;
157
173
  wss;
158
174
  sessions;
159
175
  publicDir;
@@ -238,6 +254,10 @@ export class RelayServer {
238
254
  pendingUITrees = new Map();
239
255
  constructor(options) {
240
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
+ }
241
261
  this.backpressureBytes = options.wsBackpressureBytes ?? DEFAULT_BACKPRESSURE_BYTES;
242
262
  this.screenshotTimeoutMs = options.screenshotTimeoutMs ?? 10_000;
243
263
  // Longer than the screenshot default: the Android agent's device-side dump
@@ -278,6 +298,24 @@ export class RelayServer {
278
298
  this.wss = new WebSocketServer({ server: this.httpServer });
279
299
  this.wss.on('connection', (ws, request) => this.handleConnection(ws, request));
280
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
+ }
281
319
  }
282
320
  registerRoutes() {
283
321
  const u = this.uploadsDir;
@@ -322,10 +360,10 @@ export class RelayServer {
322
360
  this.router.delete('/api/v1/comments/:id', handleDeleteComment);
323
361
  // team
324
362
  this.router.get('/api/v1/team/members', handleListMembers);
325
- this.router.post('/api/v1/team/invite', handleInvite);
363
+ this.router.post('/api/v1/team/invite', (req, res) => handleInvite(req, res, this.options.tunnel));
326
364
  this.router.patch('/api/v1/team/members/:id', handleUpdateMember);
327
365
  this.router.delete('/api/v1/team/members/:id', handleDeleteMember);
328
- 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));
329
367
  // tokens
330
368
  this.router.get('/api/v1/tokens', handleListTokens);
331
369
  this.router.post('/api/v1/tokens', handleCreateToken);
@@ -346,14 +384,22 @@ export class RelayServer {
346
384
  res.writeHead(200, { 'Content-Type': 'application/json' });
347
385
  res.end(JSON.stringify(this.logBuffer.slice(-lines)));
348
386
  });
349
- // relay host — 대시보드가 agent 실행 커맨드에 박을 LAN 주소 (뷰어가 localhost로 접속한 경우의 치환용, #271)
387
+ // What the dashboard needs to build an address for someone else (#271, #788): a link base for a
388
+ // teammate's browser and an agent address, reported separately because they come from different
389
+ // settings (see lib/publicUrl.ts). Config comes from the singleton, as in the team handlers; whether a
390
+ // tunnel actually came up is known only to the entry point that started it, hence the `tunnel` option.
350
391
  this.router.get('/api/v1/relay/host', (req, res) => {
351
392
  if (!requireAuth(req, res))
352
393
  return;
353
394
  const addr = this.httpServer.address();
354
395
  const port = typeof addr === 'object' && addr !== null ? addr.port : this.options.port;
355
396
  res.writeHead(200, { 'Content-Type': 'application/json' });
356
- res.end(JSON.stringify({ lanHost: pickLanAddress(os.networkInterfaces()), port }));
397
+ res.end(JSON.stringify({
398
+ lanHost: runningInContainer() ? null : pickLanAddress(os.networkInterfaces()),
399
+ port,
400
+ publicBaseUrl: forTeammates(resolvePublicBaseUrl(config, this.options.tunnel)),
401
+ agentRelayUrl: forTeammates(resolveAgentRelayUrl(config)),
402
+ }));
357
403
  });
358
404
  // agent resources
359
405
  this.router.get('/api/v1/agents', handleListAgents);
@@ -397,7 +443,30 @@ export class RelayServer {
397
443
  });
398
444
  // Bind dual-stack (IPv4 + IPv6). A bare listen(port) binds IPv6-only on some
399
445
  // macOS/node setups, so LAN agents connecting over IPv4 (ws://<ipv4>:port) time out.
400
- 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;
401
470
  });
402
471
  }
403
472
  stop() {
@@ -435,16 +504,27 @@ export class RelayServer {
435
504
  for (const requester of this.networkStateRequesters.values())
436
505
  requester.dispose();
437
506
  this.networkStateRequesters.clear();
438
- 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) => {
439
514
  this.wss.clients.forEach((ws) => ws.terminate());
440
515
  this.wss.close(() => {
441
516
  this.httpServer.close((err) => (err ? reject(err) : resolve()));
442
517
  });
443
518
  });
519
+ return Promise.all([closeMain, closeTunnel]).then(() => undefined);
444
520
  }
445
521
  address() {
446
522
  return this.httpServer.address();
447
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
+ }
448
528
  // Terminate sockets that missed the previous pong; ping the rest. Covers all roles via wss.clients.
449
529
  runHeartbeat(clients = this.wss.clients) {
450
530
  for (const ws of clients) {
@@ -478,8 +558,10 @@ export class RelayServer {
478
558
  }
479
559
  // 갱신된 cert를 재시작 없이 핫스왑한다(https 종단일 때만 의미 있음).
480
560
  updateTlsContext(material) {
481
- if (this.httpServer instanceof https.Server) {
482
- 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
+ }
483
565
  }
484
566
  }
485
567
  /**
@@ -594,8 +676,9 @@ export class RelayServer {
594
676
  const a = socketAddr.replace(/^::ffff:/, '');
595
677
  if (a === '::1' || a.startsWith('127.')) {
596
678
  logger.warn('Received X-Forwarded-For from a loopback connection but TAPFLOW_TRUSTED_PROXIES is unset. ' +
597
- 'If the relay runs behind a same-host reverse proxy, set TAPFLOW_TRUSTED_PROXIES so the real ' +
598
- '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.');
599
682
  this.warnedProxyMisconfig = true;
600
683
  }
601
684
  }
@@ -603,11 +686,15 @@ export class RelayServer {
603
686
  const socketAddr = this.remoteAddressOf(request);
604
687
  const xff = request.headers['x-forwarded-for'];
605
688
  const forwardedFor = Array.isArray(xff) ? xff[0] : xff;
606
- 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);
607
693
  const { addr, isLocal } = resolveClientAddress({
608
694
  socketAddr,
609
695
  forwardedFor,
610
696
  trustedProxies: this.options.trustedProxies ?? [],
697
+ viaTunnel,
611
698
  });
612
699
  const hasCookieAuth = getAuth(request) !== null;
613
700
  // DB lookup — only when the connection can't be classified without it (remote, no cookie).
@@ -954,15 +1041,32 @@ export class RelayServer {
954
1041
  // two-member union** while `AgentRegister.platform` is `string`, open so that a third-party
955
1042
  // platform can register through `AgentRegistry.register()` (root AGENTS.md, OCP). Validating
956
1043
  // this would refuse a platform the repo promises to support — and refusing costs more than a
957
- // dropped frame, because the message arrives once per boot and skipping `setChromeData` also
958
- // empties what the re-join replay reads. The relay never looks inside; the viewer does, and it
959
- // 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.
960
1050
  this.sessions.setChromeData(session.id, raw['payload']);
961
1051
  if (session.browserSocket?.readyState === WebSocket.OPEN) {
962
1052
  session.browserSocket.send(JSON.stringify(raw));
963
1053
  }
964
1054
  break;
965
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
+ }
966
1070
  case 'session:deviceInfo': {
967
1071
  const session = this.sessions.get(msg.sessionId);
968
1072
  if (!session)
@@ -1218,6 +1322,7 @@ export class RelayServer {
1218
1322
  case 'input:pinch:start':
1219
1323
  case 'input:pinch:move':
1220
1324
  case 'input:rotate':
1325
+ case 'input:posture':
1221
1326
  case 'input:keyboard:toggle': {
1222
1327
  // No ack, so no correlator to check and nothing to answer. An unowned frame is dropped rather
1223
1328
  // than refused for the same reason: there is no waiter to tell.
@@ -1617,6 +1722,9 @@ export class RelayServer {
1617
1722
  if (session.deviceInfo) {
1618
1723
  this.sendTo(ws, { type: 'session:deviceInfo', sessionId: session.id, payload: session.deviceInfo });
1619
1724
  }
1725
+ if (session.postures) {
1726
+ this.sendTo(ws, { type: 'device:postures', sessionId: session.id, payload: session.postures });
1727
+ }
1620
1728
  // Replay device:ready only if this session actually announced one (browser WS blip reconnect).
1621
1729
  // Not `deviceStatus`: that starts from the agent's `simctl list` snapshot, so a session for a
1622
1730
  // simulator that was already running would fire this before the agent had done anything —