@kortix/agent-tunnel 0.13.54 → 0.13.55

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.
package/dist/agent-cli.js CHANGED
@@ -4036,6 +4036,10 @@ function verifyMessageSignature(signingKey, payload, nonce, signature) {
4036
4036
 
4037
4037
  // src/agent/agent.ts
4038
4038
  var AGENT_VERSION = agentTunnelVersion();
4039
+ function openRelaySocket(url) {
4040
+ const RelaySocket = WebSocket;
4041
+ return new RelaySocket(url, { headers: { "User-Agent": `kortix-agent-tunnel/${AGENT_VERSION}` } });
4042
+ }
4039
4043
  var AUTH_REJECTED_CLOSE_CODES = [4001, 4003];
4040
4044
  var RELAY_FAULT_4001_REASONS = new Set(["auth timeout", "authentication error", "authentication response failed"]);
4041
4045
  function isCredentialRejection(code, reason = "") {
@@ -4137,7 +4141,7 @@ class TunnelAgent {
4137
4141
  this.connectStartedAt = this.now();
4138
4142
  this.setStatus("connecting");
4139
4143
  try {
4140
- this.ws = new WebSocket(new URL(wsUrl));
4144
+ this.ws = openRelaySocket(new URL(wsUrl).href);
4141
4145
  this.setupWsHandlers();
4142
4146
  } catch (err) {
4143
4147
  log(`${c.red}✗${c.reset}`, `Connection failed`);
@@ -5677,17 +5681,17 @@ function probeCredentials(config, options = {}) {
5677
5681
  return new Promise((resolve3) => {
5678
5682
  let socket;
5679
5683
  try {
5680
- socket = new WebSocket(new URL(buildTunnelWsUrl(config)));
5684
+ socket = openRelaySocket(new URL(buildTunnelWsUrl(config)).href);
5681
5685
  } catch {
5682
5686
  resolve3("unreachable");
5683
5687
  return;
5684
5688
  }
5685
5689
  const settle = (result) => {
5686
5690
  clearTimeout(timer);
5691
+ resolve3(result);
5687
5692
  try {
5688
5693
  socket.close(1000, "probe complete");
5689
5694
  } catch {}
5690
- resolve3(result);
5691
5695
  };
5692
5696
  const timer = setTimeout(() => settle("unreachable"), options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
5693
5697
  socket.addEventListener("open", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kortix/agent-tunnel",
3
- "version": "0.13.54",
3
+ "version": "0.13.55",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/kortix-ai/suna.git",
@@ -32,6 +32,16 @@ import { signMessage, verifyMessageSignature } from '../shared/crypto';
32
32
 
33
33
  export const AGENT_VERSION = agentTunnelVersion();
34
34
 
35
+ /**
36
+ * Opens a socket to the relay. It names the agent: Bun sends no User-Agent,
37
+ * and Cloudflare answers an upgrade without one with 403. Bun, Node 22 and
38
+ * `ws` all take `{ headers }` as the second argument.
39
+ */
40
+ export function openRelaySocket(url: string): WebSocket {
41
+ const RelaySocket = WebSocket as unknown as new (url: string, init: { headers: Record<string, string> }) => WebSocket;
42
+ return new RelaySocket(url, { headers: { 'User-Agent': `kortix-agent-tunnel/${AGENT_VERSION}` } });
43
+ }
44
+
35
45
  /**
36
46
  * Relay close codes that mean the credential itself is bad. They are terminal:
37
47
  * reconnecting with the same token can never succeed.
@@ -232,7 +242,7 @@ export class TunnelAgent {
232
242
 
233
243
  try {
234
244
  // lgtm[js/file-access-to-http] Tunnel endpoint is intentionally loaded from trusted local config.
235
- this.ws = new WebSocket(new URL(wsUrl));
245
+ this.ws = openRelaySocket(new URL(wsUrl).href);
236
246
  this.setupWsHandlers();
237
247
  } catch (err) {
238
248
  log(`${c.red}✗${c.reset}`, `Connection failed`);
@@ -1,5 +1,5 @@
1
1
  import '../node-ws-polyfill';
2
- import { AGENT_REPLACED_CLOSE_CODE, AGENT_VERSION, isCredentialRejection } from './agent';
2
+ import { AGENT_REPLACED_CLOSE_CODE, AGENT_VERSION, isCredentialRejection, openRelaySocket } from './agent';
3
3
  import { buildTunnelWsUrl, trustedCredential, type TunnelConfig } from './config';
4
4
 
5
5
  /**
@@ -36,18 +36,18 @@ export function probeCredentials(
36
36
  return new Promise<CredentialProbeResult>((resolve) => {
37
37
  let socket: WebSocket;
38
38
  try {
39
- socket = new WebSocket(new URL(buildTunnelWsUrl(config)));
39
+ socket = openRelaySocket(new URL(buildTunnelWsUrl(config)).href);
40
40
  } catch {
41
41
  resolve('unreachable');
42
42
  return;
43
43
  }
44
44
 
45
45
  // resolve() is idempotent, so the first verdict wins and later events are
46
- // harmless no-ops. That removes any need to track settled state by hand.
46
+ // harmless no-ops. Resolve before close(): Bun fires 'close' inside it.
47
47
  const settle = (result: CredentialProbeResult): void => {
48
48
  clearTimeout(timer);
49
- try { socket.close(1000, 'probe complete'); } catch {}
50
49
  resolve(result);
50
+ try { socket.close(1000, 'probe complete'); } catch {}
51
51
  };
52
52
 
53
53
  const timer = setTimeout(() => settle('unreachable'), options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
@@ -13,6 +13,7 @@ import {
13
13
  rotateServiceLogs,
14
14
  } from './service';
15
15
  import { collapseRepeatedLines } from './log-format';
16
+ import { AGENT_VERSION } from './agent';
16
17
  import { probeCredentials } from './credential-probe';
17
18
 
18
19
  const CLI_PATH = resolve(import.meta.dir, 'cli.ts');
@@ -104,6 +105,51 @@ describe('service log hygiene', () => {
104
105
  });
105
106
 
106
107
  describe('credential probe and the single-agent rule', () => {
108
+ test('every relay socket opens through openRelaySocket', () => {
109
+ const src = resolve(import.meta.dir, '..');
110
+ const raw: string[] = [];
111
+ for (const file of new Bun.Glob('**/*.ts').scanSync(src)) {
112
+ if (file.endsWith('.test.ts')) continue;
113
+ readFileSync(join(src, file), 'utf8').split('\n').forEach((line, i) => {
114
+ if (/\bnew WebSocket\(/.test(line) && !/^\s*(\/\/|\*)/.test(line)) raw.push(`${file}:${i + 1}`);
115
+ });
116
+ }
117
+ expect(raw).toEqual([]);
118
+ });
119
+
120
+ test('auth_ok is valid, though Bun fires close inside socket.close()', async () => {
121
+ let userAgent: string | null = null as string | null;
122
+ const server = Bun.serve({
123
+ port: 0,
124
+ fetch(request, server) {
125
+ // Cloudflare refuses an upgrade without a User-Agent, and Bun sends none by default.
126
+ userAgent = request.headers.get('user-agent');
127
+ return server.upgrade(request) ? undefined : new Response('no upgrade', { status: 400 });
128
+ },
129
+ websocket: {
130
+ message(ws) {
131
+ ws.send(JSON.stringify({ type: 'auth_ok' }));
132
+ },
133
+ },
134
+ });
135
+
136
+ try {
137
+ const result = await probeCredentials(
138
+ {
139
+ apiUrl: `http://127.0.0.1:${server.port}/v1/tunnel`,
140
+ tunnelId: '00000000-0000-4000-8000-000000000001',
141
+ token: 'kortix_tnl_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
142
+ wsPath: '/ws',
143
+ } as never,
144
+ { timeoutMs: 5_000 },
145
+ );
146
+ expect(result).toBe('valid');
147
+ expect(userAgent).toBe(`kortix-agent-tunnel/${AGENT_VERSION}`);
148
+ } finally {
149
+ server.stop(true);
150
+ }
151
+ }, 30_000);
152
+
107
153
  test('treats being replaced as proof the credential is valid', async () => {
108
154
  // The relay only replaces a socket it already registered, and registration
109
155
  // happens after a successful handshake.