@tapflowio/relay 0.27.0 → 0.27.1

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 (136) hide show
  1. package/dist/RelayServer.d.ts +20 -0
  2. package/dist/RelayServer.d.ts.map +1 -1
  3. package/dist/RelayServer.js +98 -6
  4. package/dist/RelayServer.js.map +1 -1
  5. package/dist/api/builds.d.ts.map +1 -1
  6. package/dist/api/builds.js +13 -8
  7. package/dist/api/builds.js.map +1 -1
  8. package/dist/api/comments.d.ts.map +1 -1
  9. package/dist/api/comments.js +5 -7
  10. package/dist/api/comments.js.map +1 -1
  11. package/dist/api/invitations.d.ts.map +1 -1
  12. package/dist/api/invitations.js +3 -0
  13. package/dist/api/invitations.js.map +1 -1
  14. package/dist/api/profile.d.ts.map +1 -1
  15. package/dist/api/profile.js +28 -9
  16. package/dist/api/profile.js.map +1 -1
  17. package/dist/api/recordings.d.ts.map +1 -1
  18. package/dist/api/recordings.js +5 -6
  19. package/dist/api/recordings.js.map +1 -1
  20. package/dist/api/settings.d.ts.map +1 -1
  21. package/dist/api/settings.js +28 -9
  22. package/dist/api/settings.js.map +1 -1
  23. package/dist/lib/clientAddress.d.ts +12 -0
  24. package/dist/lib/clientAddress.d.ts.map +1 -1
  25. package/dist/lib/clientAddress.js +22 -0
  26. package/dist/lib/clientAddress.js.map +1 -1
  27. package/dist/lib/uploads.d.ts +13 -0
  28. package/dist/lib/uploads.d.ts.map +1 -1
  29. package/dist/lib/uploads.js +41 -0
  30. package/dist/lib/uploads.js.map +1 -1
  31. package/package.json +3 -3
  32. package/public/assets/{AppCenter-BMdYtQbV.js → AppCenter-CeLjUbnR.js} +1 -1
  33. package/public/assets/AppCenter-CeLjUbnR.js.br +0 -0
  34. package/public/assets/AppCenter-CeLjUbnR.js.gz +0 -0
  35. package/public/assets/{Default-0DHHVGaN.js → Default-UB0Pa3lN.js} +1 -1
  36. package/public/assets/Default-UB0Pa3lN.js.br +0 -0
  37. package/public/assets/Default-UB0Pa3lN.js.gz +0 -0
  38. package/public/assets/{Invite-C0ZB_HiU.js → Invite-Dv3omu3x.js} +1 -1
  39. package/public/assets/Invite-Dv3omu3x.js.br +0 -0
  40. package/public/assets/Invite-Dv3omu3x.js.gz +0 -0
  41. package/public/assets/{MacResources-DmY5pRV2.js → MacResources-CY4-OdGt.js} +1 -1
  42. package/public/assets/MacResources-CY4-OdGt.js.br +0 -0
  43. package/public/assets/MacResources-CY4-OdGt.js.gz +0 -0
  44. package/public/assets/QASession-H5eRSwZv.js +150 -0
  45. package/public/assets/QASession-H5eRSwZv.js.br +0 -0
  46. package/public/assets/QASession-H5eRSwZv.js.gz +0 -0
  47. package/public/assets/{ResetPassword-jNRuertx.js → ResetPassword-nFTigYt2.js} +1 -1
  48. package/public/assets/ResetPassword-nFTigYt2.js.br +0 -0
  49. package/public/assets/ResetPassword-nFTigYt2.js.gz +0 -0
  50. package/public/assets/{Setup-CF_FN9Dt.js → Setup-UmLtF2PF.js} +1 -1
  51. package/public/assets/Setup-UmLtF2PF.js.br +0 -0
  52. package/public/assets/Setup-UmLtF2PF.js.gz +0 -0
  53. package/public/assets/{Team-BbiyOGau.js → Team-DW40kt3H.js} +1 -1
  54. package/public/assets/Team-DW40kt3H.js.br +0 -0
  55. package/public/assets/Team-DW40kt3H.js.gz +0 -0
  56. package/public/assets/{Tokens-CZo4sPTx.js → Tokens-B1a4ChJK.js} +1 -1
  57. package/public/assets/Tokens-B1a4ChJK.js.br +0 -0
  58. package/public/assets/Tokens-B1a4ChJK.js.gz +0 -0
  59. package/public/assets/{alert-dialog-DFz8G_zu.js → alert-dialog-aoqYdAwx.js} +1 -1
  60. package/public/assets/alert-dialog-aoqYdAwx.js.br +0 -0
  61. package/public/assets/alert-dialog-aoqYdAwx.js.gz +0 -0
  62. package/public/assets/{badge-B3vaBVIQ.js → badge-DNaS0Mu3.js} +1 -1
  63. package/public/assets/badge-DNaS0Mu3.js.br +0 -0
  64. package/public/assets/badge-DNaS0Mu3.js.gz +0 -0
  65. package/public/assets/{build-format-dbQPU3A4.js → build-format-DF5aFcag.js} +1 -1
  66. package/public/assets/build-format-DF5aFcag.js.br +0 -0
  67. package/public/assets/build-format-DF5aFcag.js.gz +0 -0
  68. package/public/assets/{dialog-DeBPk-I3.js → dialog-DE2WUMuX.js} +1 -1
  69. package/public/assets/dialog-DE2WUMuX.js.br +0 -0
  70. package/public/assets/dialog-DE2WUMuX.js.gz +0 -0
  71. package/public/assets/{field-hint-CMKmbVpy.js → field-hint-Csb40Gzp.js} +1 -1
  72. package/public/assets/field-hint-Csb40Gzp.js.br +2 -0
  73. package/public/assets/field-hint-Csb40Gzp.js.gz +0 -0
  74. package/public/assets/index-BbCkTtjO.css +1 -0
  75. package/public/assets/index-BbCkTtjO.css.br +0 -0
  76. package/public/assets/index-BbCkTtjO.css.gz +0 -0
  77. package/public/assets/{index-B4w4Lh3e.js → index-DSW9h8XX.js} +2 -2
  78. package/public/assets/index-DSW9h8XX.js.br +0 -0
  79. package/public/assets/index-DSW9h8XX.js.gz +0 -0
  80. package/public/assets/{list-view-BvINA5t4.js → list-view-C0w97ltJ.js} +1 -1
  81. package/public/assets/list-view-C0w97ltJ.js.br +0 -0
  82. package/public/assets/list-view-C0w97ltJ.js.gz +0 -0
  83. package/public/assets/{pencil-CycAOoC_.js → pencil-BN18R4oa.js} +1 -1
  84. package/public/assets/pencil-BN18R4oa.js.br +0 -0
  85. package/public/assets/pencil-BN18R4oa.js.gz +0 -0
  86. package/public/assets/{resource-health-C1qSG3MK.js → resource-health-Bi20EUDs.js} +1 -1
  87. package/public/assets/resource-health-Bi20EUDs.js.br +0 -0
  88. package/public/assets/resource-health-Bi20EUDs.js.gz +0 -0
  89. package/public/assets/{trash-BXQnOI5U.js → trash-D1l1Fm-Z.js} +1 -1
  90. package/public/assets/trash-D1l1Fm-Z.js.br +0 -0
  91. package/public/assets/trash-D1l1Fm-Z.js.gz +0 -0
  92. package/public/index.html +2 -2
  93. package/public/index.html.br +0 -0
  94. package/public/index.html.gz +0 -0
  95. package/public/assets/AppCenter-BMdYtQbV.js.br +0 -0
  96. package/public/assets/AppCenter-BMdYtQbV.js.gz +0 -0
  97. package/public/assets/Default-0DHHVGaN.js.br +0 -0
  98. package/public/assets/Default-0DHHVGaN.js.gz +0 -0
  99. package/public/assets/Invite-C0ZB_HiU.js.br +0 -0
  100. package/public/assets/Invite-C0ZB_HiU.js.gz +0 -0
  101. package/public/assets/MacResources-DmY5pRV2.js.br +0 -0
  102. package/public/assets/MacResources-DmY5pRV2.js.gz +0 -0
  103. package/public/assets/QASession-2QogWEX7.js +0 -150
  104. package/public/assets/QASession-2QogWEX7.js.br +0 -0
  105. package/public/assets/QASession-2QogWEX7.js.gz +0 -0
  106. package/public/assets/ResetPassword-jNRuertx.js.br +0 -0
  107. package/public/assets/ResetPassword-jNRuertx.js.gz +0 -0
  108. package/public/assets/Setup-CF_FN9Dt.js.br +0 -0
  109. package/public/assets/Setup-CF_FN9Dt.js.gz +0 -0
  110. package/public/assets/Team-BbiyOGau.js.br +0 -0
  111. package/public/assets/Team-BbiyOGau.js.gz +0 -0
  112. package/public/assets/Tokens-CZo4sPTx.js.br +0 -0
  113. package/public/assets/Tokens-CZo4sPTx.js.gz +0 -0
  114. package/public/assets/alert-dialog-DFz8G_zu.js.br +0 -0
  115. package/public/assets/alert-dialog-DFz8G_zu.js.gz +0 -0
  116. package/public/assets/badge-B3vaBVIQ.js.br +0 -0
  117. package/public/assets/badge-B3vaBVIQ.js.gz +0 -0
  118. package/public/assets/build-format-dbQPU3A4.js.br +0 -0
  119. package/public/assets/build-format-dbQPU3A4.js.gz +0 -0
  120. package/public/assets/dialog-DeBPk-I3.js.br +0 -0
  121. package/public/assets/dialog-DeBPk-I3.js.gz +0 -0
  122. package/public/assets/field-hint-CMKmbVpy.js.br +0 -0
  123. package/public/assets/field-hint-CMKmbVpy.js.gz +0 -0
  124. package/public/assets/index-B4w4Lh3e.js.br +0 -0
  125. package/public/assets/index-B4w4Lh3e.js.gz +0 -0
  126. package/public/assets/index-CDWu0GQL.css +0 -1
  127. package/public/assets/index-CDWu0GQL.css.br +0 -0
  128. package/public/assets/index-CDWu0GQL.css.gz +0 -0
  129. package/public/assets/list-view-BvINA5t4.js.br +0 -0
  130. package/public/assets/list-view-BvINA5t4.js.gz +0 -0
  131. package/public/assets/pencil-CycAOoC_.js.br +0 -0
  132. package/public/assets/pencil-CycAOoC_.js.gz +0 -0
  133. package/public/assets/resource-health-C1qSG3MK.js.br +0 -0
  134. package/public/assets/resource-health-C1qSG3MK.js.gz +0 -0
  135. package/public/assets/trash-BXQnOI5U.js.br +0 -0
  136. package/public/assets/trash-BXQnOI5U.js.gz +0 -0
@@ -64,6 +64,15 @@ export declare class RelayServer {
64
64
  private purgeBuildsTimer;
65
65
  private flushResourcesTimer;
66
66
  private heartbeatTimer;
67
+ /** When the last heartbeat sweep ran, by the same clock `isAlive` judges with. */
68
+ private lastSweepAt;
69
+ /** Names agents give at registration, kept apart from the session map so an agent with no sessions
70
+ * can still be named when its socket goes. */
71
+ private agentNames;
72
+ private releaseStallWatch;
73
+ /** Agent sockets whose close was already explained — ended by a sweep, or replaced by the same agent
74
+ * registering again — so the close handler does not log them as a fresh drop. */
75
+ private explainedCloses;
67
76
  /**
68
77
  * When each socket last answered a ping. WeakMap → no manual cleanup on close (GC handles it).
69
78
  *
@@ -133,6 +142,7 @@ export declare class RelayServer {
133
142
  private readonly pendingBootTtlMs;
134
143
  private readonly corsAllowed;
135
144
  private warnedProxyMisconfig;
145
+ private warnedTrustedProxyWithoutXff;
136
146
  /** Agent identities already warned about a missing `build-download`. See `warnLegacyInstaller`. */
137
147
  private readonly warnedLegacyInstaller;
138
148
  private readonly buildTickets;
@@ -212,6 +222,10 @@ export declare class RelayServer {
212
222
  */
213
223
  private warnOrphanedAgentTokens;
214
224
  private runHeartbeat;
225
+ /** A sweep that runs late means this process was held, or the machine slept: the pongs it judges were
226
+ * never read, so its terminations say nothing about the clients. Same clock as `isAlive`. */
227
+ private noteLateSweep;
228
+ private pongAgeSeconds;
215
229
  /**
216
230
  * Injected into the handlers whose write can take access away — member update and removal, token
217
231
  * revocation, a password reset or change — and called after the write commits. The sweep re-derives
@@ -302,6 +316,12 @@ export declare class RelayServer {
302
316
  */
303
317
  private networkStateRequester;
304
318
  private remoteAddressOf;
319
+ /**
320
+ * Called on both doors — the WebSocket upgrade and every HTTP request — since `auth/init` and the logs
321
+ * route decide locality on the HTTP side. Not on the tunnel listener, where every request is remote
322
+ * whatever it carries.
323
+ */
324
+ private warnTrustedProxyWithoutXffOnce;
305
325
  private warnProxyMisconfigOnce;
306
326
  private handleConnection;
307
327
  /**
@@ -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, touchPat, findPat, findUser } from './middleware/auth.js';
12
12
  import { AGENT_SCOPE, WS_ACCESS_CHANGED_REASON, WS_AGENT_OWNER_REASON, WS_SCOPE_REASON, classifyConnection, isAffectedBy, revalidatePrincipal } from './lib/connectionAuth.js';
13
- import { isTunnelIngress, markTunnelIngress, resolveClientAddress, resolveRequestClient } from './lib/clientAddress.js';
13
+ import { isTunnelIngress, markTunnelIngress, proxiedWithoutForwardedFor, resolveClientAddress, resolveRequestClient } from './lib/clientAddress.js';
14
14
  import { BuildTicketStore } from './lib/buildTickets.js';
15
15
  import { resolveBuildFile } from './lib/buildFiles.js';
16
16
  import { resolveCorsHeaders } from './lib/cors.js';
@@ -23,6 +23,7 @@ import { getDb } from './db.js';
23
23
  import { handleLogin, handleLogout, handleMe, handleChangePassword, handleInit, handleAuthStatus } from './api/auth.js';
24
24
  import { handleVerify, handleAccept } from './api/invitations.js';
25
25
  import { createLogger, hasCapability } from '@tapflowio/agent-core';
26
+ import { acquireLoopStallWatch } from '@tapflowio/agent-core/utils';
26
27
  import { createKeyframeAwareSender, createRateLimitedDropWarn, sendAudioYieldingToVideo, DEFAULT_BACKPRESSURE_BYTES, hasEnvelope, patchRelayedAt, readEnvelopeFlags, CODEC_JPEG, CODEC_AUDIO, } from '@tapflowio/agent-core/utils';
27
28
  const logger = createLogger('relay');
28
29
  // True when a remote IP is public — i.e. not loopback, private LAN, or link-local. Used to pick the
@@ -214,6 +215,15 @@ export class RelayServer {
214
215
  purgeBuildsTimer = null;
215
216
  flushResourcesTimer = null;
216
217
  heartbeatTimer = null;
218
+ /** When the last heartbeat sweep ran, by the same clock `isAlive` judges with. */
219
+ lastSweepAt = 0;
220
+ /** Names agents give at registration, kept apart from the session map so an agent with no sessions
221
+ * can still be named when its socket goes. */
222
+ agentNames = new WeakMap();
223
+ releaseStallWatch = null;
224
+ /** Agent sockets whose close was already explained — ended by a sweep, or replaced by the same agent
225
+ * registering again — so the close handler does not log them as a fresh drop. */
226
+ explainedCloses = new WeakMap();
217
227
  /**
218
228
  * When each socket last answered a ping. WeakMap → no manual cleanup on close (GC handles it).
219
229
  *
@@ -297,6 +307,7 @@ export class RelayServer {
297
307
  corsAllowed;
298
308
  // One-shot warning when XFF arrives on a loopback socket but TAPFLOW_TRUSTED_PROXIES is unset.
299
309
  warnedProxyMisconfig = false;
310
+ warnedTrustedProxyWithoutXff = false;
300
311
  /** Agent identities already warned about a missing `build-download`. See `warnLegacyInstaller`. */
301
312
  warnedLegacyInstaller = new Set();
302
313
  buildTickets = new BuildTicketStore();
@@ -526,8 +537,17 @@ export class RelayServer {
526
537
  this.purgeBuildsTimer.unref();
527
538
  this.flushResourcesTimer = setInterval(() => this.flushResourceBuffers(), 60_000);
528
539
  this.flushResourcesTimer.unref();
529
- this.heartbeatTimer = setInterval(() => this.runHeartbeat(), HEARTBEAT_MS);
540
+ this.lastSweepAt = Date.now();
541
+ this.heartbeatTimer = setInterval(() => { this.noteLateSweep(); this.runHeartbeat(); }, HEARTBEAT_MS);
530
542
  this.heartbeatTimer.unref();
543
+ this.releaseStallWatch ??= acquireLoopStallWatch();
544
+ // The rule a proxy has to follow, said once to whoever set the list: the case it covers (a proxy that
545
+ // sends no forwarding header at all) is one no request-time check can see.
546
+ const trustedProxies = this.options.trustedProxies ?? [];
547
+ if (trustedProxies.length > 0) {
548
+ logger.info(`Trusted proxies: ${trustedProxies.join(', ')}. A request from one of these without ` +
549
+ 'X-Forwarded-For is treated as coming from that address itself, so the proxy must send X-Forwarded-For.');
550
+ }
531
551
  return new Promise((resolve, reject) => {
532
552
  this.httpServer.once('error', (err) => {
533
553
  if (err.code === 'EADDRINUSE') {
@@ -586,6 +606,8 @@ export class RelayServer {
586
606
  clearInterval(this.heartbeatTimer);
587
607
  this.heartbeatTimer = null;
588
608
  }
609
+ this.releaseStallWatch?.();
610
+ this.releaseStallWatch = null;
589
611
  // `unref()` would not do: the test runner's process outlives the server, so an un-cleared hold
590
612
  // still fires — against a server that has already stopped.
591
613
  this.stopping = true;
@@ -664,14 +686,42 @@ export class RelayServer {
664
686
  // interval, whatever path the change took. `onAuthChanged` is the same check run early.
665
687
  runHeartbeat(clients = this.wss.clients) {
666
688
  this.revalidateSockets(clients);
689
+ // Agents one line each; everything else summed, because a relay waking from sleep finds every socket
690
+ // stale at once and one line per browser tab would push the agent lines out of the log buffer.
691
+ const others = new Map();
667
692
  for (const ws of clients) {
668
693
  if (!this.isAlive(ws)) {
694
+ const role = this.wsRoles.get(ws);
695
+ if (role === 'agent') {
696
+ this.pushLog(`heartbeat terminated agent ${this.agentNames.get(ws) ?? 'unknown'} — last pong ${this.pongAgeSeconds(ws)}s ago`);
697
+ this.explainedCloses.set(ws, 'terminated');
698
+ }
699
+ else {
700
+ const key = role ?? 'unregistered';
701
+ others.set(key, (others.get(key) ?? 0) + 1);
702
+ }
669
703
  ws.terminate();
670
704
  continue;
671
705
  }
672
706
  if (ws.readyState === WebSocket.OPEN)
673
707
  ws.ping();
674
708
  }
709
+ if (others.size > 0) {
710
+ this.pushLog(`heartbeat terminated ${[...others].map(([role, n]) => `${n} ${role}`).join(', ')} socket(s)`);
711
+ }
712
+ }
713
+ /** A sweep that runs late means this process was held, or the machine slept: the pongs it judges were
714
+ * never read, so its terminations say nothing about the clients. Same clock as `isAlive`. */
715
+ noteLateSweep(now = Date.now()) {
716
+ const late = now - this.lastSweepAt - HEARTBEAT_MS;
717
+ if (late > HEARTBEAT_MS / 2) {
718
+ this.pushLog(`heartbeat sweep ran ${Math.round(late / 1000)}s late — the relay was stalled or the system slept; this sweep may end sockets that were alive`);
719
+ }
720
+ this.lastSweepAt = now;
721
+ }
722
+ pongAgeSeconds(ws) {
723
+ const at = this.lastPongAt.get(ws);
724
+ return at === undefined ? '?' : Math.round((Date.now() - at) / 1000);
675
725
  }
676
726
  /**
677
727
  * Injected into the handlers whose write can take access away — member update and removal, token
@@ -897,6 +947,24 @@ export class RelayServer {
897
947
  remoteAddressOf(request) {
898
948
  return request.socket.remoteAddress ?? '';
899
949
  }
950
+ /**
951
+ * Called on both doors — the WebSocket upgrade and every HTTP request — since `auth/init` and the logs
952
+ * route decide locality on the HTTP side. Not on the tunnel listener, where every request is remote
953
+ * whatever it carries.
954
+ */
955
+ warnTrustedProxyWithoutXffOnce(request) {
956
+ if (this.warnedTrustedProxyWithoutXff || isTunnelIngress(request))
957
+ return;
958
+ const header = proxiedWithoutForwardedFor(this.remoteAddressOf(request), request.headers, this.options.trustedProxies ?? []);
959
+ if (!header)
960
+ return;
961
+ logger.warn(`A request from a trusted proxy (TAPFLOW_TRUSTED_PROXIES) carried ${header} but no X-Forwarded-For. ` +
962
+ 'Without that header the relay cannot see the real client, so it treats the request as coming from the ' +
963
+ 'proxy itself: local, and so unauthenticated, when that address is loopback. Configure the proxy to ' +
964
+ 'send X-Forwarded-For. A proxy on this host can instead use the tunnel port, which is open only when a ' +
965
+ 'tunnel is configured or TAPFLOW_TUNNEL_PORT names a port.');
966
+ this.warnedTrustedProxyWithoutXff = true;
967
+ }
900
968
  warnProxyMisconfigOnce(socketAddr, forwardedFor) {
901
969
  if (this.warnedProxyMisconfig)
902
970
  return;
@@ -906,12 +974,19 @@ export class RelayServer {
906
974
  if (a === '::1' || a.startsWith('127.')) {
907
975
  logger.warn('Received X-Forwarded-For from a loopback connection but TAPFLOW_TRUSTED_PROXIES is unset. ' +
908
976
  'If a same-host reverse proxy or `tailscale serve` forwards to this port, every client it forwards is ' +
909
- 'treated as localhost (unauthenticated). Point it at the tunnel port instead (TAPFLOW_TUNNEL_PORT, ' +
910
- 'default 4001), or set TAPFLOW_TRUSTED_PROXIES so the real client IP is used.');
977
+ 'treated as localhost (unauthenticated). Point it at the tunnel port instead (open only when a tunnel ' +
978
+ 'is configured or TAPFLOW_TUNNEL_PORT names a port), or set TAPFLOW_TRUSTED_PROXIES so the real ' +
979
+ 'client IP is used.');
911
980
  this.warnedProxyMisconfig = true;
912
981
  }
913
982
  }
914
983
  handleConnection(ws, request) {
984
+ // **First, before anything can return.** A frame that breaks the protocol makes `ws` emit `'error'` on
985
+ // this socket (a reset it handles itself), and an EventEmitter with no `'error'` listener throws: from
986
+ // inside `ws`'s receiver, with nothing above it to catch. One client could end the relay for everyone,
987
+ // signed in or not. `ws` closes the socket itself after emitting, so the `'close'` handler below still runs.
988
+ // Debug, not warn: any client can produce this at will, and the close is already logged where it matters.
989
+ ws.on('error', (err) => logger.debug(`WebSocket error: ${err.message}`));
915
990
  const socketAddr = this.remoteAddressOf(request);
916
991
  const xff = request.headers['x-forwarded-for'];
917
992
  const forwardedFor = Array.isArray(xff) ? xff[0] : xff;
@@ -919,6 +994,7 @@ export class RelayServer {
919
994
  // A proxy in front of the tunnel port adds the header too, and there it changes nothing about auth.
920
995
  if (!viaTunnel)
921
996
  this.warnProxyMisconfigOnce(socketAddr, forwardedFor);
997
+ this.warnTrustedProxyWithoutXffOnce(request);
922
998
  const { addr, isLocal } = resolveClientAddress({
923
999
  socketAddr,
924
1000
  forwardedFor,
@@ -1085,7 +1161,19 @@ export class RelayServer {
1085
1161
  logger.error(`route failed for ${inbound.msg.type}:`, e);
1086
1162
  }
1087
1163
  });
1088
- ws.on('close', () => {
1164
+ ws.on('close', (code, reason) => {
1165
+ // Not while stopping: the relay ending every socket itself says nothing about the agents.
1166
+ if (this.wsRoles.get(ws) === 'agent' && !this.stopping) {
1167
+ const name = this.agentNames.get(ws) ?? 'unknown';
1168
+ const explained = this.explainedCloses.get(ws);
1169
+ if (explained === 'replaced') {
1170
+ this.pushLog(`agent socket replaced: ${name} registered again on a new connection`);
1171
+ }
1172
+ else if (!explained) {
1173
+ const why = reason.length > 0 ? ` ${reason.toString()}` : '';
1174
+ this.pushLog(`agent socket closed: ${name} (code ${code}${why}, last pong ${this.pongAgeSeconds(ws)}s ago)`);
1175
+ }
1176
+ }
1089
1177
  this.wsRoles.delete(ws);
1090
1178
  this.wsExternal.delete(ws);
1091
1179
  this.forgetShutdownRequesters(ws);
@@ -2040,6 +2128,7 @@ export class RelayServer {
2040
2128
  // gone and its in-flight screenshots would be undiscoverable — reject them here instead.
2041
2129
  // The rebound sessions have already moved off `old`, so this no longer covers them.
2042
2130
  this.evictAgentSocket(old, 'replaced');
2131
+ this.explainedCloses.set(old, 'replaced');
2043
2132
  old.terminate();
2044
2133
  }
2045
2134
  }
@@ -2097,7 +2186,9 @@ export class RelayServer {
2097
2186
  // transition, matching the disconnect line in evictAgentSocket.
2098
2187
  // `||`, not `??`: the schema defaults both of these to `''` for an agent that omits them, so `??`
2099
2188
  // would print an empty name and an empty platform where this used to print `unknown`.
2100
- logger.info(`agent connected: ${msg.agentName || msg.agentId || 'unknown'} (${msg.platform || 'unknown'}) — ${registeredSessions.length} device(s)`);
2189
+ this.agentNames.set(ws, msg.agentName || msg.agentId || 'unknown');
2190
+ // Through `pushLog` too, so `tapflow logs` shows an agent coming back after the line about it leaving.
2191
+ this.pushLog(`agent connected: ${msg.agentName || msg.agentId || 'unknown'} (${msg.platform || 'unknown'}) — ${registeredSessions.length} device(s)`);
2101
2192
  }
2102
2193
  /** The only producer of `error` — all five exits below, and nothing else in the repo sends that message.
2103
2194
  *
@@ -2791,6 +2882,7 @@ export class RelayServer {
2791
2882
  this.resourceBuffers.clear();
2792
2883
  }
2793
2884
  async handleRequest(req, res) {
2885
+ this.warnTrustedProxyWithoutXffOnce(req);
2794
2886
  const corsHeaders = resolveCorsHeaders(req.headers.origin, this.corsAllowed);
2795
2887
  if (corsHeaders) {
2796
2888
  for (const [k, v] of Object.entries(corsHeaders))