@kortix/agent-tunnel 0.13.54 → 0.13.56
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 =
|
|
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 =
|
|
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
package/src/agent/agent.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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.
|
|
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.
|