@limrun/api 0.28.4 → 0.28.5

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/src/tunnel.ts CHANGED
@@ -42,6 +42,16 @@ export interface Tunnel {
42
42
  onConnectionStateChange: (callback: TunnelConnectionStateCallback) => () => void;
43
43
  }
44
44
 
45
+ export interface ReverseTunnel {
46
+ remoteAddress: {
47
+ address: string;
48
+ port: number;
49
+ };
50
+ close: () => void;
51
+ getConnectionState: () => TunnelConnectionState;
52
+ onConnectionStateChange: (callback: TunnelConnectionStateCallback) => () => void;
53
+ }
54
+
45
55
  export interface TcpTunnelOptions {
46
56
  /**
47
57
  * Maximum number of reconnection attempts
@@ -73,6 +83,38 @@ export interface TcpTunnelOptions {
73
83
  mode?: TunnelMode;
74
84
  }
75
85
 
86
+ export interface ReverseTcpTunnelOptions {
87
+ /**
88
+ * Hostname or IP address of the user-local service.
89
+ * @default '127.0.0.1'
90
+ */
91
+ localHost?: string;
92
+ /**
93
+ * Port of the user-local service.
94
+ */
95
+ localPort: number;
96
+ /**
97
+ * Controls logging verbosity
98
+ * @default 'info'
99
+ */
100
+ logLevel?: LogLevel;
101
+ /**
102
+ * Maximum concurrent remote TCP connections.
103
+ * @default 64
104
+ */
105
+ maxConnections?: number;
106
+ /**
107
+ * Maximum bytes buffered per connection while the local TCP connect is pending.
108
+ * @default 16777216
109
+ */
110
+ maxPendingBytesPerConnection?: number;
111
+ /**
112
+ * Local TCP connect timeout in milliseconds.
113
+ * @default 10000
114
+ */
115
+ connectTimeoutMs?: number;
116
+ }
117
+
76
118
  /**
77
119
  * Starts a persistent TCP → WebSocket proxy.
78
120
  *
@@ -105,6 +147,403 @@ export async function startTcpTunnel(
105
147
  return startSingletonTcpTunnel(remoteURL, token, hostname, port, options);
106
148
  }
107
149
 
150
+ /**
151
+ * Starts a reverse TCP tunnel for client-first protocols.
152
+ *
153
+ * The remote endpoint accepts TCP connections near the simulator and sends them
154
+ * through `remoteURL` as multiplexed WebSocket frames. For each remote connID,
155
+ * this client opens a TCP connection to `localHost:localPort` after the first
156
+ * payload arrives, then pipes bytes in both directions.
157
+ */
158
+ export async function startReverseTcpTunnel(
159
+ remoteURL: string,
160
+ token: string,
161
+ options: ReverseTcpTunnelOptions,
162
+ ): Promise<ReverseTunnel> {
163
+ assertPort(options.localPort, 'localPort', 1, 65535);
164
+
165
+ const localHost = options.localHost ?? '127.0.0.1';
166
+ const localPort = options.localPort;
167
+ const logLevel = options.logLevel ?? 'info';
168
+ const maxConnections = options.maxConnections ?? 64;
169
+ const maxPendingBytesPerConnection = options.maxPendingBytesPerConnection ?? 16 * 1024 * 1024;
170
+ const connectTimeoutMs = options.connectTimeoutMs ?? 10_000;
171
+
172
+ const logger = {
173
+ debug: (...args: any[]) => {
174
+ if (logLevel === 'debug') console.log('[ReverseTunnel]', ...args);
175
+ },
176
+ info: (...args: any[]) => {
177
+ if (logLevel === 'info' || logLevel === 'debug') console.log('[ReverseTunnel]', ...args);
178
+ },
179
+ warn: (...args: any[]) => {
180
+ if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug')
181
+ console.warn('[ReverseTunnel]', ...args);
182
+ },
183
+ error: (...args: any[]) => {
184
+ if (logLevel !== 'none') console.error('[ReverseTunnel]', ...args);
185
+ },
186
+ };
187
+
188
+ return new Promise((resolve, reject) => {
189
+ const connections: Map<number, net.Socket> = new Map();
190
+ const connecting: Set<number> = new Set();
191
+ const pendingPerConn: Map<number, Buffer[]> = new Map();
192
+ const pendingBytesPerConn: Map<number, number> = new Map();
193
+ const connectTimers: Map<number, NodeJS.Timeout> = new Map();
194
+ const recentlyClosedConnIds: Map<number, NodeJS.Timeout> = new Map();
195
+ const stateChangeCallbacks: Set<TunnelConnectionStateCallback> = new Set();
196
+
197
+ let ws: WebSocket | undefined;
198
+ let pingInterval: NodeJS.Timeout | undefined;
199
+ let intentionalDisconnect = false;
200
+ let connectionState: TunnelConnectionState = 'connecting';
201
+ let hasResolved = false;
202
+ let remoteAddress: ReverseTunnel['remoteAddress'] | undefined;
203
+
204
+ const updateConnectionState = (newState: TunnelConnectionState): void => {
205
+ if (connectionState !== newState) {
206
+ connectionState = newState;
207
+ logger.debug(`Connection state changed to: ${newState}`);
208
+ stateChangeCallbacks.forEach((callback) => {
209
+ try {
210
+ callback(newState);
211
+ } catch (err) {
212
+ logger.error('Error in connection state callback:', err);
213
+ }
214
+ });
215
+ }
216
+ };
217
+
218
+ const sendCloseSignal = (connId: number): void => {
219
+ if (ws && ws.readyState === WebSocket.OPEN) {
220
+ ws.send(encodeConnectionHeader(connId));
221
+ }
222
+ };
223
+
224
+ const markRecentlyClosed = (connId: number): void => {
225
+ const existingTimer = recentlyClosedConnIds.get(connId);
226
+ if (existingTimer) {
227
+ clearTimeout(existingTimer);
228
+ }
229
+ const timer = setTimeout(() => {
230
+ recentlyClosedConnIds.delete(connId);
231
+ }, 30_000);
232
+ timer.unref();
233
+ recentlyClosedConnIds.set(connId, timer);
234
+ };
235
+
236
+ const removeConnection = (connId: number, sendClose: boolean, graceful = false): void => {
237
+ const socket = connections.get(connId);
238
+ const connectTimer = connectTimers.get(connId);
239
+ connections.delete(connId);
240
+ connecting.delete(connId);
241
+ pendingPerConn.delete(connId);
242
+ pendingBytesPerConn.delete(connId);
243
+ connectTimers.delete(connId);
244
+ markRecentlyClosed(connId);
245
+ if (connectTimer) {
246
+ clearTimeout(connectTimer);
247
+ }
248
+ if (sendClose) {
249
+ sendCloseSignal(connId);
250
+ }
251
+ if (socket && !socket.destroyed) {
252
+ if (graceful) {
253
+ socket.end();
254
+ setTimeout(() => {
255
+ if (!socket.destroyed) {
256
+ socket.destroy();
257
+ }
258
+ }, 1_000).unref();
259
+ } else {
260
+ socket.destroy();
261
+ }
262
+ }
263
+ };
264
+
265
+ const closeAllConnections = (): void => {
266
+ for (const connId of Array.from(connections.keys())) {
267
+ removeConnection(connId, false);
268
+ }
269
+ connections.clear();
270
+ connecting.clear();
271
+ pendingPerConn.clear();
272
+ pendingBytesPerConn.clear();
273
+ for (const timer of recentlyClosedConnIds.values()) {
274
+ clearTimeout(timer);
275
+ }
276
+ recentlyClosedConnIds.clear();
277
+ };
278
+
279
+ const cleanupWebSocket = (): void => {
280
+ if (pingInterval) {
281
+ clearInterval(pingInterval);
282
+ pingInterval = undefined;
283
+ }
284
+ if (ws) {
285
+ ws.removeAllListeners();
286
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
287
+ ws.close(1000, 'close');
288
+ }
289
+ ws = undefined;
290
+ }
291
+ };
292
+
293
+ const close = (): void => {
294
+ intentionalDisconnect = true;
295
+ cleanupWebSocket();
296
+ closeAllConnections();
297
+ updateConnectionState('disconnected');
298
+ };
299
+
300
+ const getConnectionState = (): TunnelConnectionState => {
301
+ return connectionState;
302
+ };
303
+
304
+ const onConnectionStateChange = (callback: TunnelConnectionStateCallback): (() => void) => {
305
+ stateChangeCallbacks.add(callback);
306
+ return () => {
307
+ stateChangeCallbacks.delete(callback);
308
+ };
309
+ };
310
+
311
+ const resolveReady = (address: ReverseTunnel['remoteAddress']): void => {
312
+ remoteAddress = address;
313
+ hasResolved = true;
314
+ resolve({
315
+ remoteAddress,
316
+ close,
317
+ getConnectionState,
318
+ onConnectionStateChange,
319
+ });
320
+ };
321
+
322
+ const rejectStartup = (err: Error): void => {
323
+ if (!hasResolved) {
324
+ cleanupWebSocket();
325
+ closeAllConnections();
326
+ updateConnectionState('disconnected');
327
+ reject(err);
328
+ } else {
329
+ logger.error(err.message);
330
+ }
331
+ };
332
+
333
+ const flushPending = (connId: number, socket: net.Socket): void => {
334
+ const connectTimer = connectTimers.get(connId);
335
+ if (connectTimer) {
336
+ clearTimeout(connectTimer);
337
+ connectTimers.delete(connId);
338
+ }
339
+ const pending = pendingPerConn.get(connId) ?? [];
340
+ pendingPerConn.delete(connId);
341
+ pendingBytesPerConn.delete(connId);
342
+ connecting.delete(connId);
343
+ for (const payload of pending) {
344
+ socket.write(payload);
345
+ }
346
+ };
347
+
348
+ const connectLocal = (connId: number, firstPayload: Buffer): void => {
349
+ // The current wire protocol has data and close frames, but no explicit
350
+ // "remote TCP accepted" frame. Connect lazily on first payload, which fits
351
+ // HTTP/WebSocket dev servers. Server-first protocols need a protocol
352
+ // extension before this can behave like a transparent TCP forward.
353
+ if (recentlyClosedConnIds.has(connId)) {
354
+ logger.debug(`Ignoring payload for closed conn=${connId}`);
355
+ return;
356
+ }
357
+ if (connections.size >= maxConnections) {
358
+ logger.warn(`Rejecting reverse tunnel conn=${connId}: max connections reached`);
359
+ sendCloseSignal(connId);
360
+ markRecentlyClosed(connId);
361
+ return;
362
+ }
363
+ if (firstPayload.length > maxPendingBytesPerConnection) {
364
+ logger.warn(`Rejecting reverse tunnel conn=${connId}: initial payload exceeds pending byte limit`);
365
+ sendCloseSignal(connId);
366
+ markRecentlyClosed(connId);
367
+ return;
368
+ }
369
+
370
+ pendingPerConn.set(connId, [firstPayload]);
371
+ pendingBytesPerConn.set(connId, firstPayload.length);
372
+ connecting.add(connId);
373
+
374
+ const socket = net.createConnection({ host: localHost, port: localPort });
375
+ connections.set(connId, socket);
376
+ const connectTimer = setTimeout(() => {
377
+ logger.error(`Local TCP connect timed out conn=${connId} after ${connectTimeoutMs}ms`);
378
+ if (connections.has(connId)) {
379
+ removeConnection(connId, true);
380
+ }
381
+ }, connectTimeoutMs);
382
+ connectTimer.unref();
383
+ connectTimers.set(connId, connectTimer);
384
+
385
+ socket.on('connect', () => {
386
+ logger.debug(`Connected conn=${connId} to ${localHost}:${localPort}`);
387
+ flushPending(connId, socket);
388
+ });
389
+
390
+ socket.on('data', (chunk: Buffer) => {
391
+ if (ws && ws.readyState === WebSocket.OPEN) {
392
+ ws.send(Buffer.concat([encodeConnectionHeader(connId), chunk]), (err) => {
393
+ if (err) {
394
+ logger.error(`Failed to send conn=${connId} data: ${err.message}`);
395
+ }
396
+ });
397
+ }
398
+ });
399
+
400
+ socket.on('close', () => {
401
+ logger.debug(`Local TCP connection closed conn=${connId}`);
402
+ if (connections.has(connId)) {
403
+ removeConnection(connId, true);
404
+ }
405
+ });
406
+
407
+ socket.on('error', (err: Error) => {
408
+ logger.error(`Local TCP connection error conn=${connId}: ${err.message}`);
409
+ if (connections.has(connId)) {
410
+ removeConnection(connId, true);
411
+ }
412
+ });
413
+ };
414
+
415
+ const handleBinaryFrame = (buffer: Buffer): void => {
416
+ if (buffer.length < 4) {
417
+ logger.error('Received binary frame shorter than 4 bytes; dropping');
418
+ return;
419
+ }
420
+
421
+ const connId = decodeConnectionHeader(buffer);
422
+ const payload = buffer.subarray(4);
423
+
424
+ if (payload.length === 0) {
425
+ logger.debug(`Received remote close for conn=${connId}`);
426
+ removeConnection(connId, false, true);
427
+ return;
428
+ }
429
+ if (recentlyClosedConnIds.has(connId)) {
430
+ logger.debug(`Ignoring payload for closed conn=${connId}`);
431
+ return;
432
+ }
433
+
434
+ const socket = connections.get(connId);
435
+ if (!socket) {
436
+ connectLocal(connId, payload);
437
+ return;
438
+ }
439
+
440
+ if (connecting.has(connId)) {
441
+ const pending = pendingPerConn.get(connId) ?? [];
442
+ const pendingBytes = (pendingBytesPerConn.get(connId) ?? 0) + payload.length;
443
+ if (pendingBytes > maxPendingBytesPerConnection) {
444
+ logger.warn(`Closing reverse tunnel conn=${connId}: pending byte limit exceeded`);
445
+ removeConnection(connId, true);
446
+ return;
447
+ }
448
+ pending.push(payload);
449
+ pendingPerConn.set(connId, pending);
450
+ pendingBytesPerConn.set(connId, pendingBytes);
451
+ return;
452
+ }
453
+
454
+ socket.write(payload);
455
+ };
456
+
457
+ const handleControlMessage = (data: any): void => {
458
+ let parsed: any;
459
+ try {
460
+ parsed = JSON.parse(data.toString());
461
+ } catch {
462
+ rejectStartup(new Error(`Malformed reverse tunnel control message: ${data.toString()}`));
463
+ return;
464
+ }
465
+
466
+ if (parsed.type === 'ready') {
467
+ if (typeof parsed.remoteHost !== 'string' || typeof parsed.remotePort !== 'number') {
468
+ rejectStartup(new Error(`Malformed reverse tunnel ready message: ${data.toString()}`));
469
+ return;
470
+ }
471
+ logger.info(`Reverse tunnel ready: ${parsed.remoteHost}:${parsed.remotePort}`);
472
+ updateConnectionState('connected');
473
+ resolveReady({ address: parsed.remoteHost, port: parsed.remotePort });
474
+ return;
475
+ }
476
+
477
+ if (parsed.type === 'error') {
478
+ rejectStartup(new Error(parsed.message || 'Reverse tunnel failed to start'));
479
+ return;
480
+ }
481
+
482
+ rejectStartup(new Error(`Unexpected reverse tunnel control message type: ${parsed.type}`));
483
+ };
484
+
485
+ const url = new URL(remoteURL);
486
+ const proxyAgent = nodeProxyTransport.getWebSocketAgent(url.toString());
487
+ ws = new WebSocket(url.toString(), {
488
+ headers: { Authorization: `Bearer ${token}` },
489
+ ...(proxyAgent ? { agent: proxyAgent } : {}),
490
+ perMessageDeflate: false,
491
+ });
492
+
493
+ ws.on('error', (err: Error) => {
494
+ logger.error('WebSocket error:', err.message);
495
+ rejectStartup(err);
496
+ });
497
+
498
+ ws.on('close', (code: number, reason: Buffer) => {
499
+ if (pingInterval) {
500
+ clearInterval(pingInterval);
501
+ pingInterval = undefined;
502
+ }
503
+ logger.debug(`WebSocket disconnected (code=${code}, reason=${reason.toString()})`);
504
+ closeAllConnections();
505
+ updateConnectionState('disconnected');
506
+ if (!intentionalDisconnect && !hasResolved) {
507
+ rejectStartup(
508
+ new Error(`Reverse tunnel WebSocket closed before ready: ${code} ${reason.toString()}`),
509
+ );
510
+ }
511
+ });
512
+
513
+ ws.on('open', () => {
514
+ const socket = ws as WebSocket;
515
+ logger.debug(`Connected WebSocket to ${url.toString()}`);
516
+ pingInterval = setInterval(() => {
517
+ if (socket.readyState === WebSocket.OPEN) {
518
+ (socket as any).ping();
519
+ }
520
+ }, 30_000);
521
+ });
522
+
523
+ ws.on('message', (data: any, isBinary: boolean) => {
524
+ if (!hasResolved) {
525
+ if (isBinary) {
526
+ rejectStartup(new Error('Received reverse tunnel binary frame before ready control message'));
527
+ return;
528
+ }
529
+ handleControlMessage(data);
530
+ return;
531
+ }
532
+
533
+ if (!isBinary) {
534
+ logger.warn(`Ignoring unexpected reverse tunnel text message: ${data.toString()}`);
535
+ return;
536
+ }
537
+
538
+ handleBinaryFrame(
539
+ Buffer.isBuffer(data) ? data
540
+ : Array.isArray(data) ? Buffer.concat(data)
541
+ : Buffer.from(data),
542
+ );
543
+ });
544
+ });
545
+ }
546
+
108
547
  /**
109
548
  * Singleton mode: Single TCP connection forwarded to WebSocket
110
549
  */
@@ -729,6 +1168,12 @@ export const isNonRetryableError = (errMessage: string): boolean => {
729
1168
  return false;
730
1169
  };
731
1170
 
1171
+ export function assertPort(port: number, name: string, min: number, max: number): void {
1172
+ if (!Number.isInteger(port) || port < min || port > max) {
1173
+ throw new Error(`${name} must be an integer between ${min} and ${max}`);
1174
+ }
1175
+ }
1176
+
732
1177
  /**
733
1178
  * Encode a 32-bit connection ID as a 4-byte big-endian header
734
1179
  */
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.28.4'; // x-release-please-version
1
+ export const VERSION = '0.28.5'; // x-release-please-version
package/tunnel.d.mts CHANGED
@@ -32,6 +32,15 @@ export interface Tunnel {
32
32
  */
33
33
  onConnectionStateChange: (callback: TunnelConnectionStateCallback) => () => void;
34
34
  }
35
+ export interface ReverseTunnel {
36
+ remoteAddress: {
37
+ address: string;
38
+ port: number;
39
+ };
40
+ close: () => void;
41
+ getConnectionState: () => TunnelConnectionState;
42
+ onConnectionStateChange: (callback: TunnelConnectionStateCallback) => () => void;
43
+ }
35
44
  export interface TcpTunnelOptions {
36
45
  /**
37
46
  * Maximum number of reconnection attempts
@@ -62,6 +71,37 @@ export interface TcpTunnelOptions {
62
71
  */
63
72
  mode?: TunnelMode;
64
73
  }
74
+ export interface ReverseTcpTunnelOptions {
75
+ /**
76
+ * Hostname or IP address of the user-local service.
77
+ * @default '127.0.0.1'
78
+ */
79
+ localHost?: string;
80
+ /**
81
+ * Port of the user-local service.
82
+ */
83
+ localPort: number;
84
+ /**
85
+ * Controls logging verbosity
86
+ * @default 'info'
87
+ */
88
+ logLevel?: LogLevel;
89
+ /**
90
+ * Maximum concurrent remote TCP connections.
91
+ * @default 64
92
+ */
93
+ maxConnections?: number;
94
+ /**
95
+ * Maximum bytes buffered per connection while the local TCP connect is pending.
96
+ * @default 16777216
97
+ */
98
+ maxPendingBytesPerConnection?: number;
99
+ /**
100
+ * Local TCP connect timeout in milliseconds.
101
+ * @default 10000
102
+ */
103
+ connectTimeoutMs?: number;
104
+ }
65
105
  /**
66
106
  * Starts a persistent TCP → WebSocket proxy.
67
107
  *
@@ -79,7 +119,17 @@ export interface TcpTunnelOptions {
79
119
  * @param options Optional reconnection configuration
80
120
  */
81
121
  export declare function startTcpTunnel(remoteURL: string, token: string, hostname: string, port: number, options?: TcpTunnelOptions): Promise<Tunnel>;
122
+ /**
123
+ * Starts a reverse TCP tunnel for client-first protocols.
124
+ *
125
+ * The remote endpoint accepts TCP connections near the simulator and sends them
126
+ * through `remoteURL` as multiplexed WebSocket frames. For each remote connID,
127
+ * this client opens a TCP connection to `localHost:localPort` after the first
128
+ * payload arrives, then pipes bytes in both directions.
129
+ */
130
+ export declare function startReverseTcpTunnel(remoteURL: string, token: string, options: ReverseTcpTunnelOptions): Promise<ReverseTunnel>;
82
131
  export declare const isNonRetryableError: (errMessage: string) => boolean;
132
+ export declare function assertPort(port: number, name: string, min: number, max: number): void;
83
133
  /**
84
134
  * Encode a 32-bit connection ID as a 4-byte big-endian header
85
135
  */
package/tunnel.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel.d.mts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAymBD,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,KAAG,OAOxD,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7D"}
1
+ {"version":3,"file":"tunnel.d.mts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+XxB;AAymBD,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,KAAG,OAOxD,CAAC;AAEF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAIrF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7D"}
package/tunnel.d.ts CHANGED
@@ -32,6 +32,15 @@ export interface Tunnel {
32
32
  */
33
33
  onConnectionStateChange: (callback: TunnelConnectionStateCallback) => () => void;
34
34
  }
35
+ export interface ReverseTunnel {
36
+ remoteAddress: {
37
+ address: string;
38
+ port: number;
39
+ };
40
+ close: () => void;
41
+ getConnectionState: () => TunnelConnectionState;
42
+ onConnectionStateChange: (callback: TunnelConnectionStateCallback) => () => void;
43
+ }
35
44
  export interface TcpTunnelOptions {
36
45
  /**
37
46
  * Maximum number of reconnection attempts
@@ -62,6 +71,37 @@ export interface TcpTunnelOptions {
62
71
  */
63
72
  mode?: TunnelMode;
64
73
  }
74
+ export interface ReverseTcpTunnelOptions {
75
+ /**
76
+ * Hostname or IP address of the user-local service.
77
+ * @default '127.0.0.1'
78
+ */
79
+ localHost?: string;
80
+ /**
81
+ * Port of the user-local service.
82
+ */
83
+ localPort: number;
84
+ /**
85
+ * Controls logging verbosity
86
+ * @default 'info'
87
+ */
88
+ logLevel?: LogLevel;
89
+ /**
90
+ * Maximum concurrent remote TCP connections.
91
+ * @default 64
92
+ */
93
+ maxConnections?: number;
94
+ /**
95
+ * Maximum bytes buffered per connection while the local TCP connect is pending.
96
+ * @default 16777216
97
+ */
98
+ maxPendingBytesPerConnection?: number;
99
+ /**
100
+ * Local TCP connect timeout in milliseconds.
101
+ * @default 10000
102
+ */
103
+ connectTimeoutMs?: number;
104
+ }
65
105
  /**
66
106
  * Starts a persistent TCP → WebSocket proxy.
67
107
  *
@@ -79,7 +119,17 @@ export interface TcpTunnelOptions {
79
119
  * @param options Optional reconnection configuration
80
120
  */
81
121
  export declare function startTcpTunnel(remoteURL: string, token: string, hostname: string, port: number, options?: TcpTunnelOptions): Promise<Tunnel>;
122
+ /**
123
+ * Starts a reverse TCP tunnel for client-first protocols.
124
+ *
125
+ * The remote endpoint accepts TCP connections near the simulator and sends them
126
+ * through `remoteURL` as multiplexed WebSocket frames. For each remote connID,
127
+ * this client opens a TCP connection to `localHost:localPort` after the first
128
+ * payload arrives, then pipes bytes in both directions.
129
+ */
130
+ export declare function startReverseTcpTunnel(remoteURL: string, token: string, options: ReverseTcpTunnelOptions): Promise<ReverseTunnel>;
82
131
  export declare const isNonRetryableError: (errMessage: string) => boolean;
132
+ export declare function assertPort(port: number, name: string, min: number, max: number): void;
83
133
  /**
84
134
  * Encode a 32-bit connection ID as a 4-byte big-endian header
85
135
  */
package/tunnel.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAymBD,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,KAAG,OAOxD,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7D"}
1
+ {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+XxB;AAymBD,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,KAAG,OAOxD,CAAC;AAEF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAIrF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7D"}