@liveport/agent-sdk 0.1.0 → 0.1.2
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/README.md +2 -2
- package/dist/index.d.mts +90 -9
- package/dist/index.d.ts +90 -9
- package/dist/index.js +265 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ new LivePortAgent(config: LivePortAgentConfig)
|
|
|
66
66
|
| Option | Type | Required | Default | Description |
|
|
67
67
|
|--------|------|----------|---------|-------------|
|
|
68
68
|
| `key` | string | ✅ | - | Bridge key for authentication |
|
|
69
|
-
| `apiUrl` | string | ❌ | `https://
|
|
69
|
+
| `apiUrl` | string | ❌ | `https://liveport.dev` | API base URL |
|
|
70
70
|
| `timeout` | number | ❌ | `30000` | Default timeout in milliseconds |
|
|
71
71
|
|
|
72
72
|
**Example:**
|
|
@@ -74,7 +74,7 @@ new LivePortAgent(config: LivePortAgentConfig)
|
|
|
74
74
|
```typescript
|
|
75
75
|
const agent = new LivePortAgent({
|
|
76
76
|
key: 'lpk_abc123...',
|
|
77
|
-
apiUrl: 'https://
|
|
77
|
+
apiUrl: 'https://liveport.dev',
|
|
78
78
|
timeout: 60000
|
|
79
79
|
});
|
|
80
80
|
```
|
package/dist/index.d.mts
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
export { Tunnel } from '@liveport/shared';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* LivePort Agent SDK
|
|
5
3
|
*
|
|
6
4
|
* TypeScript SDK for AI agents to wait for and access localhost tunnels.
|
|
7
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Tunnel record as stored in the database.
|
|
8
|
+
*
|
|
9
|
+
* TODO: Remove this duplicate once @liveport/shared is published to npm
|
|
10
|
+
* and agent-sdk can depend on it directly. Until then, keep in sync with
|
|
11
|
+
* the Tunnel type in packages/shared/src/types/index.ts.
|
|
12
|
+
*/
|
|
13
|
+
interface Tunnel {
|
|
14
|
+
id: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
bridgeKeyId?: string;
|
|
17
|
+
subdomain: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
localPort: number;
|
|
20
|
+
publicUrl: string;
|
|
21
|
+
region: string;
|
|
22
|
+
connectedAt: Date;
|
|
23
|
+
disconnectedAt?: Date;
|
|
24
|
+
requestCount: number;
|
|
25
|
+
bytesTransferred: number;
|
|
26
|
+
}
|
|
8
27
|
interface LivePortAgentConfig {
|
|
9
28
|
/** Bridge key for authentication */
|
|
10
29
|
key: string;
|
|
11
|
-
/** API base URL (default: https://
|
|
30
|
+
/** API base URL (default: https://liveport.dev) */
|
|
12
31
|
apiUrl?: string;
|
|
32
|
+
/** Tunnel server URL for connect() (default: https://tunnel.liveport.online) */
|
|
33
|
+
tunnelUrl?: string;
|
|
13
34
|
/** Default timeout in milliseconds (default: 30000) */
|
|
14
35
|
timeout?: number;
|
|
15
36
|
}
|
|
@@ -19,6 +40,20 @@ interface WaitForTunnelOptions {
|
|
|
19
40
|
/** Poll interval in milliseconds (default: 1000) */
|
|
20
41
|
pollInterval?: number;
|
|
21
42
|
}
|
|
43
|
+
interface ConnectOptions {
|
|
44
|
+
/** Tunnel server URL (overrides tunnelUrl from config) */
|
|
45
|
+
serverUrl?: string;
|
|
46
|
+
/** Connection timeout in milliseconds (default: 30000) */
|
|
47
|
+
timeout?: number;
|
|
48
|
+
}
|
|
49
|
+
interface WaitForReadyOptions {
|
|
50
|
+
/** Timeout in milliseconds (default: 30000) */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
/** Poll interval in milliseconds (default: 1000) */
|
|
53
|
+
pollInterval?: number;
|
|
54
|
+
/** Health check path (default: "/") */
|
|
55
|
+
healthPath?: string;
|
|
56
|
+
}
|
|
22
57
|
interface AgentTunnel {
|
|
23
58
|
tunnelId: string;
|
|
24
59
|
subdomain: string;
|
|
@@ -37,10 +72,14 @@ declare class ApiError extends Error {
|
|
|
37
72
|
readonly code: string;
|
|
38
73
|
constructor(statusCode: number, code: string, message: string);
|
|
39
74
|
}
|
|
75
|
+
/** Error thrown when WebSocket connection fails */
|
|
76
|
+
declare class ConnectionError extends Error {
|
|
77
|
+
constructor(message: string);
|
|
78
|
+
}
|
|
40
79
|
/**
|
|
41
80
|
* LivePort Agent SDK
|
|
42
81
|
*
|
|
43
|
-
* Allows AI agents to wait for and access localhost tunnels.
|
|
82
|
+
* Allows AI agents to wait for, connect to, and access localhost tunnels.
|
|
44
83
|
*
|
|
45
84
|
* @example
|
|
46
85
|
* ```typescript
|
|
@@ -50,9 +89,12 @@ declare class ApiError extends Error {
|
|
|
50
89
|
* key: process.env.LIVEPORT_BRIDGE_KEY!
|
|
51
90
|
* });
|
|
52
91
|
*
|
|
53
|
-
* //
|
|
54
|
-
* const tunnel = await agent.
|
|
55
|
-
* console.log(`
|
|
92
|
+
* // Create a tunnel to local port 3000
|
|
93
|
+
* const tunnel = await agent.connect(3000);
|
|
94
|
+
* console.log(`Tunnel URL: ${tunnel.url}`);
|
|
95
|
+
*
|
|
96
|
+
* // Wait for the local server to be reachable through the tunnel
|
|
97
|
+
* await agent.waitForReady(tunnel);
|
|
56
98
|
*
|
|
57
99
|
* // Run your tests against tunnel.url
|
|
58
100
|
*
|
|
@@ -63,7 +105,33 @@ declare class ApiError extends Error {
|
|
|
63
105
|
declare class LivePortAgent {
|
|
64
106
|
private config;
|
|
65
107
|
private abortController;
|
|
108
|
+
private wsConnection;
|
|
66
109
|
constructor(config: LivePortAgentConfig);
|
|
110
|
+
/**
|
|
111
|
+
* Connect to the tunnel server and create a tunnel for the given local port.
|
|
112
|
+
*
|
|
113
|
+
* Opens a WebSocket to the tunnel server, authenticates with the bridge key,
|
|
114
|
+
* and waits for a tunnel assignment. Incoming HTTP requests from the tunnel
|
|
115
|
+
* are forwarded to localhost:<port>.
|
|
116
|
+
*
|
|
117
|
+
* @param port - The local port to tunnel
|
|
118
|
+
* @param options - Connection options
|
|
119
|
+
* @returns The tunnel info once connected
|
|
120
|
+
* @throws ConnectionError if the connection fails or times out
|
|
121
|
+
*/
|
|
122
|
+
connect(port: number, options?: ConnectOptions): Promise<AgentTunnel>;
|
|
123
|
+
/**
|
|
124
|
+
* Wait for the tunnel's public URL to become reachable.
|
|
125
|
+
*
|
|
126
|
+
* Polls the tunnel's public URL (not localhost) with HTTP GET requests
|
|
127
|
+
* until a 2xx response is received, or the timeout is exceeded. This
|
|
128
|
+
* validates the full tunnel path end-to-end.
|
|
129
|
+
*
|
|
130
|
+
* @param tunnel - The tunnel to check
|
|
131
|
+
* @param options - Wait options
|
|
132
|
+
* @throws TunnelTimeoutError if the tunnel is not ready within timeout
|
|
133
|
+
*/
|
|
134
|
+
waitForReady(tunnel: AgentTunnel, options?: WaitForReadyOptions): Promise<void>;
|
|
67
135
|
/**
|
|
68
136
|
* Wait for a tunnel to become available
|
|
69
137
|
*
|
|
@@ -85,9 +153,22 @@ declare class LivePortAgent {
|
|
|
85
153
|
/**
|
|
86
154
|
* Disconnect and clean up
|
|
87
155
|
*
|
|
88
|
-
* Cancels any pending waitForTunnel calls
|
|
156
|
+
* Cancels any pending waitForTunnel calls and closes any WebSocket
|
|
157
|
+
* connection created by connect().
|
|
89
158
|
*/
|
|
90
159
|
disconnect(): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Build WebSocket URL from server URL
|
|
162
|
+
*/
|
|
163
|
+
private buildWebSocketUrl;
|
|
164
|
+
/**
|
|
165
|
+
* Handle incoming WebSocket messages from the tunnel server
|
|
166
|
+
*/
|
|
167
|
+
private handleWsMessage;
|
|
168
|
+
/**
|
|
169
|
+
* Forward an HTTP request from the tunnel server to localhost
|
|
170
|
+
*/
|
|
171
|
+
private handleHttpRequest;
|
|
91
172
|
/**
|
|
92
173
|
* Make an authenticated API request
|
|
93
174
|
*/
|
|
@@ -102,4 +183,4 @@ declare class LivePortAgent {
|
|
|
102
183
|
private sleep;
|
|
103
184
|
}
|
|
104
185
|
|
|
105
|
-
export { type AgentTunnel, ApiError, LivePortAgent, type LivePortAgentConfig, TunnelTimeoutError, type WaitForTunnelOptions };
|
|
186
|
+
export { type AgentTunnel, ApiError, type ConnectOptions, ConnectionError, LivePortAgent, type LivePortAgentConfig, type Tunnel, TunnelTimeoutError, type WaitForReadyOptions, type WaitForTunnelOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
export { Tunnel } from '@liveport/shared';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* LivePort Agent SDK
|
|
5
3
|
*
|
|
6
4
|
* TypeScript SDK for AI agents to wait for and access localhost tunnels.
|
|
7
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Tunnel record as stored in the database.
|
|
8
|
+
*
|
|
9
|
+
* TODO: Remove this duplicate once @liveport/shared is published to npm
|
|
10
|
+
* and agent-sdk can depend on it directly. Until then, keep in sync with
|
|
11
|
+
* the Tunnel type in packages/shared/src/types/index.ts.
|
|
12
|
+
*/
|
|
13
|
+
interface Tunnel {
|
|
14
|
+
id: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
bridgeKeyId?: string;
|
|
17
|
+
subdomain: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
localPort: number;
|
|
20
|
+
publicUrl: string;
|
|
21
|
+
region: string;
|
|
22
|
+
connectedAt: Date;
|
|
23
|
+
disconnectedAt?: Date;
|
|
24
|
+
requestCount: number;
|
|
25
|
+
bytesTransferred: number;
|
|
26
|
+
}
|
|
8
27
|
interface LivePortAgentConfig {
|
|
9
28
|
/** Bridge key for authentication */
|
|
10
29
|
key: string;
|
|
11
|
-
/** API base URL (default: https://
|
|
30
|
+
/** API base URL (default: https://liveport.dev) */
|
|
12
31
|
apiUrl?: string;
|
|
32
|
+
/** Tunnel server URL for connect() (default: https://tunnel.liveport.online) */
|
|
33
|
+
tunnelUrl?: string;
|
|
13
34
|
/** Default timeout in milliseconds (default: 30000) */
|
|
14
35
|
timeout?: number;
|
|
15
36
|
}
|
|
@@ -19,6 +40,20 @@ interface WaitForTunnelOptions {
|
|
|
19
40
|
/** Poll interval in milliseconds (default: 1000) */
|
|
20
41
|
pollInterval?: number;
|
|
21
42
|
}
|
|
43
|
+
interface ConnectOptions {
|
|
44
|
+
/** Tunnel server URL (overrides tunnelUrl from config) */
|
|
45
|
+
serverUrl?: string;
|
|
46
|
+
/** Connection timeout in milliseconds (default: 30000) */
|
|
47
|
+
timeout?: number;
|
|
48
|
+
}
|
|
49
|
+
interface WaitForReadyOptions {
|
|
50
|
+
/** Timeout in milliseconds (default: 30000) */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
/** Poll interval in milliseconds (default: 1000) */
|
|
53
|
+
pollInterval?: number;
|
|
54
|
+
/** Health check path (default: "/") */
|
|
55
|
+
healthPath?: string;
|
|
56
|
+
}
|
|
22
57
|
interface AgentTunnel {
|
|
23
58
|
tunnelId: string;
|
|
24
59
|
subdomain: string;
|
|
@@ -37,10 +72,14 @@ declare class ApiError extends Error {
|
|
|
37
72
|
readonly code: string;
|
|
38
73
|
constructor(statusCode: number, code: string, message: string);
|
|
39
74
|
}
|
|
75
|
+
/** Error thrown when WebSocket connection fails */
|
|
76
|
+
declare class ConnectionError extends Error {
|
|
77
|
+
constructor(message: string);
|
|
78
|
+
}
|
|
40
79
|
/**
|
|
41
80
|
* LivePort Agent SDK
|
|
42
81
|
*
|
|
43
|
-
* Allows AI agents to wait for and access localhost tunnels.
|
|
82
|
+
* Allows AI agents to wait for, connect to, and access localhost tunnels.
|
|
44
83
|
*
|
|
45
84
|
* @example
|
|
46
85
|
* ```typescript
|
|
@@ -50,9 +89,12 @@ declare class ApiError extends Error {
|
|
|
50
89
|
* key: process.env.LIVEPORT_BRIDGE_KEY!
|
|
51
90
|
* });
|
|
52
91
|
*
|
|
53
|
-
* //
|
|
54
|
-
* const tunnel = await agent.
|
|
55
|
-
* console.log(`
|
|
92
|
+
* // Create a tunnel to local port 3000
|
|
93
|
+
* const tunnel = await agent.connect(3000);
|
|
94
|
+
* console.log(`Tunnel URL: ${tunnel.url}`);
|
|
95
|
+
*
|
|
96
|
+
* // Wait for the local server to be reachable through the tunnel
|
|
97
|
+
* await agent.waitForReady(tunnel);
|
|
56
98
|
*
|
|
57
99
|
* // Run your tests against tunnel.url
|
|
58
100
|
*
|
|
@@ -63,7 +105,33 @@ declare class ApiError extends Error {
|
|
|
63
105
|
declare class LivePortAgent {
|
|
64
106
|
private config;
|
|
65
107
|
private abortController;
|
|
108
|
+
private wsConnection;
|
|
66
109
|
constructor(config: LivePortAgentConfig);
|
|
110
|
+
/**
|
|
111
|
+
* Connect to the tunnel server and create a tunnel for the given local port.
|
|
112
|
+
*
|
|
113
|
+
* Opens a WebSocket to the tunnel server, authenticates with the bridge key,
|
|
114
|
+
* and waits for a tunnel assignment. Incoming HTTP requests from the tunnel
|
|
115
|
+
* are forwarded to localhost:<port>.
|
|
116
|
+
*
|
|
117
|
+
* @param port - The local port to tunnel
|
|
118
|
+
* @param options - Connection options
|
|
119
|
+
* @returns The tunnel info once connected
|
|
120
|
+
* @throws ConnectionError if the connection fails or times out
|
|
121
|
+
*/
|
|
122
|
+
connect(port: number, options?: ConnectOptions): Promise<AgentTunnel>;
|
|
123
|
+
/**
|
|
124
|
+
* Wait for the tunnel's public URL to become reachable.
|
|
125
|
+
*
|
|
126
|
+
* Polls the tunnel's public URL (not localhost) with HTTP GET requests
|
|
127
|
+
* until a 2xx response is received, or the timeout is exceeded. This
|
|
128
|
+
* validates the full tunnel path end-to-end.
|
|
129
|
+
*
|
|
130
|
+
* @param tunnel - The tunnel to check
|
|
131
|
+
* @param options - Wait options
|
|
132
|
+
* @throws TunnelTimeoutError if the tunnel is not ready within timeout
|
|
133
|
+
*/
|
|
134
|
+
waitForReady(tunnel: AgentTunnel, options?: WaitForReadyOptions): Promise<void>;
|
|
67
135
|
/**
|
|
68
136
|
* Wait for a tunnel to become available
|
|
69
137
|
*
|
|
@@ -85,9 +153,22 @@ declare class LivePortAgent {
|
|
|
85
153
|
/**
|
|
86
154
|
* Disconnect and clean up
|
|
87
155
|
*
|
|
88
|
-
* Cancels any pending waitForTunnel calls
|
|
156
|
+
* Cancels any pending waitForTunnel calls and closes any WebSocket
|
|
157
|
+
* connection created by connect().
|
|
89
158
|
*/
|
|
90
159
|
disconnect(): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Build WebSocket URL from server URL
|
|
162
|
+
*/
|
|
163
|
+
private buildWebSocketUrl;
|
|
164
|
+
/**
|
|
165
|
+
* Handle incoming WebSocket messages from the tunnel server
|
|
166
|
+
*/
|
|
167
|
+
private handleWsMessage;
|
|
168
|
+
/**
|
|
169
|
+
* Forward an HTTP request from the tunnel server to localhost
|
|
170
|
+
*/
|
|
171
|
+
private handleHttpRequest;
|
|
91
172
|
/**
|
|
92
173
|
* Make an authenticated API request
|
|
93
174
|
*/
|
|
@@ -102,4 +183,4 @@ declare class LivePortAgent {
|
|
|
102
183
|
private sleep;
|
|
103
184
|
}
|
|
104
185
|
|
|
105
|
-
export { type AgentTunnel, ApiError, LivePortAgent, type LivePortAgentConfig, TunnelTimeoutError, type WaitForTunnelOptions };
|
|
186
|
+
export { type AgentTunnel, ApiError, type ConnectOptions, ConnectionError, LivePortAgent, type LivePortAgentConfig, type Tunnel, TunnelTimeoutError, type WaitForReadyOptions, type WaitForTunnelOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,16 +17,26 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
ApiError: () => ApiError,
|
|
34
|
+
ConnectionError: () => ConnectionError,
|
|
24
35
|
LivePortAgent: () => LivePortAgent,
|
|
25
36
|
TunnelTimeoutError: () => TunnelTimeoutError
|
|
26
37
|
});
|
|
27
38
|
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
var import_ws = __toESM(require("ws"));
|
|
28
40
|
var TunnelTimeoutError = class extends Error {
|
|
29
41
|
constructor(timeout) {
|
|
30
42
|
super(`Tunnel not available within ${timeout}ms timeout`);
|
|
@@ -41,19 +53,150 @@ var ApiError = class extends Error {
|
|
|
41
53
|
this.code = code;
|
|
42
54
|
}
|
|
43
55
|
};
|
|
56
|
+
var ConnectionError = class extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "ConnectionError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
44
62
|
var LivePortAgent = class {
|
|
45
63
|
config;
|
|
46
64
|
abortController = null;
|
|
65
|
+
wsConnection = null;
|
|
47
66
|
constructor(config) {
|
|
48
67
|
if (!config.key) {
|
|
49
68
|
throw new Error("Bridge key is required");
|
|
50
69
|
}
|
|
51
70
|
this.config = {
|
|
52
71
|
key: config.key,
|
|
53
|
-
apiUrl: config.apiUrl || "https://
|
|
72
|
+
apiUrl: config.apiUrl || "https://liveport.dev",
|
|
73
|
+
tunnelUrl: config.tunnelUrl || "https://tunnel.liveport.online",
|
|
54
74
|
timeout: config.timeout || 3e4
|
|
55
75
|
};
|
|
56
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Connect to the tunnel server and create a tunnel for the given local port.
|
|
79
|
+
*
|
|
80
|
+
* Opens a WebSocket to the tunnel server, authenticates with the bridge key,
|
|
81
|
+
* and waits for a tunnel assignment. Incoming HTTP requests from the tunnel
|
|
82
|
+
* are forwarded to localhost:<port>.
|
|
83
|
+
*
|
|
84
|
+
* @param port - The local port to tunnel
|
|
85
|
+
* @param options - Connection options
|
|
86
|
+
* @returns The tunnel info once connected
|
|
87
|
+
* @throws ConnectionError if the connection fails or times out
|
|
88
|
+
*/
|
|
89
|
+
async connect(port, options) {
|
|
90
|
+
if (this.wsConnection) {
|
|
91
|
+
throw new ConnectionError("Already connected \u2014 call disconnect() first");
|
|
92
|
+
}
|
|
93
|
+
const serverUrl = options?.serverUrl || this.config.tunnelUrl;
|
|
94
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
95
|
+
const WS = options?._WebSocketClass ?? import_ws.default;
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const wsUrl = this.buildWebSocketUrl(serverUrl);
|
|
98
|
+
const headers = {
|
|
99
|
+
"X-Bridge-Key": this.config.key,
|
|
100
|
+
"X-Local-Port": String(port)
|
|
101
|
+
};
|
|
102
|
+
const socket = new WS(wsUrl, {
|
|
103
|
+
headers,
|
|
104
|
+
perMessageDeflate: false
|
|
105
|
+
});
|
|
106
|
+
this.wsConnection = socket;
|
|
107
|
+
let settled = false;
|
|
108
|
+
const connectTimeout = setTimeout(() => {
|
|
109
|
+
if (!settled) {
|
|
110
|
+
settled = true;
|
|
111
|
+
socket.close();
|
|
112
|
+
reject(new ConnectionError("Connection timeout"));
|
|
113
|
+
}
|
|
114
|
+
}, timeout);
|
|
115
|
+
socket.on("open", () => {
|
|
116
|
+
});
|
|
117
|
+
socket.on("message", (data) => {
|
|
118
|
+
try {
|
|
119
|
+
const message = JSON.parse(data.toString());
|
|
120
|
+
this.handleWsMessage(message, port, socket);
|
|
121
|
+
if (message.type === "connected" && !settled) {
|
|
122
|
+
clearTimeout(connectTimeout);
|
|
123
|
+
settled = true;
|
|
124
|
+
const payload = message.payload;
|
|
125
|
+
const tunnel = {
|
|
126
|
+
tunnelId: payload.tunnelId,
|
|
127
|
+
subdomain: payload.subdomain,
|
|
128
|
+
url: payload.url,
|
|
129
|
+
localPort: port,
|
|
130
|
+
// Use server-provided createdAt if the server sends it; fall back to client time
|
|
131
|
+
// (ConnectedPayload doesn't include createdAt today, but may in a future server version)
|
|
132
|
+
createdAt: payload.createdAt ? new Date(payload.createdAt) : /* @__PURE__ */ new Date(),
|
|
133
|
+
expiresAt: new Date(payload.expiresAt)
|
|
134
|
+
};
|
|
135
|
+
resolve(tunnel);
|
|
136
|
+
} else if (message.type === "error" && message.payload?.fatal && !settled) {
|
|
137
|
+
clearTimeout(connectTimeout);
|
|
138
|
+
settled = true;
|
|
139
|
+
reject(new ConnectionError(message.payload.message));
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
socket.on("close", () => {
|
|
145
|
+
clearTimeout(connectTimeout);
|
|
146
|
+
if (!settled) {
|
|
147
|
+
settled = true;
|
|
148
|
+
reject(new ConnectionError("Connection closed before tunnel was established"));
|
|
149
|
+
}
|
|
150
|
+
if (this.wsConnection === socket) {
|
|
151
|
+
this.wsConnection = null;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
socket.on("error", (err) => {
|
|
155
|
+
clearTimeout(connectTimeout);
|
|
156
|
+
if (!settled) {
|
|
157
|
+
settled = true;
|
|
158
|
+
if (this.wsConnection === socket) {
|
|
159
|
+
this.wsConnection = null;
|
|
160
|
+
}
|
|
161
|
+
reject(new ConnectionError(err.message));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Wait for the tunnel's public URL to become reachable.
|
|
168
|
+
*
|
|
169
|
+
* Polls the tunnel's public URL (not localhost) with HTTP GET requests
|
|
170
|
+
* until a 2xx response is received, or the timeout is exceeded. This
|
|
171
|
+
* validates the full tunnel path end-to-end.
|
|
172
|
+
*
|
|
173
|
+
* @param tunnel - The tunnel to check
|
|
174
|
+
* @param options - Wait options
|
|
175
|
+
* @throws TunnelTimeoutError if the tunnel is not ready within timeout
|
|
176
|
+
*/
|
|
177
|
+
async waitForReady(tunnel, options) {
|
|
178
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
179
|
+
const pollInterval = options?.pollInterval ?? 1e3;
|
|
180
|
+
const healthPath = options?.healthPath ?? "/";
|
|
181
|
+
const url = tunnel.url + healthPath;
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
while (Date.now() - startTime < timeout) {
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetch(url, {
|
|
186
|
+
method: "GET",
|
|
187
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(5e3, timeout - (Date.now() - startTime))))
|
|
188
|
+
});
|
|
189
|
+
if (response.ok) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
const remaining = timeout - (Date.now() - startTime);
|
|
195
|
+
if (remaining <= 0) break;
|
|
196
|
+
await this.sleep(Math.min(pollInterval, remaining));
|
|
197
|
+
}
|
|
198
|
+
throw new TunnelTimeoutError(timeout);
|
|
199
|
+
}
|
|
57
200
|
/**
|
|
58
201
|
* Wait for a tunnel to become available
|
|
59
202
|
*
|
|
@@ -113,13 +256,115 @@ var LivePortAgent = class {
|
|
|
113
256
|
/**
|
|
114
257
|
* Disconnect and clean up
|
|
115
258
|
*
|
|
116
|
-
* Cancels any pending waitForTunnel calls
|
|
259
|
+
* Cancels any pending waitForTunnel calls and closes any WebSocket
|
|
260
|
+
* connection created by connect().
|
|
117
261
|
*/
|
|
118
262
|
async disconnect() {
|
|
119
263
|
if (this.abortController) {
|
|
120
264
|
this.abortController.abort();
|
|
121
265
|
this.abortController = null;
|
|
122
266
|
}
|
|
267
|
+
if (this.wsConnection) {
|
|
268
|
+
const ws = this.wsConnection;
|
|
269
|
+
this.wsConnection = null;
|
|
270
|
+
if (ws.readyState === 1 || ws.readyState === 0) {
|
|
271
|
+
ws.close(1e3, "Agent disconnect");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Build WebSocket URL from server URL
|
|
277
|
+
*/
|
|
278
|
+
buildWebSocketUrl(serverUrl) {
|
|
279
|
+
const url = new URL(serverUrl);
|
|
280
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
281
|
+
url.pathname = "/connect";
|
|
282
|
+
return url.toString();
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Handle incoming WebSocket messages from the tunnel server
|
|
286
|
+
*/
|
|
287
|
+
handleWsMessage(message, localPort, socket) {
|
|
288
|
+
switch (message.type) {
|
|
289
|
+
case "http_request":
|
|
290
|
+
void this.handleHttpRequest(message, localPort, socket).catch(() => {
|
|
291
|
+
});
|
|
292
|
+
break;
|
|
293
|
+
case "heartbeat":
|
|
294
|
+
if (socket.readyState === 1) {
|
|
295
|
+
socket.send(JSON.stringify({
|
|
296
|
+
type: "heartbeat_ack",
|
|
297
|
+
timestamp: Date.now()
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Forward an HTTP request from the tunnel server to localhost
|
|
305
|
+
*/
|
|
306
|
+
async handleHttpRequest(message, localPort, socket) {
|
|
307
|
+
const payload = message.payload;
|
|
308
|
+
try {
|
|
309
|
+
const safePath = payload.path?.startsWith("/") ? payload.path : `/${payload.path || ""}`;
|
|
310
|
+
const url = `http://localhost:${localPort}${safePath}`;
|
|
311
|
+
const HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
312
|
+
"host",
|
|
313
|
+
"connection",
|
|
314
|
+
"transfer-encoding",
|
|
315
|
+
"te",
|
|
316
|
+
"trailer",
|
|
317
|
+
"upgrade",
|
|
318
|
+
"keep-alive",
|
|
319
|
+
"proxy-authenticate",
|
|
320
|
+
"proxy-authorization"
|
|
321
|
+
]);
|
|
322
|
+
const safeHeaders = Object.fromEntries(
|
|
323
|
+
Object.entries(payload.headers || {}).filter(([k]) => !HOP_BY_HOP.has(k.toLowerCase()))
|
|
324
|
+
);
|
|
325
|
+
const requestInit = {
|
|
326
|
+
method: payload.method,
|
|
327
|
+
headers: safeHeaders
|
|
328
|
+
};
|
|
329
|
+
if (payload.body) {
|
|
330
|
+
requestInit.body = Buffer.from(payload.body, "base64");
|
|
331
|
+
}
|
|
332
|
+
const response = await fetch(url, requestInit);
|
|
333
|
+
const responseBody = await response.arrayBuffer();
|
|
334
|
+
const responseHeaders = {};
|
|
335
|
+
response.headers.forEach((value, key) => {
|
|
336
|
+
responseHeaders[key] = value;
|
|
337
|
+
});
|
|
338
|
+
const responseMessage = {
|
|
339
|
+
type: "http_response",
|
|
340
|
+
id: message.id,
|
|
341
|
+
timestamp: Date.now(),
|
|
342
|
+
payload: {
|
|
343
|
+
status: response.status,
|
|
344
|
+
headers: responseHeaders,
|
|
345
|
+
body: responseBody.byteLength > 0 ? Buffer.from(responseBody).toString("base64") : void 0
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
if (socket.readyState === 1) {
|
|
349
|
+
socket.send(JSON.stringify(responseMessage));
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const errorResponse = {
|
|
353
|
+
type: "http_response",
|
|
354
|
+
id: message.id,
|
|
355
|
+
timestamp: Date.now(),
|
|
356
|
+
payload: {
|
|
357
|
+
status: 502,
|
|
358
|
+
headers: { "Content-Type": "text/plain" },
|
|
359
|
+
body: Buffer.from(
|
|
360
|
+
`Error connecting to local server: ${err.message}`
|
|
361
|
+
).toString("base64")
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
if (socket.readyState === 1) {
|
|
365
|
+
socket.send(JSON.stringify(errorResponse));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
123
368
|
}
|
|
124
369
|
/**
|
|
125
370
|
* Make an authenticated API request
|
|
@@ -139,12 +384,24 @@ var LivePortAgent = class {
|
|
|
139
384
|
* Parse tunnel response into AgentTunnel
|
|
140
385
|
*/
|
|
141
386
|
parseTunnel(data) {
|
|
387
|
+
const tunnelId = data.tunnelId ?? data.id;
|
|
388
|
+
const subdomain = data.subdomain;
|
|
389
|
+
const url = data.url;
|
|
390
|
+
const localPort = data.localPort;
|
|
391
|
+
if (!tunnelId || !subdomain || !url || localPort == null || typeof localPort !== "number") {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`parseTunnel: missing required fields in API response (tunnelId=${tunnelId}, subdomain=${subdomain}, url=${url}, localPort=${localPort})`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (!data.expiresAt) {
|
|
397
|
+
throw new Error("parseTunnel: missing expiresAt in API response");
|
|
398
|
+
}
|
|
142
399
|
return {
|
|
143
|
-
tunnelId
|
|
144
|
-
subdomain
|
|
145
|
-
url
|
|
146
|
-
localPort
|
|
147
|
-
createdAt: new Date(data.createdAt),
|
|
400
|
+
tunnelId,
|
|
401
|
+
subdomain,
|
|
402
|
+
url,
|
|
403
|
+
localPort,
|
|
404
|
+
createdAt: data.createdAt ? new Date(data.createdAt) : /* @__PURE__ */ new Date(),
|
|
148
405
|
expiresAt: new Date(data.expiresAt)
|
|
149
406
|
};
|
|
150
407
|
}
|
|
@@ -158,6 +415,7 @@ var LivePortAgent = class {
|
|
|
158
415
|
// Annotate the CommonJS export names for ESM import in node:
|
|
159
416
|
0 && (module.exports = {
|
|
160
417
|
ApiError,
|
|
418
|
+
ConnectionError,
|
|
161
419
|
LivePortAgent,
|
|
162
420
|
TunnelTimeoutError
|
|
163
421
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport type { Tunnel } from \"@liveport/shared\";\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://app.liveport.dev) */\n apiUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Wait for a tunnel to become available\n * const tunnel = await agent.waitForTunnel({ timeout: 30000 });\n * console.log(`Testing at: ${tunnel.url}`);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://app.liveport.dev\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls.\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n return {\n tunnelId: data.tunnelId as string || data.id as string,\n subdomain: data.subdomain as string,\n url: data.url as string,\n localPort: data.localPort as number,\n createdAt: new Date(data.createdAt as string),\n expiresAt: new Date(data.expiresAt as string),\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n// Re-export types\nexport type { Tunnel } from \"@liveport/shared\";\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport WebSocket from \"ws\";\n\n/**\n * Tunnel record as stored in the database.\n *\n * TODO: Remove this duplicate once @liveport/shared is published to npm\n * and agent-sdk can depend on it directly. Until then, keep in sync with\n * the Tunnel type in packages/shared/src/types/index.ts.\n */\nexport interface Tunnel {\n id: string;\n userId: string;\n bridgeKeyId?: string;\n subdomain: string;\n name?: string;\n localPort: number;\n publicUrl: string;\n region: string;\n connectedAt: Date;\n disconnectedAt?: Date;\n requestCount: number;\n bytesTransferred: number;\n}\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://liveport.dev) */\n apiUrl?: string;\n /** Tunnel server URL for connect() (default: https://tunnel.liveport.online) */\n tunnelUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface ConnectOptions {\n /** Tunnel server URL (overrides tunnelUrl from config) */\n serverUrl?: string;\n /** Connection timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\n/** @internal Extended connect options with injectable WebSocket class for testing */\ninterface InternalConnectOptions extends ConnectOptions {\n _WebSocketClass?: typeof WebSocket;\n}\n\nexport interface WaitForReadyOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n /** Health check path (default: \"/\") */\n healthPath?: string;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/** Error thrown when WebSocket connection fails */\nexport class ConnectionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ConnectionError\";\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for, connect to, and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Create a tunnel to local port 3000\n * const tunnel = await agent.connect(3000);\n * console.log(`Tunnel URL: ${tunnel.url}`);\n *\n * // Wait for the local server to be reachable through the tunnel\n * await agent.waitForReady(tunnel);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n private wsConnection: WebSocket | null = null;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://liveport.dev\",\n tunnelUrl: config.tunnelUrl || \"https://tunnel.liveport.online\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Connect to the tunnel server and create a tunnel for the given local port.\n *\n * Opens a WebSocket to the tunnel server, authenticates with the bridge key,\n * and waits for a tunnel assignment. Incoming HTTP requests from the tunnel\n * are forwarded to localhost:<port>.\n *\n * @param port - The local port to tunnel\n * @param options - Connection options\n * @returns The tunnel info once connected\n * @throws ConnectionError if the connection fails or times out\n */\n async connect(port: number, options?: ConnectOptions): Promise<AgentTunnel> {\n if (this.wsConnection) {\n throw new ConnectionError(\"Already connected — call disconnect() first\");\n }\n\n const serverUrl = options?.serverUrl || this.config.tunnelUrl;\n const timeout = options?.timeout ?? this.config.timeout;\n const WS = (options as InternalConnectOptions)?._WebSocketClass ?? WebSocket;\n\n return new Promise<AgentTunnel>((resolve, reject) => {\n const wsUrl = this.buildWebSocketUrl(serverUrl);\n\n const headers: Record<string, string> = {\n \"X-Bridge-Key\": this.config.key,\n \"X-Local-Port\": String(port),\n };\n\n const socket = new WS(wsUrl, {\n headers,\n perMessageDeflate: false,\n });\n\n this.wsConnection = socket;\n\n let settled = false;\n\n const connectTimeout = setTimeout(() => {\n if (!settled) {\n settled = true;\n socket.close();\n reject(new ConnectionError(\"Connection timeout\"));\n }\n }, timeout);\n\n socket.on(\"open\", () => {\n // Waiting for \"connected\" message from server\n });\n\n socket.on(\"message\", (data: Buffer | string) => {\n try {\n const message = JSON.parse(data.toString());\n this.handleWsMessage(message, port, socket);\n\n if (message.type === \"connected\" && !settled) {\n clearTimeout(connectTimeout);\n settled = true;\n const payload = message.payload;\n const tunnel: AgentTunnel = {\n tunnelId: payload.tunnelId,\n subdomain: payload.subdomain,\n url: payload.url,\n localPort: port,\n // Use server-provided createdAt if the server sends it; fall back to client time\n // (ConnectedPayload doesn't include createdAt today, but may in a future server version)\n createdAt: payload.createdAt ? new Date(payload.createdAt as string | number) : new Date(),\n expiresAt: new Date(payload.expiresAt),\n };\n resolve(tunnel);\n } else if (message.type === \"error\" && message.payload?.fatal && !settled) {\n clearTimeout(connectTimeout);\n settled = true;\n reject(new ConnectionError(message.payload.message));\n }\n } catch {\n // Non-JSON messages (e.g. binary frames, malformed data) are\n // silently dropped. The connect timeout will fire if the\n // \"connected\" message never arrives.\n }\n });\n\n socket.on(\"close\", () => {\n clearTimeout(connectTimeout);\n if (!settled) {\n settled = true;\n reject(new ConnectionError(\"Connection closed before tunnel was established\"));\n }\n // Only null wsConnection if it's still this socket (avoid clobbering\n // a new connection created after disconnect() + connect())\n if (this.wsConnection === socket) {\n this.wsConnection = null;\n }\n });\n\n socket.on(\"error\", (err: Error) => {\n clearTimeout(connectTimeout);\n if (!settled) {\n settled = true;\n // Clear wsConnection on error so a new connect() call can proceed\n if (this.wsConnection === socket) {\n this.wsConnection = null;\n }\n reject(new ConnectionError(err.message));\n }\n });\n });\n }\n\n /**\n * Wait for the tunnel's public URL to become reachable.\n *\n * Polls the tunnel's public URL (not localhost) with HTTP GET requests\n * until a 2xx response is received, or the timeout is exceeded. This\n * validates the full tunnel path end-to-end.\n *\n * @param tunnel - The tunnel to check\n * @param options - Wait options\n * @throws TunnelTimeoutError if the tunnel is not ready within timeout\n */\n async waitForReady(tunnel: AgentTunnel, options?: WaitForReadyOptions): Promise<void> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n const healthPath = options?.healthPath ?? \"/\";\n\n const url = tunnel.url + healthPath;\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await fetch(url, {\n method: \"GET\",\n signal: AbortSignal.timeout(Math.max(1, Math.min(5000, timeout - (Date.now() - startTime)))),\n });\n if (response.ok) {\n return;\n }\n } catch {\n // Network error or timeout, continue polling\n }\n\n const remaining = timeout - (Date.now() - startTime);\n if (remaining <= 0) break;\n await this.sleep(Math.min(pollInterval, remaining));\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls and closes any WebSocket\n * connection created by connect().\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n\n if (this.wsConnection) {\n const ws = this.wsConnection;\n this.wsConnection = null; // Clear reference first so connect() can be called after\n\n // Use numeric constants (OPEN=1, CONNECTING=0) to avoid coupling to\n // the imported WebSocket class, which may differ from the injected one\n if (ws.readyState === 1 || ws.readyState === 0) {\n ws.close(1000, \"Agent disconnect\");\n }\n }\n }\n\n /**\n * Build WebSocket URL from server URL\n */\n private buildWebSocketUrl(serverUrl: string): string {\n const url = new URL(serverUrl);\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n url.pathname = \"/connect\";\n return url.toString();\n }\n\n /**\n * Handle incoming WebSocket messages from the tunnel server\n */\n private handleWsMessage(\n message: { type: string; id?: string; payload?: Record<string, unknown> },\n localPort: number,\n socket: WebSocket\n ): void {\n switch (message.type) {\n case \"http_request\":\n void this.handleHttpRequest(message, localPort, socket).catch(() => {\n // Errors are handled inside handleHttpRequest (sends 502 response)\n });\n break;\n\n case \"heartbeat\":\n // Respond with heartbeat_ack\n if (socket.readyState === 1 /* OPEN */) {\n socket.send(JSON.stringify({\n type: \"heartbeat_ack\",\n timestamp: Date.now(),\n }));\n }\n break;\n\n // connected and error are handled in the connect() promise\n // Other message types can be extended in the future\n }\n }\n\n /**\n * Forward an HTTP request from the tunnel server to localhost\n */\n private async handleHttpRequest(\n message: { type: string; id?: string; payload?: Record<string, unknown> },\n localPort: number,\n socket: WebSocket\n ): Promise<void> {\n const payload = message.payload as {\n method: string;\n path: string;\n headers: Record<string, string>;\n body?: string;\n };\n\n try {\n // Validate path to prevent injection (e.g. path traversal or host override)\n const safePath = payload.path?.startsWith(\"/\") ? payload.path : `/${payload.path || \"\"}`;\n const url = `http://localhost:${localPort}${safePath}`;\n\n // Strip hop-by-hop headers that could cause request smuggling or\n // confuse the local server when forwarded from the tunnel\n const HOP_BY_HOP = new Set([\n \"host\", \"connection\", \"transfer-encoding\", \"te\", \"trailer\",\n \"upgrade\", \"keep-alive\", \"proxy-authenticate\", \"proxy-authorization\",\n ]);\n const safeHeaders = Object.fromEntries(\n Object.entries(payload.headers || {}).filter(([k]) => !HOP_BY_HOP.has(k.toLowerCase()))\n );\n\n const requestInit: RequestInit = {\n method: payload.method,\n headers: safeHeaders,\n };\n\n if (payload.body) {\n requestInit.body = Buffer.from(payload.body, \"base64\");\n }\n\n const response = await fetch(url, requestInit);\n const responseBody = await response.arrayBuffer();\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n const responseMessage = {\n type: \"http_response\",\n id: message.id,\n timestamp: Date.now(),\n payload: {\n status: response.status,\n headers: responseHeaders,\n body: responseBody.byteLength > 0\n ? Buffer.from(responseBody).toString(\"base64\")\n : undefined,\n },\n };\n\n if (socket.readyState === 1 /* OPEN */) {\n socket.send(JSON.stringify(responseMessage));\n }\n } catch (err) {\n const errorResponse = {\n type: \"http_response\",\n id: message.id,\n timestamp: Date.now(),\n payload: {\n status: 502,\n headers: { \"Content-Type\": \"text/plain\" },\n body: Buffer.from(\n `Error connecting to local server: ${(err as Error).message}`\n ).toString(\"base64\"),\n },\n };\n\n if (socket.readyState === 1 /* OPEN */) {\n socket.send(JSON.stringify(errorResponse));\n }\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n // Accept both tunnelId (new API shape) and id (legacy shape) for backward compatibility\n const tunnelId = (data.tunnelId ?? data.id) as string;\n const subdomain = data.subdomain as string;\n const url = data.url as string;\n const localPort = data.localPort as number;\n\n if (!tunnelId || !subdomain || !url || localPort == null || typeof localPort !== \"number\") {\n throw new Error(\n `parseTunnel: missing required fields in API response (tunnelId=${tunnelId}, subdomain=${subdomain}, url=${url}, localPort=${localPort})`\n );\n }\n\n if (!data.expiresAt) {\n throw new Error(\"parseTunnel: missing expiresAt in API response\");\n }\n\n return {\n tunnelId,\n subdomain,\n url,\n localPort,\n createdAt: data.createdAt ? new Date(data.createdAt as string | number) : new Date(),\n expiresAt: new Date(data.expiresAt as string | number),\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":["WebSocket"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import WebSocket from "ws";
|
|
2
3
|
var TunnelTimeoutError = class extends Error {
|
|
3
4
|
constructor(timeout) {
|
|
4
5
|
super(`Tunnel not available within ${timeout}ms timeout`);
|
|
@@ -15,19 +16,150 @@ var ApiError = class extends Error {
|
|
|
15
16
|
this.code = code;
|
|
16
17
|
}
|
|
17
18
|
};
|
|
19
|
+
var ConnectionError = class extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "ConnectionError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
18
25
|
var LivePortAgent = class {
|
|
19
26
|
config;
|
|
20
27
|
abortController = null;
|
|
28
|
+
wsConnection = null;
|
|
21
29
|
constructor(config) {
|
|
22
30
|
if (!config.key) {
|
|
23
31
|
throw new Error("Bridge key is required");
|
|
24
32
|
}
|
|
25
33
|
this.config = {
|
|
26
34
|
key: config.key,
|
|
27
|
-
apiUrl: config.apiUrl || "https://
|
|
35
|
+
apiUrl: config.apiUrl || "https://liveport.dev",
|
|
36
|
+
tunnelUrl: config.tunnelUrl || "https://tunnel.liveport.online",
|
|
28
37
|
timeout: config.timeout || 3e4
|
|
29
38
|
};
|
|
30
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Connect to the tunnel server and create a tunnel for the given local port.
|
|
42
|
+
*
|
|
43
|
+
* Opens a WebSocket to the tunnel server, authenticates with the bridge key,
|
|
44
|
+
* and waits for a tunnel assignment. Incoming HTTP requests from the tunnel
|
|
45
|
+
* are forwarded to localhost:<port>.
|
|
46
|
+
*
|
|
47
|
+
* @param port - The local port to tunnel
|
|
48
|
+
* @param options - Connection options
|
|
49
|
+
* @returns The tunnel info once connected
|
|
50
|
+
* @throws ConnectionError if the connection fails or times out
|
|
51
|
+
*/
|
|
52
|
+
async connect(port, options) {
|
|
53
|
+
if (this.wsConnection) {
|
|
54
|
+
throw new ConnectionError("Already connected \u2014 call disconnect() first");
|
|
55
|
+
}
|
|
56
|
+
const serverUrl = options?.serverUrl || this.config.tunnelUrl;
|
|
57
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
58
|
+
const WS = options?._WebSocketClass ?? WebSocket;
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const wsUrl = this.buildWebSocketUrl(serverUrl);
|
|
61
|
+
const headers = {
|
|
62
|
+
"X-Bridge-Key": this.config.key,
|
|
63
|
+
"X-Local-Port": String(port)
|
|
64
|
+
};
|
|
65
|
+
const socket = new WS(wsUrl, {
|
|
66
|
+
headers,
|
|
67
|
+
perMessageDeflate: false
|
|
68
|
+
});
|
|
69
|
+
this.wsConnection = socket;
|
|
70
|
+
let settled = false;
|
|
71
|
+
const connectTimeout = setTimeout(() => {
|
|
72
|
+
if (!settled) {
|
|
73
|
+
settled = true;
|
|
74
|
+
socket.close();
|
|
75
|
+
reject(new ConnectionError("Connection timeout"));
|
|
76
|
+
}
|
|
77
|
+
}, timeout);
|
|
78
|
+
socket.on("open", () => {
|
|
79
|
+
});
|
|
80
|
+
socket.on("message", (data) => {
|
|
81
|
+
try {
|
|
82
|
+
const message = JSON.parse(data.toString());
|
|
83
|
+
this.handleWsMessage(message, port, socket);
|
|
84
|
+
if (message.type === "connected" && !settled) {
|
|
85
|
+
clearTimeout(connectTimeout);
|
|
86
|
+
settled = true;
|
|
87
|
+
const payload = message.payload;
|
|
88
|
+
const tunnel = {
|
|
89
|
+
tunnelId: payload.tunnelId,
|
|
90
|
+
subdomain: payload.subdomain,
|
|
91
|
+
url: payload.url,
|
|
92
|
+
localPort: port,
|
|
93
|
+
// Use server-provided createdAt if the server sends it; fall back to client time
|
|
94
|
+
// (ConnectedPayload doesn't include createdAt today, but may in a future server version)
|
|
95
|
+
createdAt: payload.createdAt ? new Date(payload.createdAt) : /* @__PURE__ */ new Date(),
|
|
96
|
+
expiresAt: new Date(payload.expiresAt)
|
|
97
|
+
};
|
|
98
|
+
resolve(tunnel);
|
|
99
|
+
} else if (message.type === "error" && message.payload?.fatal && !settled) {
|
|
100
|
+
clearTimeout(connectTimeout);
|
|
101
|
+
settled = true;
|
|
102
|
+
reject(new ConnectionError(message.payload.message));
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
socket.on("close", () => {
|
|
108
|
+
clearTimeout(connectTimeout);
|
|
109
|
+
if (!settled) {
|
|
110
|
+
settled = true;
|
|
111
|
+
reject(new ConnectionError("Connection closed before tunnel was established"));
|
|
112
|
+
}
|
|
113
|
+
if (this.wsConnection === socket) {
|
|
114
|
+
this.wsConnection = null;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
socket.on("error", (err) => {
|
|
118
|
+
clearTimeout(connectTimeout);
|
|
119
|
+
if (!settled) {
|
|
120
|
+
settled = true;
|
|
121
|
+
if (this.wsConnection === socket) {
|
|
122
|
+
this.wsConnection = null;
|
|
123
|
+
}
|
|
124
|
+
reject(new ConnectionError(err.message));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Wait for the tunnel's public URL to become reachable.
|
|
131
|
+
*
|
|
132
|
+
* Polls the tunnel's public URL (not localhost) with HTTP GET requests
|
|
133
|
+
* until a 2xx response is received, or the timeout is exceeded. This
|
|
134
|
+
* validates the full tunnel path end-to-end.
|
|
135
|
+
*
|
|
136
|
+
* @param tunnel - The tunnel to check
|
|
137
|
+
* @param options - Wait options
|
|
138
|
+
* @throws TunnelTimeoutError if the tunnel is not ready within timeout
|
|
139
|
+
*/
|
|
140
|
+
async waitForReady(tunnel, options) {
|
|
141
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
142
|
+
const pollInterval = options?.pollInterval ?? 1e3;
|
|
143
|
+
const healthPath = options?.healthPath ?? "/";
|
|
144
|
+
const url = tunnel.url + healthPath;
|
|
145
|
+
const startTime = Date.now();
|
|
146
|
+
while (Date.now() - startTime < timeout) {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch(url, {
|
|
149
|
+
method: "GET",
|
|
150
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(5e3, timeout - (Date.now() - startTime))))
|
|
151
|
+
});
|
|
152
|
+
if (response.ok) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
const remaining = timeout - (Date.now() - startTime);
|
|
158
|
+
if (remaining <= 0) break;
|
|
159
|
+
await this.sleep(Math.min(pollInterval, remaining));
|
|
160
|
+
}
|
|
161
|
+
throw new TunnelTimeoutError(timeout);
|
|
162
|
+
}
|
|
31
163
|
/**
|
|
32
164
|
* Wait for a tunnel to become available
|
|
33
165
|
*
|
|
@@ -87,13 +219,115 @@ var LivePortAgent = class {
|
|
|
87
219
|
/**
|
|
88
220
|
* Disconnect and clean up
|
|
89
221
|
*
|
|
90
|
-
* Cancels any pending waitForTunnel calls
|
|
222
|
+
* Cancels any pending waitForTunnel calls and closes any WebSocket
|
|
223
|
+
* connection created by connect().
|
|
91
224
|
*/
|
|
92
225
|
async disconnect() {
|
|
93
226
|
if (this.abortController) {
|
|
94
227
|
this.abortController.abort();
|
|
95
228
|
this.abortController = null;
|
|
96
229
|
}
|
|
230
|
+
if (this.wsConnection) {
|
|
231
|
+
const ws = this.wsConnection;
|
|
232
|
+
this.wsConnection = null;
|
|
233
|
+
if (ws.readyState === 1 || ws.readyState === 0) {
|
|
234
|
+
ws.close(1e3, "Agent disconnect");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Build WebSocket URL from server URL
|
|
240
|
+
*/
|
|
241
|
+
buildWebSocketUrl(serverUrl) {
|
|
242
|
+
const url = new URL(serverUrl);
|
|
243
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
244
|
+
url.pathname = "/connect";
|
|
245
|
+
return url.toString();
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Handle incoming WebSocket messages from the tunnel server
|
|
249
|
+
*/
|
|
250
|
+
handleWsMessage(message, localPort, socket) {
|
|
251
|
+
switch (message.type) {
|
|
252
|
+
case "http_request":
|
|
253
|
+
void this.handleHttpRequest(message, localPort, socket).catch(() => {
|
|
254
|
+
});
|
|
255
|
+
break;
|
|
256
|
+
case "heartbeat":
|
|
257
|
+
if (socket.readyState === 1) {
|
|
258
|
+
socket.send(JSON.stringify({
|
|
259
|
+
type: "heartbeat_ack",
|
|
260
|
+
timestamp: Date.now()
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Forward an HTTP request from the tunnel server to localhost
|
|
268
|
+
*/
|
|
269
|
+
async handleHttpRequest(message, localPort, socket) {
|
|
270
|
+
const payload = message.payload;
|
|
271
|
+
try {
|
|
272
|
+
const safePath = payload.path?.startsWith("/") ? payload.path : `/${payload.path || ""}`;
|
|
273
|
+
const url = `http://localhost:${localPort}${safePath}`;
|
|
274
|
+
const HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
275
|
+
"host",
|
|
276
|
+
"connection",
|
|
277
|
+
"transfer-encoding",
|
|
278
|
+
"te",
|
|
279
|
+
"trailer",
|
|
280
|
+
"upgrade",
|
|
281
|
+
"keep-alive",
|
|
282
|
+
"proxy-authenticate",
|
|
283
|
+
"proxy-authorization"
|
|
284
|
+
]);
|
|
285
|
+
const safeHeaders = Object.fromEntries(
|
|
286
|
+
Object.entries(payload.headers || {}).filter(([k]) => !HOP_BY_HOP.has(k.toLowerCase()))
|
|
287
|
+
);
|
|
288
|
+
const requestInit = {
|
|
289
|
+
method: payload.method,
|
|
290
|
+
headers: safeHeaders
|
|
291
|
+
};
|
|
292
|
+
if (payload.body) {
|
|
293
|
+
requestInit.body = Buffer.from(payload.body, "base64");
|
|
294
|
+
}
|
|
295
|
+
const response = await fetch(url, requestInit);
|
|
296
|
+
const responseBody = await response.arrayBuffer();
|
|
297
|
+
const responseHeaders = {};
|
|
298
|
+
response.headers.forEach((value, key) => {
|
|
299
|
+
responseHeaders[key] = value;
|
|
300
|
+
});
|
|
301
|
+
const responseMessage = {
|
|
302
|
+
type: "http_response",
|
|
303
|
+
id: message.id,
|
|
304
|
+
timestamp: Date.now(),
|
|
305
|
+
payload: {
|
|
306
|
+
status: response.status,
|
|
307
|
+
headers: responseHeaders,
|
|
308
|
+
body: responseBody.byteLength > 0 ? Buffer.from(responseBody).toString("base64") : void 0
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
if (socket.readyState === 1) {
|
|
312
|
+
socket.send(JSON.stringify(responseMessage));
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const errorResponse = {
|
|
316
|
+
type: "http_response",
|
|
317
|
+
id: message.id,
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
payload: {
|
|
320
|
+
status: 502,
|
|
321
|
+
headers: { "Content-Type": "text/plain" },
|
|
322
|
+
body: Buffer.from(
|
|
323
|
+
`Error connecting to local server: ${err.message}`
|
|
324
|
+
).toString("base64")
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
if (socket.readyState === 1) {
|
|
328
|
+
socket.send(JSON.stringify(errorResponse));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
97
331
|
}
|
|
98
332
|
/**
|
|
99
333
|
* Make an authenticated API request
|
|
@@ -113,12 +347,24 @@ var LivePortAgent = class {
|
|
|
113
347
|
* Parse tunnel response into AgentTunnel
|
|
114
348
|
*/
|
|
115
349
|
parseTunnel(data) {
|
|
350
|
+
const tunnelId = data.tunnelId ?? data.id;
|
|
351
|
+
const subdomain = data.subdomain;
|
|
352
|
+
const url = data.url;
|
|
353
|
+
const localPort = data.localPort;
|
|
354
|
+
if (!tunnelId || !subdomain || !url || localPort == null || typeof localPort !== "number") {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`parseTunnel: missing required fields in API response (tunnelId=${tunnelId}, subdomain=${subdomain}, url=${url}, localPort=${localPort})`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (!data.expiresAt) {
|
|
360
|
+
throw new Error("parseTunnel: missing expiresAt in API response");
|
|
361
|
+
}
|
|
116
362
|
return {
|
|
117
|
-
tunnelId
|
|
118
|
-
subdomain
|
|
119
|
-
url
|
|
120
|
-
localPort
|
|
121
|
-
createdAt: new Date(data.createdAt),
|
|
363
|
+
tunnelId,
|
|
364
|
+
subdomain,
|
|
365
|
+
url,
|
|
366
|
+
localPort,
|
|
367
|
+
createdAt: data.createdAt ? new Date(data.createdAt) : /* @__PURE__ */ new Date(),
|
|
122
368
|
expiresAt: new Date(data.expiresAt)
|
|
123
369
|
};
|
|
124
370
|
}
|
|
@@ -131,6 +377,7 @@ var LivePortAgent = class {
|
|
|
131
377
|
};
|
|
132
378
|
export {
|
|
133
379
|
ApiError,
|
|
380
|
+
ConnectionError,
|
|
134
381
|
LivePortAgent,
|
|
135
382
|
TunnelTimeoutError
|
|
136
383
|
};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport type { Tunnel } from \"@liveport/shared\";\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://app.liveport.dev) */\n apiUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Wait for a tunnel to become available\n * const tunnel = await agent.waitForTunnel({ timeout: 30000 });\n * console.log(`Testing at: ${tunnel.url}`);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://app.liveport.dev\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls.\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n return {\n tunnelId: data.tunnelId as string || data.id as string,\n subdomain: data.subdomain as string,\n url: data.url as string,\n localPort: data.localPort as number,\n createdAt: new Date(data.createdAt as string),\n expiresAt: new Date(data.expiresAt as string),\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n// Re-export types\nexport type { Tunnel } from \"@liveport/shared\";\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport WebSocket from \"ws\";\n\n/**\n * Tunnel record as stored in the database.\n *\n * TODO: Remove this duplicate once @liveport/shared is published to npm\n * and agent-sdk can depend on it directly. Until then, keep in sync with\n * the Tunnel type in packages/shared/src/types/index.ts.\n */\nexport interface Tunnel {\n id: string;\n userId: string;\n bridgeKeyId?: string;\n subdomain: string;\n name?: string;\n localPort: number;\n publicUrl: string;\n region: string;\n connectedAt: Date;\n disconnectedAt?: Date;\n requestCount: number;\n bytesTransferred: number;\n}\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://liveport.dev) */\n apiUrl?: string;\n /** Tunnel server URL for connect() (default: https://tunnel.liveport.online) */\n tunnelUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface ConnectOptions {\n /** Tunnel server URL (overrides tunnelUrl from config) */\n serverUrl?: string;\n /** Connection timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\n/** @internal Extended connect options with injectable WebSocket class for testing */\ninterface InternalConnectOptions extends ConnectOptions {\n _WebSocketClass?: typeof WebSocket;\n}\n\nexport interface WaitForReadyOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n /** Health check path (default: \"/\") */\n healthPath?: string;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/** Error thrown when WebSocket connection fails */\nexport class ConnectionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ConnectionError\";\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for, connect to, and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Create a tunnel to local port 3000\n * const tunnel = await agent.connect(3000);\n * console.log(`Tunnel URL: ${tunnel.url}`);\n *\n * // Wait for the local server to be reachable through the tunnel\n * await agent.waitForReady(tunnel);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n private wsConnection: WebSocket | null = null;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://liveport.dev\",\n tunnelUrl: config.tunnelUrl || \"https://tunnel.liveport.online\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Connect to the tunnel server and create a tunnel for the given local port.\n *\n * Opens a WebSocket to the tunnel server, authenticates with the bridge key,\n * and waits for a tunnel assignment. Incoming HTTP requests from the tunnel\n * are forwarded to localhost:<port>.\n *\n * @param port - The local port to tunnel\n * @param options - Connection options\n * @returns The tunnel info once connected\n * @throws ConnectionError if the connection fails or times out\n */\n async connect(port: number, options?: ConnectOptions): Promise<AgentTunnel> {\n if (this.wsConnection) {\n throw new ConnectionError(\"Already connected — call disconnect() first\");\n }\n\n const serverUrl = options?.serverUrl || this.config.tunnelUrl;\n const timeout = options?.timeout ?? this.config.timeout;\n const WS = (options as InternalConnectOptions)?._WebSocketClass ?? WebSocket;\n\n return new Promise<AgentTunnel>((resolve, reject) => {\n const wsUrl = this.buildWebSocketUrl(serverUrl);\n\n const headers: Record<string, string> = {\n \"X-Bridge-Key\": this.config.key,\n \"X-Local-Port\": String(port),\n };\n\n const socket = new WS(wsUrl, {\n headers,\n perMessageDeflate: false,\n });\n\n this.wsConnection = socket;\n\n let settled = false;\n\n const connectTimeout = setTimeout(() => {\n if (!settled) {\n settled = true;\n socket.close();\n reject(new ConnectionError(\"Connection timeout\"));\n }\n }, timeout);\n\n socket.on(\"open\", () => {\n // Waiting for \"connected\" message from server\n });\n\n socket.on(\"message\", (data: Buffer | string) => {\n try {\n const message = JSON.parse(data.toString());\n this.handleWsMessage(message, port, socket);\n\n if (message.type === \"connected\" && !settled) {\n clearTimeout(connectTimeout);\n settled = true;\n const payload = message.payload;\n const tunnel: AgentTunnel = {\n tunnelId: payload.tunnelId,\n subdomain: payload.subdomain,\n url: payload.url,\n localPort: port,\n // Use server-provided createdAt if the server sends it; fall back to client time\n // (ConnectedPayload doesn't include createdAt today, but may in a future server version)\n createdAt: payload.createdAt ? new Date(payload.createdAt as string | number) : new Date(),\n expiresAt: new Date(payload.expiresAt),\n };\n resolve(tunnel);\n } else if (message.type === \"error\" && message.payload?.fatal && !settled) {\n clearTimeout(connectTimeout);\n settled = true;\n reject(new ConnectionError(message.payload.message));\n }\n } catch {\n // Non-JSON messages (e.g. binary frames, malformed data) are\n // silently dropped. The connect timeout will fire if the\n // \"connected\" message never arrives.\n }\n });\n\n socket.on(\"close\", () => {\n clearTimeout(connectTimeout);\n if (!settled) {\n settled = true;\n reject(new ConnectionError(\"Connection closed before tunnel was established\"));\n }\n // Only null wsConnection if it's still this socket (avoid clobbering\n // a new connection created after disconnect() + connect())\n if (this.wsConnection === socket) {\n this.wsConnection = null;\n }\n });\n\n socket.on(\"error\", (err: Error) => {\n clearTimeout(connectTimeout);\n if (!settled) {\n settled = true;\n // Clear wsConnection on error so a new connect() call can proceed\n if (this.wsConnection === socket) {\n this.wsConnection = null;\n }\n reject(new ConnectionError(err.message));\n }\n });\n });\n }\n\n /**\n * Wait for the tunnel's public URL to become reachable.\n *\n * Polls the tunnel's public URL (not localhost) with HTTP GET requests\n * until a 2xx response is received, or the timeout is exceeded. This\n * validates the full tunnel path end-to-end.\n *\n * @param tunnel - The tunnel to check\n * @param options - Wait options\n * @throws TunnelTimeoutError if the tunnel is not ready within timeout\n */\n async waitForReady(tunnel: AgentTunnel, options?: WaitForReadyOptions): Promise<void> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n const healthPath = options?.healthPath ?? \"/\";\n\n const url = tunnel.url + healthPath;\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await fetch(url, {\n method: \"GET\",\n signal: AbortSignal.timeout(Math.max(1, Math.min(5000, timeout - (Date.now() - startTime)))),\n });\n if (response.ok) {\n return;\n }\n } catch {\n // Network error or timeout, continue polling\n }\n\n const remaining = timeout - (Date.now() - startTime);\n if (remaining <= 0) break;\n await this.sleep(Math.min(pollInterval, remaining));\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls and closes any WebSocket\n * connection created by connect().\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n\n if (this.wsConnection) {\n const ws = this.wsConnection;\n this.wsConnection = null; // Clear reference first so connect() can be called after\n\n // Use numeric constants (OPEN=1, CONNECTING=0) to avoid coupling to\n // the imported WebSocket class, which may differ from the injected one\n if (ws.readyState === 1 || ws.readyState === 0) {\n ws.close(1000, \"Agent disconnect\");\n }\n }\n }\n\n /**\n * Build WebSocket URL from server URL\n */\n private buildWebSocketUrl(serverUrl: string): string {\n const url = new URL(serverUrl);\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n url.pathname = \"/connect\";\n return url.toString();\n }\n\n /**\n * Handle incoming WebSocket messages from the tunnel server\n */\n private handleWsMessage(\n message: { type: string; id?: string; payload?: Record<string, unknown> },\n localPort: number,\n socket: WebSocket\n ): void {\n switch (message.type) {\n case \"http_request\":\n void this.handleHttpRequest(message, localPort, socket).catch(() => {\n // Errors are handled inside handleHttpRequest (sends 502 response)\n });\n break;\n\n case \"heartbeat\":\n // Respond with heartbeat_ack\n if (socket.readyState === 1 /* OPEN */) {\n socket.send(JSON.stringify({\n type: \"heartbeat_ack\",\n timestamp: Date.now(),\n }));\n }\n break;\n\n // connected and error are handled in the connect() promise\n // Other message types can be extended in the future\n }\n }\n\n /**\n * Forward an HTTP request from the tunnel server to localhost\n */\n private async handleHttpRequest(\n message: { type: string; id?: string; payload?: Record<string, unknown> },\n localPort: number,\n socket: WebSocket\n ): Promise<void> {\n const payload = message.payload as {\n method: string;\n path: string;\n headers: Record<string, string>;\n body?: string;\n };\n\n try {\n // Validate path to prevent injection (e.g. path traversal or host override)\n const safePath = payload.path?.startsWith(\"/\") ? payload.path : `/${payload.path || \"\"}`;\n const url = `http://localhost:${localPort}${safePath}`;\n\n // Strip hop-by-hop headers that could cause request smuggling or\n // confuse the local server when forwarded from the tunnel\n const HOP_BY_HOP = new Set([\n \"host\", \"connection\", \"transfer-encoding\", \"te\", \"trailer\",\n \"upgrade\", \"keep-alive\", \"proxy-authenticate\", \"proxy-authorization\",\n ]);\n const safeHeaders = Object.fromEntries(\n Object.entries(payload.headers || {}).filter(([k]) => !HOP_BY_HOP.has(k.toLowerCase()))\n );\n\n const requestInit: RequestInit = {\n method: payload.method,\n headers: safeHeaders,\n };\n\n if (payload.body) {\n requestInit.body = Buffer.from(payload.body, \"base64\");\n }\n\n const response = await fetch(url, requestInit);\n const responseBody = await response.arrayBuffer();\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n const responseMessage = {\n type: \"http_response\",\n id: message.id,\n timestamp: Date.now(),\n payload: {\n status: response.status,\n headers: responseHeaders,\n body: responseBody.byteLength > 0\n ? Buffer.from(responseBody).toString(\"base64\")\n : undefined,\n },\n };\n\n if (socket.readyState === 1 /* OPEN */) {\n socket.send(JSON.stringify(responseMessage));\n }\n } catch (err) {\n const errorResponse = {\n type: \"http_response\",\n id: message.id,\n timestamp: Date.now(),\n payload: {\n status: 502,\n headers: { \"Content-Type\": \"text/plain\" },\n body: Buffer.from(\n `Error connecting to local server: ${(err as Error).message}`\n ).toString(\"base64\"),\n },\n };\n\n if (socket.readyState === 1 /* OPEN */) {\n socket.send(JSON.stringify(errorResponse));\n }\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n // Accept both tunnelId (new API shape) and id (legacy shape) for backward compatibility\n const tunnelId = (data.tunnelId ?? data.id) as string;\n const subdomain = data.subdomain as string;\n const url = data.url as string;\n const localPort = data.localPort as number;\n\n if (!tunnelId || !subdomain || !url || localPort == null || typeof localPort !== \"number\") {\n throw new Error(\n `parseTunnel: missing required fields in API response (tunnelId=${tunnelId}, subdomain=${subdomain}, url=${url}, localPort=${localPort})`\n );\n }\n\n if (!data.expiresAt) {\n throw new Error(\"parseTunnel: missing expiresAt in API response\");\n }\n\n return {\n tunnelId,\n subdomain,\n url,\n localPort,\n createdAt: data.createdAt ? new Date(data.createdAt as string | number) : new Date(),\n expiresAt: new Date(data.expiresAt as string | number),\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liveport/agent-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "TypeScript SDK for AI agents to access LivePort tunnels",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup",
|
|
21
21
|
"dev": "tsup --watch",
|
|
22
|
-
"lint": "
|
|
22
|
+
"lint": "echo 'Skipping lint - ESLint not configured' && exit 0",
|
|
23
23
|
"test": "vitest run --passWithNoTests",
|
|
24
24
|
"test:watch": "vitest",
|
|
25
25
|
"clean": "rm -rf dist",
|
|
@@ -55,11 +55,12 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^20.10.0",
|
|
58
|
+
"@types/ws": "^8.5.0",
|
|
58
59
|
"tsup": "^8.0.0",
|
|
59
60
|
"typescript": "^5.3.0",
|
|
60
61
|
"vitest": "^2.0.0"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
|
-
"
|
|
64
|
+
"ws": "^8.18.0"
|
|
64
65
|
}
|
|
65
66
|
}
|