@tapflowio/relay 0.22.0 → 0.24.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 (191) hide show
  1. package/dist/RelayServer.d.ts +18 -0
  2. package/dist/RelayServer.d.ts.map +1 -1
  3. package/dist/RelayServer.js +128 -20
  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 +1 -1
  10. package/dist/api/auth.d.ts.map +1 -1
  11. package/dist/api/auth.js +13 -4
  12. package/dist/api/auth.js.map +1 -1
  13. package/dist/api/builds.d.ts +1 -1
  14. package/dist/api/builds.d.ts.map +1 -1
  15. package/dist/api/builds.js +8 -2
  16. package/dist/api/builds.js.map +1 -1
  17. package/dist/api/passwordReset.d.ts +6 -2
  18. package/dist/api/passwordReset.d.ts.map +1 -1
  19. package/dist/api/passwordReset.js +21 -6
  20. package/dist/api/passwordReset.js.map +1 -1
  21. package/dist/index.d.ts +5 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/buildFiles.d.ts +23 -0
  26. package/dist/lib/buildFiles.d.ts.map +1 -0
  27. package/dist/lib/buildFiles.js +39 -0
  28. package/dist/lib/buildFiles.js.map +1 -0
  29. package/dist/lib/clientAddress.d.ts +15 -0
  30. package/dist/lib/clientAddress.d.ts.map +1 -1
  31. package/dist/lib/clientAddress.js +21 -0
  32. package/dist/lib/clientAddress.js.map +1 -1
  33. package/dist/lib/config.d.ts +28 -1
  34. package/dist/lib/config.d.ts.map +1 -1
  35. package/dist/lib/config.js +87 -17
  36. package/dist/lib/config.js.map +1 -1
  37. package/dist/lib/dataDir.d.ts +48 -1
  38. package/dist/lib/dataDir.d.ts.map +1 -1
  39. package/dist/lib/dataDir.js +108 -12
  40. package/dist/lib/dataDir.js.map +1 -1
  41. package/dist/lib/proxyConfig.d.ts +10 -0
  42. package/dist/lib/proxyConfig.d.ts.map +1 -1
  43. package/dist/lib/proxyConfig.js +14 -0
  44. package/dist/lib/proxyConfig.js.map +1 -1
  45. package/dist/middleware/auth.js +3 -3
  46. package/dist/middleware/auth.js.map +1 -1
  47. package/dist/server.js +5 -2
  48. package/dist/server.js.map +1 -1
  49. package/package.json +5 -5
  50. package/public/assets/AppCenter-BHz6vh5p.js +26 -0
  51. package/public/assets/AppCenter-BHz6vh5p.js.br +0 -0
  52. package/public/assets/AppCenter-BHz6vh5p.js.gz +0 -0
  53. package/public/assets/Default-bCqU0Qz9.js +1 -0
  54. package/public/assets/Default-bCqU0Qz9.js.br +0 -0
  55. package/public/assets/Default-bCqU0Qz9.js.gz +0 -0
  56. package/public/assets/Invite-EVMTsuNz.js +1 -0
  57. package/public/assets/Invite-EVMTsuNz.js.br +0 -0
  58. package/public/assets/Invite-EVMTsuNz.js.gz +0 -0
  59. package/public/assets/MacResources-fCn40Tf5.js +1 -0
  60. package/public/assets/MacResources-fCn40Tf5.js.br +0 -0
  61. package/public/assets/MacResources-fCn40Tf5.js.gz +0 -0
  62. package/public/assets/NotFound-wHOnT2Fl.js +1 -0
  63. package/public/assets/NotFound-wHOnT2Fl.js.br +0 -0
  64. package/public/assets/NotFound-wHOnT2Fl.js.gz +0 -0
  65. package/public/assets/QASession-CL-eyFxG.js +150 -0
  66. package/public/assets/QASession-CL-eyFxG.js.br +0 -0
  67. package/public/assets/QASession-CL-eyFxG.js.gz +0 -0
  68. package/public/assets/ResetPassword-D-SFg4ap.js +1 -0
  69. package/public/assets/ResetPassword-D-SFg4ap.js.br +0 -0
  70. package/public/assets/ResetPassword-D-SFg4ap.js.gz +0 -0
  71. package/public/assets/Setup-ULjvIfAi.js +1 -0
  72. package/public/assets/Setup-ULjvIfAi.js.br +0 -0
  73. package/public/assets/Setup-ULjvIfAi.js.gz +0 -0
  74. package/public/assets/Team-DH91GD4w.js +6 -0
  75. package/public/assets/Team-DH91GD4w.js.br +0 -0
  76. package/public/assets/Team-DH91GD4w.js.gz +0 -0
  77. package/public/assets/Tokens-DImLpOVZ.js +1 -0
  78. package/public/assets/Tokens-DImLpOVZ.js.br +0 -0
  79. package/public/assets/Tokens-DImLpOVZ.js.gz +0 -0
  80. package/public/assets/alert-dialog-CwcUyDZQ.js +1 -0
  81. package/public/assets/alert-dialog-CwcUyDZQ.js.br +0 -0
  82. package/public/assets/alert-dialog-CwcUyDZQ.js.gz +0 -0
  83. package/public/assets/badge-B4QAWrzA.js +1 -0
  84. package/public/assets/badge-B4QAWrzA.js.br +0 -0
  85. package/public/assets/badge-B4QAWrzA.js.gz +0 -0
  86. package/public/assets/build-format-CtmRXihN.js +11 -0
  87. package/public/assets/build-format-CtmRXihN.js.br +0 -0
  88. package/public/assets/build-format-CtmRXihN.js.gz +0 -0
  89. package/public/assets/dialog-BAFN_ONP.js +11 -0
  90. package/public/assets/dialog-BAFN_ONP.js.br +0 -0
  91. package/public/assets/dialog-BAFN_ONP.js.gz +0 -0
  92. package/public/assets/index-COOWQXpW.js +218 -0
  93. package/public/assets/index-COOWQXpW.js.br +0 -0
  94. package/public/assets/index-COOWQXpW.js.gz +0 -0
  95. package/public/assets/index-ozCdBYxk.css +1 -0
  96. package/public/assets/index-ozCdBYxk.css.br +0 -0
  97. package/public/assets/index-ozCdBYxk.css.gz +0 -0
  98. package/public/assets/list-view-Rrqp8TkG.js +1 -0
  99. package/public/assets/list-view-Rrqp8TkG.js.br +0 -0
  100. package/public/assets/list-view-Rrqp8TkG.js.gz +0 -0
  101. package/public/assets/pencil-CAHyK7Qf.js +6 -0
  102. package/public/assets/pencil-CAHyK7Qf.js.br +0 -0
  103. package/public/assets/pencil-CAHyK7Qf.js.gz +0 -0
  104. package/public/assets/resource-health-BRpSwarg.js +1 -0
  105. package/public/assets/resource-health-BRpSwarg.js.br +0 -0
  106. package/public/assets/resource-health-BRpSwarg.js.gz +0 -0
  107. package/public/assets/trash-CkO-5TRA.js +11 -0
  108. package/public/assets/trash-CkO-5TRA.js.br +0 -0
  109. package/public/assets/trash-CkO-5TRA.js.gz +0 -0
  110. package/public/assets/useFocusAfterSwap-BY3N9f9v.js +1 -0
  111. package/public/assets/useFocusAfterSwap-BY3N9f9v.js.br +0 -0
  112. package/public/assets/useFocusAfterSwap-BY3N9f9v.js.gz +0 -0
  113. package/public/assets/{vendor-charts-8kVeHLwY.js → vendor-charts-BykzUV2D.js} +1 -1
  114. package/public/assets/vendor-charts-BykzUV2D.js.br +0 -0
  115. package/public/assets/vendor-charts-BykzUV2D.js.gz +0 -0
  116. package/public/assets/vendor-react-BlPSzrEe.js +57 -0
  117. package/public/assets/vendor-react-BlPSzrEe.js.br +0 -0
  118. package/public/assets/vendor-react-BlPSzrEe.js.gz +0 -0
  119. package/public/assets/{vendor-router-BFibuab1.js → vendor-router-B_lVHrKl.js} +3 -3
  120. package/public/assets/vendor-router-B_lVHrKl.js.br +0 -0
  121. package/public/assets/vendor-router-B_lVHrKl.js.gz +0 -0
  122. package/public/index.html +4 -4
  123. package/public/index.html.br +0 -0
  124. package/public/index.html.gz +0 -0
  125. package/public/assets/AppCenter-gbsJscjW.js +0 -21
  126. package/public/assets/AppCenter-gbsJscjW.js.br +0 -0
  127. package/public/assets/AppCenter-gbsJscjW.js.gz +0 -0
  128. package/public/assets/Default-2rFTkbo9.js +0 -1
  129. package/public/assets/Default-2rFTkbo9.js.br +0 -0
  130. package/public/assets/Default-2rFTkbo9.js.gz +0 -0
  131. package/public/assets/Invite-aN4DXJyM.js +0 -1
  132. package/public/assets/Invite-aN4DXJyM.js.br +0 -0
  133. package/public/assets/Invite-aN4DXJyM.js.gz +0 -0
  134. package/public/assets/MacResources-D3IlesXU.js +0 -3
  135. package/public/assets/MacResources-D3IlesXU.js.br +0 -0
  136. package/public/assets/MacResources-D3IlesXU.js.gz +0 -0
  137. package/public/assets/NotFound-BoDDivK5.js +0 -1
  138. package/public/assets/NotFound-BoDDivK5.js.br +0 -0
  139. package/public/assets/NotFound-BoDDivK5.js.gz +0 -0
  140. package/public/assets/QASession-U29t9j0E.js +0 -140
  141. package/public/assets/QASession-U29t9j0E.js.br +0 -0
  142. package/public/assets/QASession-U29t9j0E.js.gz +0 -0
  143. package/public/assets/ResetPassword-DPc5kwQU.js +0 -1
  144. package/public/assets/ResetPassword-DPc5kwQU.js.br +0 -0
  145. package/public/assets/ResetPassword-DPc5kwQU.js.gz +0 -0
  146. package/public/assets/Setup-CwPx_e_O.js +0 -1
  147. package/public/assets/Setup-CwPx_e_O.js.br +0 -0
  148. package/public/assets/Setup-CwPx_e_O.js.gz +0 -0
  149. package/public/assets/Team-DD9li-7M.js +0 -6
  150. package/public/assets/Team-DD9li-7M.js.br +0 -0
  151. package/public/assets/Team-DD9li-7M.js.gz +0 -0
  152. package/public/assets/Tokens-DdMMke73.js +0 -1
  153. package/public/assets/Tokens-DdMMke73.js.br +0 -0
  154. package/public/assets/Tokens-DdMMke73.js.gz +0 -0
  155. package/public/assets/alert-dialog-DADd1jHN.js +0 -1
  156. package/public/assets/alert-dialog-DADd1jHN.js.br +0 -0
  157. package/public/assets/alert-dialog-DADd1jHN.js.gz +0 -0
  158. package/public/assets/badge-CMf8d5RJ.js +0 -1
  159. package/public/assets/badge-CMf8d5RJ.js.br +0 -0
  160. package/public/assets/badge-CMf8d5RJ.js.gz +0 -0
  161. package/public/assets/build-format-lp_8kSvq.js +0 -11
  162. package/public/assets/build-format-lp_8kSvq.js.br +0 -0
  163. package/public/assets/build-format-lp_8kSvq.js.gz +0 -0
  164. package/public/assets/dialog-pS8vHXXv.js +0 -11
  165. package/public/assets/dialog-pS8vHXXv.js.br +0 -0
  166. package/public/assets/dialog-pS8vHXXv.js.gz +0 -0
  167. package/public/assets/index-Bxs7CdEi.css +0 -1
  168. package/public/assets/index-Bxs7CdEi.css.br +0 -0
  169. package/public/assets/index-Bxs7CdEi.css.gz +0 -0
  170. package/public/assets/index-Cajf0T-5.js +0 -198
  171. package/public/assets/index-Cajf0T-5.js.br +0 -0
  172. package/public/assets/index-Cajf0T-5.js.gz +0 -0
  173. package/public/assets/pencil-LD1ga-Kk.js +0 -6
  174. package/public/assets/pencil-LD1ga-Kk.js.br +0 -0
  175. package/public/assets/pencil-LD1ga-Kk.js.gz +0 -0
  176. package/public/assets/resource-health-BScurILF.js +0 -1
  177. package/public/assets/resource-health-BScurILF.js.br +0 -0
  178. package/public/assets/resource-health-BScurILF.js.gz +0 -0
  179. package/public/assets/table-CL9AvoTp.js +0 -1
  180. package/public/assets/table-CL9AvoTp.js.br +0 -0
  181. package/public/assets/table-CL9AvoTp.js.gz +0 -0
  182. package/public/assets/trash-mvVp4nwq.js +0 -11
  183. package/public/assets/trash-mvVp4nwq.js.br +0 -0
  184. package/public/assets/trash-mvVp4nwq.js.gz +0 -0
  185. package/public/assets/vendor-charts-8kVeHLwY.js.br +0 -0
  186. package/public/assets/vendor-charts-8kVeHLwY.js.gz +0 -0
  187. package/public/assets/vendor-react-AJpf4YZ0.js +0 -49
  188. package/public/assets/vendor-react-AJpf4YZ0.js.br +0 -0
  189. package/public/assets/vendor-react-AJpf4YZ0.js.gz +0 -0
  190. package/public/assets/vendor-router-BFibuab1.js.br +0 -0
  191. 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,12 +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
+ import { resolveBuildFile } from './lib/buildFiles.js';
15
16
  import { resolveCorsHeaders } from './lib/cors.js';
16
17
  import { isCsrfBlocked } from './lib/csrf.js';
17
18
  import { pickLanAddress, runningInContainer } from './lib/lanAddress.js';
18
- import { config } from './lib/config.js';
19
+ import { config, getJwtSecret } from './lib/config.js';
19
20
  import { forTeammates, resolveAgentRelayUrl, resolvePublicBaseUrl } from './lib/publicUrl.js';
20
21
  import { createTrailingRequester, systemTimerScheduler } from './lib/trailingRequester.js';
21
22
  import { getDb } from './db.js';
@@ -156,6 +157,20 @@ function describeRejection(failure, suppressed) {
156
157
  export class RelayServer {
157
158
  options;
158
159
  httpServer;
160
+ /**
161
+ * **The port tunnel clients are pointed at, and the only thing that tells their traffic apart.**
162
+ * rathole's client and `tailscale serve` run on this machine and connect from loopback on behalf of
163
+ * someone on the internet or the tailnet; rathole forwards raw TCP, so no header marks it either. On
164
+ * the main port such a socket is indistinguishable from `tapflow start`'s own agent, which is
165
+ * unauthenticated by design — so every tunnel visitor was too. Everything accepted here is remote
166
+ * (`resolveClientAddress`'s `viaTunnel`), whatever its address.
167
+ *
168
+ * Loopback-only, because only a tunnel client on this machine has any business here; a remote client
169
+ * belongs on the main port, where it is classified by address as before. Same handler, same
170
+ * `WebSocketServer`, same TLS material — rathole carried a TLS relay's own handshake through before
171
+ * this existed, and still does.
172
+ */
173
+ tunnelServer = null;
159
174
  wss;
160
175
  sessions;
161
176
  publicDir;
@@ -240,6 +255,10 @@ export class RelayServer {
240
255
  pendingUITrees = new Map();
241
256
  constructor(options) {
242
257
  this.options = options;
258
+ // 0 on both sides is two ephemeral ports, which is what the tests ask for.
259
+ if (options.tunnelPort !== undefined && options.tunnelPort !== 0 && options.tunnelPort === options.port) {
260
+ throw new Error(`The tunnel port (${options.tunnelPort}) must differ from the relay port. Set TAPFLOW_TUNNEL_PORT to another port.`);
261
+ }
243
262
  this.backpressureBytes = options.wsBackpressureBytes ?? DEFAULT_BACKPRESSURE_BYTES;
244
263
  this.screenshotTimeoutMs = options.screenshotTimeoutMs ?? 10_000;
245
264
  // Longer than the screenshot default: the Android agent's device-side dump
@@ -280,11 +299,29 @@ export class RelayServer {
280
299
  this.wss = new WebSocketServer({ server: this.httpServer });
281
300
  this.wss.on('connection', (ws, request) => this.handleConnection(ws, request));
282
301
  this.wss.on('error', () => { });
302
+ if (options.tunnelPort !== undefined) {
303
+ // Marked before anything reads it: the router hands this same request object to `handleInit`, and
304
+ // `handleUpgrade` passes it on to the `connection` event.
305
+ const tunnelHandler = (req, res) => {
306
+ markTunnelIngress(req);
307
+ return handler(req, res);
308
+ };
309
+ const tunnelServer = options.tls
310
+ ? https.createServer({ cert: options.tls.cert, key: options.tls.key }, tunnelHandler)
311
+ : http.createServer(tunnelHandler);
312
+ tunnelServer.on('connection', (socket) => socket.setNoDelay(true));
313
+ // `handleUpgrade` adds the socket to `wss.clients`, so the heartbeat and `stop()` cover it.
314
+ tunnelServer.on('upgrade', (req, socket, head) => {
315
+ markTunnelIngress(req);
316
+ this.wss.handleUpgrade(req, socket, head, (ws) => this.wss.emit('connection', ws, req));
317
+ });
318
+ this.tunnelServer = tunnelServer;
319
+ }
283
320
  }
284
321
  registerRoutes() {
285
322
  const u = this.uploadsDir;
286
323
  // auth
287
- this.router.get('/api/v1/auth/status', handleAuthStatus);
324
+ this.router.get('/api/v1/auth/status', (req, res) => handleAuthStatus(req, res, this.options.trustedProxies ?? []));
288
325
  this.router.post('/api/v1/auth/init', (req, res) => handleInit(req, res, this.options.trustedProxies ?? []));
289
326
  this.router.get('/api/v1/auth/me', handleMe);
290
327
  this.router.post('/api/v1/auth/login', (req, res) => handleLogin(req, res, this.options.trustedProxies ?? []));
@@ -327,7 +364,7 @@ export class RelayServer {
327
364
  this.router.post('/api/v1/team/invite', (req, res) => handleInvite(req, res, this.options.tunnel));
328
365
  this.router.patch('/api/v1/team/members/:id', handleUpdateMember);
329
366
  this.router.delete('/api/v1/team/members/:id', handleDeleteMember);
330
- this.router.post('/api/v1/team/members/:id/send-reset', handleSendMemberReset);
367
+ this.router.post('/api/v1/team/members/:id/send-reset', (req, res, params) => handleSendMemberReset(req, res, params, this.options.tunnel));
331
368
  // tokens
332
369
  this.router.get('/api/v1/tokens', handleListTokens);
333
370
  this.router.post('/api/v1/tokens', handleCreateToken);
@@ -380,6 +417,9 @@ export class RelayServer {
380
417
  logger.info(msg);
381
418
  }
382
419
  start() {
420
+ // Creates this install's JWT secret if it has none. Here rather than at import, so commands
421
+ // that never run a relay stop leaving one behind — and a write failure is still a boot failure.
422
+ getJwtSecret();
383
423
  purgeExpiredRecordings(this.recordingsDir);
384
424
  this.purgeRecordingsTimer = setInterval(() => purgeExpiredRecordings(this.recordingsDir), 24 * 60 * 60 * 1000);
385
425
  this.purgeRecordingsTimer.unref();
@@ -389,8 +429,8 @@ export class RelayServer {
389
429
  purgeOldResources();
390
430
  this.purgeOldResourcesTimer = setInterval(purgeOldResources, 24 * 60 * 60 * 1000);
391
431
  this.purgeOldResourcesTimer.unref();
392
- purgeExpiredBuilds(this.recordingsDir);
393
- this.purgeBuildsTimer = setInterval(() => purgeExpiredBuilds(this.recordingsDir), 24 * 60 * 60 * 1000);
432
+ purgeExpiredBuilds(this.recordingsDir, this.uploadsDir);
433
+ this.purgeBuildsTimer = setInterval(() => purgeExpiredBuilds(this.recordingsDir, this.uploadsDir), 24 * 60 * 60 * 1000);
394
434
  this.purgeBuildsTimer.unref();
395
435
  this.flushResourcesTimer = setInterval(() => this.flushResourceBuffers(), 60_000);
396
436
  this.flushResourcesTimer.unref();
@@ -407,7 +447,30 @@ export class RelayServer {
407
447
  });
408
448
  // Bind dual-stack (IPv4 + IPv6). A bare listen(port) binds IPv6-only on some
409
449
  // 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);
450
+ this.httpServer.listen({ port: this.options.port, host: '::', ipv6Only: false }, () => resolve());
451
+ }).then(() => this.listenTunnel());
452
+ }
453
+ listenTunnel() {
454
+ const tunnelServer = this.tunnelServer;
455
+ if (!tunnelServer)
456
+ return Promise.resolve();
457
+ const port = this.options.tunnelPort ?? 0;
458
+ return new Promise((resolve, reject) => {
459
+ tunnelServer.once('error', (err) => reject(err.code === 'EADDRINUSE'
460
+ ? new Error(`Tunnel port ${port} is already in use. Stop the process holding it, or set TAPFLOW_TUNNEL_PORT to a free port.`)
461
+ : err));
462
+ // 127.0.0.1, not `localhost`: rathole's client dials that address literally, and `tailscale serve
463
+ // <port>` proxies to it.
464
+ tunnelServer.listen({ port, host: '127.0.0.1' }, () => {
465
+ // The main server's later errors reach `wss`, which swallows them; nothing forwards this one's.
466
+ tunnelServer.on('error', (err) => logger.error(`tunnel listener error: ${String(err)}`));
467
+ resolve();
468
+ });
469
+ }).catch(async (err) => {
470
+ // A relay whose tunnel port failed must not keep serving on the main one: the caller treats this
471
+ // as a failed start and moves on, and the port would stay held by a server nobody stops.
472
+ await this.stop().catch(() => { });
473
+ throw err;
411
474
  });
412
475
  }
413
476
  stop() {
@@ -445,16 +508,27 @@ export class RelayServer {
445
508
  for (const requester of this.networkStateRequesters.values())
446
509
  requester.dispose();
447
510
  this.networkStateRequesters.clear();
448
- return new Promise((resolve, reject) => {
511
+ const closeTunnel = new Promise((resolve, reject) => {
512
+ // Not listening means `start()` never reached it, or its listen failed; `close()` would reject.
513
+ if (!this.tunnelServer?.listening)
514
+ return resolve();
515
+ this.tunnelServer.close((err) => (err ? reject(err) : resolve()));
516
+ });
517
+ const closeMain = new Promise((resolve, reject) => {
449
518
  this.wss.clients.forEach((ws) => ws.terminate());
450
519
  this.wss.close(() => {
451
520
  this.httpServer.close((err) => (err ? reject(err) : resolve()));
452
521
  });
453
522
  });
523
+ return Promise.all([closeMain, closeTunnel]).then(() => undefined);
454
524
  }
455
525
  address() {
456
526
  return this.httpServer.address();
457
527
  }
528
+ /** The tunnel listener's address, or null when this relay was given no `tunnelPort`. */
529
+ tunnelAddress() {
530
+ return this.tunnelServer?.address() ?? null;
531
+ }
458
532
  // Terminate sockets that missed the previous pong; ping the rest. Covers all roles via wss.clients.
459
533
  runHeartbeat(clients = this.wss.clients) {
460
534
  for (const ws of clients) {
@@ -488,8 +562,10 @@ export class RelayServer {
488
562
  }
489
563
  // 갱신된 cert를 재시작 없이 핫스왑한다(https 종단일 때만 의미 있음).
490
564
  updateTlsContext(material) {
491
- if (this.httpServer instanceof https.Server) {
492
- this.httpServer.setSecureContext({ cert: material.cert, key: material.key });
565
+ for (const server of [this.httpServer, this.tunnelServer]) {
566
+ if (server instanceof https.Server) {
567
+ server.setSecureContext({ cert: material.cert, key: material.key });
568
+ }
493
569
  }
494
570
  }
495
571
  /**
@@ -604,8 +680,9 @@ export class RelayServer {
604
680
  const a = socketAddr.replace(/^::ffff:/, '');
605
681
  if (a === '::1' || a.startsWith('127.')) {
606
682
  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).');
683
+ 'If a same-host reverse proxy or `tailscale serve` forwards to this port, every client it forwards is ' +
684
+ 'treated as localhost (unauthenticated). Point it at the tunnel port instead (TAPFLOW_TUNNEL_PORT, ' +
685
+ 'default 4001), or set TAPFLOW_TRUSTED_PROXIES so the real client IP is used.');
609
686
  this.warnedProxyMisconfig = true;
610
687
  }
611
688
  }
@@ -613,11 +690,15 @@ export class RelayServer {
613
690
  const socketAddr = this.remoteAddressOf(request);
614
691
  const xff = request.headers['x-forwarded-for'];
615
692
  const forwardedFor = Array.isArray(xff) ? xff[0] : xff;
616
- this.warnProxyMisconfigOnce(socketAddr, forwardedFor);
693
+ const viaTunnel = isTunnelIngress(request);
694
+ // A proxy in front of the tunnel port adds the header too, and there it changes nothing about auth.
695
+ if (!viaTunnel)
696
+ this.warnProxyMisconfigOnce(socketAddr, forwardedFor);
617
697
  const { addr, isLocal } = resolveClientAddress({
618
698
  socketAddr,
619
699
  forwardedFor,
620
700
  trustedProxies: this.options.trustedProxies ?? [],
701
+ viaTunnel,
621
702
  });
622
703
  const hasCookieAuth = getAuth(request) !== null;
623
704
  // DB lookup — only when the connection can't be classified without it (remote, no cookie).
@@ -964,15 +1045,32 @@ export class RelayServer {
964
1045
  // two-member union** while `AgentRegister.platform` is `string`, open so that a third-party
965
1046
  // platform can register through `AgentRegistry.register()` (root AGENTS.md, OCP). Validating
966
1047
  // 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.
1048
+ // dropped frame: skipping `setChromeData` also
1049
+ // empties what the re-join replay reads. It is no longer true that this arrives once per boot,
1050
+ // either — a foldable re-sends it when the screen changes under the session — so the cost of
1051
+ // a refusal is a viewer stuck with the previous screen's bezel rather than one frame.
1052
+ // The relay never looks inside; the viewer does, and it is where the variants are already
1053
+ // told apart.
970
1054
  this.sessions.setChromeData(session.id, raw['payload']);
971
1055
  if (session.browserSocket?.readyState === WebSocket.OPEN) {
972
1056
  session.browserSocket.send(JSON.stringify(raw));
973
1057
  }
974
1058
  break;
975
1059
  }
1060
+ case 'device:postures': {
1061
+ const session = this.sessions.get(msg.sessionId);
1062
+ if (!session)
1063
+ break;
1064
+ // Cached for the same reason `session:chrome` is: a viewer that re-joins (a tab reload, a
1065
+ // socket blip) gets the session's state replayed, and without this the posture control would
1066
+ // simply be missing until the device was rebooted — with the picture still fine, which is
1067
+ // the hardest kind of gap to diagnose.
1068
+ this.sessions.setPostures(session.id, raw['payload']);
1069
+ if (session.browserSocket?.readyState === WebSocket.OPEN) {
1070
+ session.browserSocket.send(JSON.stringify(raw));
1071
+ }
1072
+ break;
1073
+ }
976
1074
  case 'session:deviceInfo': {
977
1075
  const session = this.sessions.get(msg.sessionId);
978
1076
  if (!session)
@@ -1228,6 +1326,7 @@ export class RelayServer {
1228
1326
  case 'input:pinch:start':
1229
1327
  case 'input:pinch:move':
1230
1328
  case 'input:rotate':
1329
+ case 'input:posture':
1231
1330
  case 'input:keyboard:toggle': {
1232
1331
  // No ack, so no correlator to check and nothing to answer. An unowned frame is dropped rather
1233
1332
  // than refused for the same reason: there is no waiter to tell.
@@ -1627,6 +1726,9 @@ export class RelayServer {
1627
1726
  if (session.deviceInfo) {
1628
1727
  this.sendTo(ws, { type: 'session:deviceInfo', sessionId: session.id, payload: session.deviceInfo });
1629
1728
  }
1729
+ if (session.postures) {
1730
+ this.sendTo(ws, { type: 'device:postures', sessionId: session.id, payload: session.postures });
1731
+ }
1630
1732
  // Replay device:ready only if this session actually announced one (browser WS blip reconnect).
1631
1733
  // Not `deviceStatus`: that starts from the agent's `simctl list` snapshot, so a session for a
1632
1734
  // simulator that was already running would fire this before the agent had done anything —
@@ -1986,9 +2088,15 @@ export class RelayServer {
1986
2088
  // A missing file is answered rather than thrown for the same reason the lookup above is: the
1987
2089
  // build row outlives its file whenever a bind mount goes away or a purge half-ran, and an
1988
2090
  // unanswered install is a spinner that never stops and a caller that times out with no cause.
2091
+ //
2092
+ // Resolved rather than read verbatim: the stored path names wherever the data directory was at
2093
+ // upload time, and a migrated, moved or restored install keeps the file under a different one.
2094
+ const filePath = resolveBuildFile(build.file_path, this.uploadsDir);
1989
2095
  let bytes;
1990
2096
  try {
1991
- bytes = fs.statSync(build.file_path).size;
2097
+ if (filePath === null)
2098
+ throw new Error('not found');
2099
+ bytes = fs.statSync(filePath).size;
1992
2100
  }
1993
2101
  catch {
1994
2102
  return fail('The relay cannot read this build file. It may have been deleted — re-upload the build.');
@@ -1999,10 +2107,10 @@ export class RelayServer {
1999
2107
  requestId,
2000
2108
  payload: {
2001
2109
  // Still sent, and sent first: an agent that predates `build-download` reads only this.
2002
- filePath: build.file_path,
2110
+ filePath,
2003
2111
  bundleId: build.bundle_id,
2004
- buildTicket: this.buildTickets.mint(msg.buildId, build.file_path),
2005
- buildName: path.basename(build.file_path),
2112
+ buildTicket: this.buildTickets.mint(msg.buildId, filePath),
2113
+ buildName: path.basename(filePath),
2006
2114
  buildBytes: bytes,
2007
2115
  },
2008
2116
  });