@limrun/api 0.60.0 → 0.62.0
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/CHANGELOG.md +50 -0
- package/destination-tunnel-dialer.d.mts +8 -0
- package/destination-tunnel-dialer.d.mts.map +1 -1
- package/destination-tunnel-dialer.d.ts +8 -0
- package/destination-tunnel-dialer.d.ts.map +1 -1
- package/destination-tunnel-dialer.js +6 -5
- package/destination-tunnel-dialer.js.map +1 -1
- package/destination-tunnel-dialer.mjs +5 -5
- package/destination-tunnel-dialer.mjs.map +1 -1
- package/internal/destination-tunnel-wire-reader.d.mts +2 -0
- package/internal/destination-tunnel-wire-reader.d.mts.map +1 -1
- package/internal/destination-tunnel-wire-reader.d.ts +2 -0
- package/internal/destination-tunnel-wire-reader.d.ts.map +1 -1
- package/internal/destination-tunnel-wire-reader.js +17 -0
- package/internal/destination-tunnel-wire-reader.js.map +1 -1
- package/internal/destination-tunnel-wire-reader.mjs +15 -0
- package/internal/destination-tunnel-wire-reader.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/android-instances.d.mts +6 -6
- package/resources/android-instances.d.ts +6 -6
- package/resources/ios-instances.d.mts +6 -6
- package/resources/ios-instances.d.ts +6 -6
- package/src/destination-tunnel-dialer.ts +6 -5
- package/src/internal/destination-tunnel-wire-reader.ts +17 -0
- package/src/resources/android-instances.ts +6 -6
- package/src/resources/ios-instances.ts +6 -6
- package/src/tunnel-connector.ts +132 -42
- package/src/version.ts +1 -1
- package/tunnel-connector.d.mts +21 -10
- package/tunnel-connector.d.mts.map +1 -1
- package/tunnel-connector.d.ts +21 -10
- package/tunnel-connector.d.ts.map +1 -1
- package/tunnel-connector.js +96 -29
- package/tunnel-connector.js.map +1 -1
- package/tunnel-connector.mjs +98 -31
- package/tunnel-connector.mjs.map +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/package.json
CHANGED
|
@@ -55,9 +55,9 @@ export declare namespace AndroidInstance {
|
|
|
55
55
|
*/
|
|
56
56
|
hardTimeout?: string;
|
|
57
57
|
/**
|
|
58
|
-
* Names a persistent tunnel
|
|
59
|
-
* instance. Creation fails while
|
|
60
|
-
* becomes ready only after the tunnel attaches.
|
|
58
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
59
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
60
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
61
61
|
*/
|
|
62
62
|
tunnel?: string;
|
|
63
63
|
}
|
|
@@ -188,9 +188,9 @@ export declare namespace AndroidInstanceCreateParams {
|
|
|
188
188
|
region?: string;
|
|
189
189
|
sandbox?: Spec.Sandbox;
|
|
190
190
|
/**
|
|
191
|
-
* Names a persistent tunnel
|
|
192
|
-
* instance. Creation fails while
|
|
193
|
-
* becomes ready only after the tunnel attaches.
|
|
191
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
192
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
193
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
194
194
|
*/
|
|
195
195
|
tunnel?: string;
|
|
196
196
|
}
|
|
@@ -55,9 +55,9 @@ export declare namespace AndroidInstance {
|
|
|
55
55
|
*/
|
|
56
56
|
hardTimeout?: string;
|
|
57
57
|
/**
|
|
58
|
-
* Names a persistent tunnel
|
|
59
|
-
* instance. Creation fails while
|
|
60
|
-
* becomes ready only after the tunnel attaches.
|
|
58
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
59
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
60
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
61
61
|
*/
|
|
62
62
|
tunnel?: string;
|
|
63
63
|
}
|
|
@@ -188,9 +188,9 @@ export declare namespace AndroidInstanceCreateParams {
|
|
|
188
188
|
region?: string;
|
|
189
189
|
sandbox?: Spec.Sandbox;
|
|
190
190
|
/**
|
|
191
|
-
* Names a persistent tunnel
|
|
192
|
-
* instance. Creation fails while
|
|
193
|
-
* becomes ready only after the tunnel attaches.
|
|
191
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
192
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
193
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
194
194
|
*/
|
|
195
195
|
tunnel?: string;
|
|
196
196
|
}
|
|
@@ -55,9 +55,9 @@ export declare namespace IosInstance {
|
|
|
55
55
|
*/
|
|
56
56
|
hardTimeout?: string;
|
|
57
57
|
/**
|
|
58
|
-
* Names a persistent tunnel
|
|
59
|
-
* instance. Creation fails while
|
|
60
|
-
* becomes ready only after the tunnel attaches.
|
|
58
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
59
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
60
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
61
61
|
*/
|
|
62
62
|
tunnel?: string;
|
|
63
63
|
}
|
|
@@ -181,9 +181,9 @@ export declare namespace IosInstanceCreateParams {
|
|
|
181
181
|
*/
|
|
182
182
|
region?: string;
|
|
183
183
|
/**
|
|
184
|
-
* Names a persistent tunnel
|
|
185
|
-
* instance. Creation fails while
|
|
186
|
-
* becomes ready only after the tunnel attaches.
|
|
184
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
185
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
186
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
187
187
|
*/
|
|
188
188
|
tunnel?: string;
|
|
189
189
|
}
|
|
@@ -55,9 +55,9 @@ export declare namespace IosInstance {
|
|
|
55
55
|
*/
|
|
56
56
|
hardTimeout?: string;
|
|
57
57
|
/**
|
|
58
|
-
* Names a persistent tunnel
|
|
59
|
-
* instance. Creation fails while
|
|
60
|
-
* becomes ready only after the tunnel attaches.
|
|
58
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
59
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
60
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
61
61
|
*/
|
|
62
62
|
tunnel?: string;
|
|
63
63
|
}
|
|
@@ -181,9 +181,9 @@ export declare namespace IosInstanceCreateParams {
|
|
|
181
181
|
*/
|
|
182
182
|
region?: string;
|
|
183
183
|
/**
|
|
184
|
-
* Names a persistent tunnel
|
|
185
|
-
* instance. Creation fails while
|
|
186
|
-
* becomes ready only after the tunnel attaches.
|
|
184
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
185
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
186
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
187
187
|
*/
|
|
188
188
|
tunnel?: string;
|
|
189
189
|
}
|
|
@@ -88,6 +88,11 @@ export type DestinationTunnelStartOptions = Pick<
|
|
|
88
88
|
'selectors' | 'inspection' | 'onInspectionEvent' | 'onInspectionError' | 'window' | 'logLevel'
|
|
89
89
|
>;
|
|
90
90
|
|
|
91
|
+
/** The inspection every instance client starts with: on, without bodies. */
|
|
92
|
+
export function destinationTunnelInspection(inspection: DestinationTunnelStartOptions['inspection']) {
|
|
93
|
+
return { enabled: true, captureBodies: false, ...(inspection ?? {}) };
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
/**
|
|
92
97
|
* Applies the defaults both instance clients share, so the platforms cannot
|
|
93
98
|
* drift: inspection on, bodies off, and the client's log level.
|
|
@@ -98,11 +103,7 @@ export function destinationTunnelDialOptions(
|
|
|
98
103
|
): DestinationTcpTunnelOptions {
|
|
99
104
|
return {
|
|
100
105
|
selectors: options.selectors,
|
|
101
|
-
inspection:
|
|
102
|
-
enabled: true,
|
|
103
|
-
captureBodies: false,
|
|
104
|
-
...(options.inspection ?? {}),
|
|
105
|
-
},
|
|
106
|
+
inspection: destinationTunnelInspection(options.inspection),
|
|
106
107
|
...(options.onInspectionEvent ? { onInspectionEvent: options.onInspectionEvent } : {}),
|
|
107
108
|
...(options.onInspectionError ? { onInspectionError: options.onInspectionError } : {}),
|
|
108
109
|
...(options.window === undefined ? {} : { window: options.window }),
|
|
@@ -20,6 +20,15 @@ export function readArray(record: Record<string, unknown>, key: string): unknown
|
|
|
20
20
|
return value;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export function readStringArray(record: Record<string, unknown>, key: string): string[] {
|
|
24
|
+
return readArray(record, key).map((value) => {
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
throw new DestinationTunnelProtocolError(`${key} must hold strings`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
export function readString(record: Record<string, unknown>, key: string): string {
|
|
24
33
|
const value = record[key];
|
|
25
34
|
if (typeof value !== 'string') {
|
|
@@ -52,6 +61,14 @@ export function readInteger(record: Record<string, unknown>, key: string): numbe
|
|
|
52
61
|
return value as number;
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
export function readPositiveInteger(record: Record<string, unknown>, key: string): number {
|
|
65
|
+
const value = readInteger(record, key);
|
|
66
|
+
if (value < 1) {
|
|
67
|
+
throw new DestinationTunnelProtocolError(`${key} must be positive`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
export function readFiniteNumber(record: Record<string, unknown>, key: string): number {
|
|
56
73
|
const value = record[key];
|
|
57
74
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
@@ -90,9 +90,9 @@ export namespace AndroidInstance {
|
|
|
90
90
|
hardTimeout?: string;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Names a persistent tunnel
|
|
94
|
-
* instance. Creation fails while
|
|
95
|
-
* becomes ready only after the tunnel attaches.
|
|
93
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
94
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
95
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
96
96
|
*/
|
|
97
97
|
tunnel?: string;
|
|
98
98
|
}
|
|
@@ -250,9 +250,9 @@ export namespace AndroidInstanceCreateParams {
|
|
|
250
250
|
sandbox?: Spec.Sandbox;
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
|
-
* Names a persistent tunnel
|
|
254
|
-
* instance. Creation fails while
|
|
255
|
-
* becomes ready only after the tunnel attaches.
|
|
253
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
254
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
255
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
256
256
|
*/
|
|
257
257
|
tunnel?: string;
|
|
258
258
|
}
|
|
@@ -90,9 +90,9 @@ export namespace IosInstance {
|
|
|
90
90
|
hardTimeout?: string;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Names a persistent tunnel
|
|
94
|
-
* instance. Creation fails while
|
|
95
|
-
* becomes ready only after the tunnel attaches.
|
|
93
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
94
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
95
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
96
96
|
*/
|
|
97
97
|
tunnel?: string;
|
|
98
98
|
}
|
|
@@ -239,9 +239,9 @@ export namespace IosInstanceCreateParams {
|
|
|
239
239
|
region?: string;
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
|
-
* Names a persistent tunnel
|
|
243
|
-
* instance. Creation fails while
|
|
244
|
-
* becomes ready only after the tunnel attaches.
|
|
242
|
+
* Names a persistent tunnel, created in the console (Network) or the API, that
|
|
243
|
+
* serves this instance. Creation fails while no connector (`lim tunnel run`)
|
|
244
|
+
* holds it, and the instance becomes ready only after the tunnel attaches.
|
|
245
245
|
*/
|
|
246
246
|
tunnel?: string;
|
|
247
247
|
}
|
package/src/tunnel-connector.ts
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
} from './internal/destination-tunnel-management';
|
|
10
10
|
import {
|
|
11
11
|
DestinationTunnelProtocolError,
|
|
12
|
-
|
|
12
|
+
readPositiveInteger,
|
|
13
|
+
readStringArray,
|
|
13
14
|
readNonEmptyString,
|
|
14
15
|
readOptionalString,
|
|
15
16
|
readRecord,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import {
|
|
20
21
|
DestinationTunnelSessionError,
|
|
21
22
|
destinationTunnelDialOptions,
|
|
23
|
+
destinationTunnelInspection,
|
|
22
24
|
startDestinationTcpTunnel,
|
|
23
25
|
type DestinationTcpTunnel,
|
|
24
26
|
} from './destination-tunnel-dialer';
|
|
@@ -31,7 +33,7 @@ import {
|
|
|
31
33
|
import { upgradeStatus, type LogLevel } from './tunnel';
|
|
32
34
|
import { VERSION } from './version';
|
|
33
35
|
|
|
34
|
-
const CONTROL_PROTOCOL_VERSION =
|
|
36
|
+
const CONTROL_PROTOCOL_VERSION = 2;
|
|
35
37
|
const CONTROL_HANDSHAKE_TIMEOUT_MS = 15_000;
|
|
36
38
|
const CONTROL_PING_INTERVAL_MS = 15_000;
|
|
37
39
|
const CONTROL_DEAD_PEER_MS = 45_000;
|
|
@@ -47,15 +49,23 @@ const MAX_REJECTION_REASON_CHARS = 200;
|
|
|
47
49
|
/** Every tunnel WebSocket counts as instance activity, so retries must end. */
|
|
48
50
|
const ATTACH_GIVE_UP_MS = 5 * 60_000;
|
|
49
51
|
const TAKEN_OVER_MESSAGE = 'another connector for this tunnel took the instance over';
|
|
52
|
+
/** A token this close to its expiry is not dialed with; the hub refreshes it at half its life. */
|
|
53
|
+
const TOKEN_EXPIRY_MARGIN_MS = 5_000;
|
|
54
|
+
/** How long before its token expires the connector starts warning, and how often. */
|
|
55
|
+
const TOKEN_WARNING_WINDOW_MS = 14 * 24 * 60 * 60_000;
|
|
56
|
+
const TOKEN_WARNING_EVERY_MS = 24 * 60 * 60_000;
|
|
50
57
|
|
|
51
58
|
export interface TunnelConnectorOptions {
|
|
52
|
-
|
|
59
|
+
/** The tunnel's token; no other credential connects. */
|
|
60
|
+
token: string;
|
|
53
61
|
/** Limrun API base URL, such as https://api.limrun.com. */
|
|
54
62
|
baseURL: string;
|
|
55
63
|
organizationId: string;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The ID of a tunnel created in the console or the API. Its name, which
|
|
66
|
+
* instances set in `spec.tunnel`, and its selectors come from the platform.
|
|
67
|
+
*/
|
|
68
|
+
tunnelId: string;
|
|
59
69
|
/** Inspection for every instance tunnel. Defaults to enabled without bodies. */
|
|
60
70
|
inspection?: Partial<DestinationTunnelInspectionConfig>;
|
|
61
71
|
/** Take the name over from the connector that holds it now. */
|
|
@@ -70,8 +80,10 @@ export interface TunnelConnectorOptions {
|
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
export type TunnelConnectorEvent =
|
|
73
|
-
/** This connector holds the
|
|
74
|
-
| { type: 'active'; sessionId: string }
|
|
83
|
+
/** This connector holds the tunnel; emitted once per session and control connection. */
|
|
84
|
+
| { type: 'active'; sessionId: string; name: string }
|
|
85
|
+
/** The connector's tunnel token expires within two weeks; emitted once a day until it does. */
|
|
86
|
+
| { type: 'tokenExpiring'; expiresAt: string }
|
|
75
87
|
/** Another connector holds the name; emitted once per holder. */
|
|
76
88
|
| { type: 'standby'; holder: { hostname: string; since?: string } }
|
|
77
89
|
| { type: 'attached'; instanceId: string; tunnelId: string }
|
|
@@ -93,7 +105,6 @@ type ControlClientMessage =
|
|
|
93
105
|
| {
|
|
94
106
|
type: 'hello';
|
|
95
107
|
version: number;
|
|
96
|
-
selectors: DestinationTunnelSelectors;
|
|
97
108
|
replace: boolean;
|
|
98
109
|
sessionId?: string;
|
|
99
110
|
attached: string[];
|
|
@@ -104,9 +115,17 @@ type ControlClientMessage =
|
|
|
104
115
|
| { type: 'bye' };
|
|
105
116
|
|
|
106
117
|
type ControlServerMessage =
|
|
107
|
-
| { type: 'active'; sessionId: string; leaseSeconds: number }
|
|
118
|
+
| { type: 'active'; sessionId: string; leaseSeconds: number; name: string; tokenExpiresAt?: string }
|
|
108
119
|
| { type: 'standby'; holder: { hostname: string; since?: string } }
|
|
109
|
-
| {
|
|
120
|
+
| {
|
|
121
|
+
type: 'attach';
|
|
122
|
+
instanceId: string;
|
|
123
|
+
url: string;
|
|
124
|
+
selectors: string[];
|
|
125
|
+
token: string;
|
|
126
|
+
expiresInSeconds: number;
|
|
127
|
+
}
|
|
128
|
+
| { type: 'token'; instanceId: string; token: string; expiresInSeconds: number }
|
|
110
129
|
| { type: 'detach'; instanceId: string; reason: string }
|
|
111
130
|
| { type: 'notice'; code: string; message: string; instanceId?: string }
|
|
112
131
|
| { type: 'revoked'; reason: string };
|
|
@@ -114,7 +133,11 @@ type ControlServerMessage =
|
|
|
114
133
|
interface InstanceAttachment {
|
|
115
134
|
/** Instance base URL: the iOS API URL or the Android ADB WebSocket URL. */
|
|
116
135
|
url: string;
|
|
136
|
+
/** Opens this instance's tunnel and nothing else; the hub keeps it fresh. */
|
|
117
137
|
token: string;
|
|
138
|
+
tokenExpiresAt: number;
|
|
139
|
+
/** The tunnel's selectors when the instance first attached; later edits apply to later instances. */
|
|
140
|
+
selectors: DestinationTunnelSelectors;
|
|
118
141
|
tunnel?: DestinationTcpTunnel | undefined;
|
|
119
142
|
/** This connector's latest tunnel on the instance, live or dropped. */
|
|
120
143
|
lastTunnelId?: string;
|
|
@@ -129,23 +152,17 @@ interface InstanceAttachment {
|
|
|
129
152
|
}
|
|
130
153
|
|
|
131
154
|
/**
|
|
132
|
-
* Serve a persistent tunnel: hold
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
155
|
+
* Serve a persistent tunnel: hold it through one control WebSocket to the
|
|
156
|
+
* Limrun API and open a destination tunnel to every instance created with
|
|
157
|
+
* `spec.tunnel` set to its name, with a short-lived token the API hands out
|
|
158
|
+
* for each. Instance tunnels stay up while the control channel reconnects,
|
|
159
|
+
* so an API outage never becomes a tunnel outage; one that drops re-dials
|
|
160
|
+
* once the API sends a fresh token.
|
|
136
161
|
*/
|
|
137
162
|
export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
138
|
-
const selectors = validateDestinationTunnelSelectors(options.selectors);
|
|
139
|
-
const dialOptions = {
|
|
140
|
-
...destinationTunnelDialOptions(
|
|
141
|
-
{ selectors, ...(options.inspection ? { inspection: options.inspection } : {}) },
|
|
142
|
-
options.logLevel ?? 'warn',
|
|
143
|
-
),
|
|
144
|
-
name: options.name,
|
|
145
|
-
};
|
|
146
163
|
// Reject a bad inspection config now rather than on every attach.
|
|
147
|
-
normalizeDestinationTunnelInspection(
|
|
148
|
-
const controlURL = deriveTunnelConnectURL(options.baseURL, options.organizationId, options.
|
|
164
|
+
normalizeDestinationTunnelInspection(destinationTunnelInspection(options.inspection));
|
|
165
|
+
const controlURL = deriveTunnelConnectURL(options.baseURL, options.organizationId, options.tunnelId);
|
|
149
166
|
const client = {
|
|
150
167
|
hostname: options.hostname ?? os.hostname(),
|
|
151
168
|
version: options.clientVersion ?? VERSION,
|
|
@@ -162,6 +179,9 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
162
179
|
let replace = options.replace ?? false;
|
|
163
180
|
// When the last confirmed lease runs out; zero while on standby.
|
|
164
181
|
let leaseExpiresAt = 0;
|
|
182
|
+
// The tunnel's name, from the first active; instances set it in spec.tunnel.
|
|
183
|
+
let tunnelName: string | undefined;
|
|
184
|
+
let lastTokenWarning = 0;
|
|
165
185
|
let announcedSession: string | undefined;
|
|
166
186
|
let announcedHolder: string | undefined;
|
|
167
187
|
|
|
@@ -211,7 +231,24 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
211
231
|
const fail = (detail: string): void => {
|
|
212
232
|
if (!shutdown()) return;
|
|
213
233
|
ws?.terminate();
|
|
214
|
-
rejectClosed(new Error(`tunnel ${options.
|
|
234
|
+
rejectClosed(new Error(`tunnel ${tunnelName ?? options.tunnelId}: ${detail}`));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const dialOptions = (attachment: InstanceAttachment) => ({
|
|
238
|
+
...destinationTunnelDialOptions(
|
|
239
|
+
{ selectors: attachment.selectors, ...(options.inspection ? { inspection: options.inspection } : {}) },
|
|
240
|
+
options.logLevel ?? 'warn',
|
|
241
|
+
),
|
|
242
|
+
...(tunnelName === undefined ? {} : { name: tunnelName }),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const tokenUsable = (attachment: InstanceAttachment): boolean =>
|
|
246
|
+
Date.now() < attachment.tokenExpiresAt - TOKEN_EXPIRY_MARGIN_MS;
|
|
247
|
+
|
|
248
|
+
// Tells the platform and the caller that the connector gave up on an instance.
|
|
249
|
+
const reportGivenUp = (instanceId: string, code: string, message: string): void => {
|
|
250
|
+
send({ type: 'attachFailed', instanceId, code, message, terminal: true });
|
|
251
|
+
emit({ type: 'attachFailed', instanceId, code, message });
|
|
215
252
|
};
|
|
216
253
|
|
|
217
254
|
const giveUp = (
|
|
@@ -221,8 +258,7 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
221
258
|
message: string,
|
|
222
259
|
): void => {
|
|
223
260
|
forget(instanceId, attachment);
|
|
224
|
-
|
|
225
|
-
emit({ type: 'attachFailed', instanceId, code, message });
|
|
261
|
+
reportGivenUp(instanceId, code, message);
|
|
226
262
|
};
|
|
227
263
|
|
|
228
264
|
const scheduleAttachRetry = (instanceId: string, attachment: InstanceAttachment, reason: string): void => {
|
|
@@ -254,7 +290,8 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
254
290
|
try {
|
|
255
291
|
const active = (await getDestinationTunnelStatus(attachment.url, attachment.token)).active;
|
|
256
292
|
return (
|
|
257
|
-
active
|
|
293
|
+
active !== undefined &&
|
|
294
|
+
active.name === tunnelName &&
|
|
258
295
|
active.state !== 'stopping' &&
|
|
259
296
|
active.tunnelId !== attachment.lastTunnelId
|
|
260
297
|
);
|
|
@@ -288,7 +325,7 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
288
325
|
retryAfterFailure(instanceId, attachment, 'already_active', 'the previous tunnel is still stopping');
|
|
289
326
|
return;
|
|
290
327
|
}
|
|
291
|
-
if (active.name !==
|
|
328
|
+
if (active.name !== tunnelName) {
|
|
292
329
|
giveUp(instanceId, attachment, 'instance_busy', 'the instance already has another tunnel');
|
|
293
330
|
return;
|
|
294
331
|
}
|
|
@@ -322,6 +359,12 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
322
359
|
retryAfterFailure(instanceId, attachment, 'not_holder', 'this connector does not hold the tunnel');
|
|
323
360
|
return;
|
|
324
361
|
}
|
|
362
|
+
// A token past its life is refused; the API sends a fresh one at half
|
|
363
|
+
// life, so this only waits while the control channel is down.
|
|
364
|
+
if (!tokenUsable(attachment)) {
|
|
365
|
+
retryAfterFailure(instanceId, attachment, 'token_expired', 'waiting for a fresh attach token');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
325
368
|
let tunnel: DestinationTcpTunnel;
|
|
326
369
|
try {
|
|
327
370
|
if (attachment.lastTunnelId !== undefined && (await takenOver(attachment))) {
|
|
@@ -334,7 +377,7 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
334
377
|
tunnel = await startDestinationTcpTunnel(
|
|
335
378
|
deriveDestinationTunnelURL(attachment.url),
|
|
336
379
|
attachment.token,
|
|
337
|
-
dialOptions,
|
|
380
|
+
dialOptions(attachment),
|
|
338
381
|
);
|
|
339
382
|
} catch (error) {
|
|
340
383
|
if (attachment.done) return;
|
|
@@ -387,17 +430,28 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
387
430
|
// close() already let every instance go; a late attach must not dial.
|
|
388
431
|
if (stopped) return;
|
|
389
432
|
switch (message.type) {
|
|
390
|
-
case 'active':
|
|
433
|
+
case 'active': {
|
|
391
434
|
replace = false;
|
|
392
435
|
leaseExpiresAt = Date.now() + message.leaseSeconds * 1000;
|
|
393
436
|
controlBackoffMs = CONTROL_INITIAL_BACKOFF_MS;
|
|
394
437
|
sessionId = message.sessionId;
|
|
438
|
+
tunnelName = message.name;
|
|
395
439
|
announcedHolder = undefined;
|
|
396
440
|
if (announcedSession !== message.sessionId) {
|
|
397
441
|
announcedSession = message.sessionId;
|
|
398
|
-
emit({ type: 'active', sessionId: message.sessionId });
|
|
442
|
+
emit({ type: 'active', sessionId: message.sessionId, name: message.name });
|
|
443
|
+
}
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
if (
|
|
446
|
+
message.tokenExpiresAt !== undefined &&
|
|
447
|
+
Date.parse(message.tokenExpiresAt) - now < TOKEN_WARNING_WINDOW_MS &&
|
|
448
|
+
now - lastTokenWarning >= TOKEN_WARNING_EVERY_MS
|
|
449
|
+
) {
|
|
450
|
+
lastTokenWarning = now;
|
|
451
|
+
emit({ type: 'tokenExpiring', expiresAt: message.tokenExpiresAt });
|
|
399
452
|
}
|
|
400
453
|
return;
|
|
454
|
+
}
|
|
401
455
|
case 'standby': {
|
|
402
456
|
leaseExpiresAt = 0;
|
|
403
457
|
controlBackoffMs = CONTROL_INITIAL_BACKOFF_MS;
|
|
@@ -410,20 +464,46 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
410
464
|
}
|
|
411
465
|
return;
|
|
412
466
|
}
|
|
467
|
+
case 'token': {
|
|
468
|
+
// A fresh token at half its life, or after a resume. An instance this
|
|
469
|
+
// connector gave up on stays given up.
|
|
470
|
+
const attachment = instances.get(message.instanceId);
|
|
471
|
+
if (attachment) {
|
|
472
|
+
attachment.token = message.token;
|
|
473
|
+
attachment.tokenExpiresAt = Date.now() + message.expiresInSeconds * 1000;
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
413
477
|
case 'attach': {
|
|
478
|
+
// Expiry is relative, so this host's clock never matters.
|
|
479
|
+
const tokenExpiresAt = Date.now() + message.expiresInSeconds * 1000;
|
|
414
480
|
const existing = instances.get(message.instanceId);
|
|
415
481
|
if (existing) {
|
|
416
|
-
//
|
|
482
|
+
// A new holder session re-attaches every instance, including ones
|
|
483
|
+
// this connector still serves; a live tunnel only needs confirming.
|
|
484
|
+
// The instance keeps the selectors it first got.
|
|
417
485
|
existing.url = message.url;
|
|
418
486
|
existing.token = message.token;
|
|
487
|
+
existing.tokenExpiresAt = tokenExpiresAt;
|
|
419
488
|
if (existing.tunnel) {
|
|
420
489
|
send({ type: 'attached', instanceId: message.instanceId, tunnelId: existing.tunnel.tunnelId });
|
|
421
490
|
}
|
|
422
491
|
return;
|
|
423
492
|
}
|
|
493
|
+
// The instance itself refuses what its platform cannot route, such
|
|
494
|
+
// as Android routes below port 1024.
|
|
495
|
+
let selectors: DestinationTunnelSelectors;
|
|
496
|
+
try {
|
|
497
|
+
selectors = validateDestinationTunnelSelectors(message.selectors);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
reportGivenUp(message.instanceId, 'invalid_selectors', errorMessage(error));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
424
502
|
const attachment: InstanceAttachment = {
|
|
425
503
|
url: message.url,
|
|
426
504
|
token: message.token,
|
|
505
|
+
tokenExpiresAt,
|
|
506
|
+
selectors,
|
|
427
507
|
mayReclaim: true,
|
|
428
508
|
failingSince: Date.now(),
|
|
429
509
|
backoffMs: ATTACH_INITIAL_BACKOFF_MS,
|
|
@@ -485,7 +565,7 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
485
565
|
if (stopped) return;
|
|
486
566
|
const proxyAgent = nodeProxyTransport.getWebSocketAgent(controlURL);
|
|
487
567
|
const socket = new WebSocket(controlURL, {
|
|
488
|
-
headers: { Authorization: `Bearer ${options.
|
|
568
|
+
headers: { Authorization: `Bearer ${options.token}` },
|
|
489
569
|
...(proxyAgent ? { agent: proxyAgent } : {}),
|
|
490
570
|
perMessageDeflate: false,
|
|
491
571
|
// A stalled upgrade, such as after a laptop wakes, would otherwise
|
|
@@ -518,7 +598,6 @@ export function runTunnel(options: TunnelConnectorOptions): TunnelConnector {
|
|
|
518
598
|
send({
|
|
519
599
|
type: 'hello',
|
|
520
600
|
version: CONTROL_PROTOCOL_VERSION,
|
|
521
|
-
selectors,
|
|
522
601
|
replace,
|
|
523
602
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
524
603
|
attached: Array.from(instances)
|
|
@@ -614,11 +693,13 @@ function decodeControlServerMessage(value: unknown): ControlServerMessage | unde
|
|
|
614
693
|
const type = readString(message, 'type');
|
|
615
694
|
switch (type) {
|
|
616
695
|
case 'active': {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
696
|
+
return {
|
|
697
|
+
type,
|
|
698
|
+
sessionId: readNonEmptyString(message, 'sessionId'),
|
|
699
|
+
leaseSeconds: readPositiveInteger(message, 'leaseSeconds'),
|
|
700
|
+
name: readNonEmptyString(message, 'name'),
|
|
701
|
+
...readOptionalString(message, 'tokenExpiresAt'),
|
|
702
|
+
};
|
|
622
703
|
}
|
|
623
704
|
case 'standby': {
|
|
624
705
|
const holder = readRecord(message['holder'], 'holder');
|
|
@@ -632,7 +713,16 @@ function decodeControlServerMessage(value: unknown): ControlServerMessage | unde
|
|
|
632
713
|
type,
|
|
633
714
|
instanceId: readNonEmptyString(message, 'instanceId'),
|
|
634
715
|
url: readNonEmptyString(message, 'url'),
|
|
716
|
+
selectors: readStringArray(message, 'selectors'),
|
|
717
|
+
token: readNonEmptyString(message, 'token'),
|
|
718
|
+
expiresInSeconds: readPositiveInteger(message, 'expiresInSeconds'),
|
|
719
|
+
};
|
|
720
|
+
case 'token':
|
|
721
|
+
return {
|
|
722
|
+
type,
|
|
723
|
+
instanceId: readNonEmptyString(message, 'instanceId'),
|
|
635
724
|
token: readNonEmptyString(message, 'token'),
|
|
725
|
+
expiresInSeconds: readPositiveInteger(message, 'expiresInSeconds'),
|
|
636
726
|
};
|
|
637
727
|
case 'detach':
|
|
638
728
|
return {
|
|
@@ -662,7 +752,7 @@ function revokedMessage(reason: string): string {
|
|
|
662
752
|
case 'deleted':
|
|
663
753
|
return 'the tunnel was deleted';
|
|
664
754
|
case 'credential':
|
|
665
|
-
return '
|
|
755
|
+
return 'its tunnel token was rotated, revoked or has expired';
|
|
666
756
|
default:
|
|
667
757
|
return `the tunnel was revoked (${reason})`;
|
|
668
758
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.62.0'; // x-release-please-version
|
package/tunnel-connector.d.mts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { type DestinationTunnelInspectionConfig
|
|
1
|
+
import { type DestinationTunnelInspectionConfig } from "./destination-tunnel.mjs";
|
|
2
2
|
import { type LogLevel } from "./tunnel.mjs";
|
|
3
3
|
export interface TunnelConnectorOptions {
|
|
4
|
-
|
|
4
|
+
/** The tunnel's token; no other credential connects. */
|
|
5
|
+
token: string;
|
|
5
6
|
/** Limrun API base URL, such as https://api.limrun.com. */
|
|
6
7
|
baseURL: string;
|
|
7
8
|
organizationId: string;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The ID of a tunnel created in the console or the API. Its name, which
|
|
11
|
+
* instances set in `spec.tunnel`, and its selectors come from the platform.
|
|
12
|
+
*/
|
|
13
|
+
tunnelId: string;
|
|
11
14
|
/** Inspection for every instance tunnel. Defaults to enabled without bodies. */
|
|
12
15
|
inspection?: Partial<DestinationTunnelInspectionConfig>;
|
|
13
16
|
/** Take the name over from the connector that holds it now. */
|
|
@@ -21,10 +24,16 @@ export interface TunnelConnectorOptions {
|
|
|
21
24
|
onEvent?: (event: TunnelConnectorEvent) => void;
|
|
22
25
|
}
|
|
23
26
|
export type TunnelConnectorEvent =
|
|
24
|
-
/** This connector holds the
|
|
27
|
+
/** This connector holds the tunnel; emitted once per session and control connection. */
|
|
25
28
|
{
|
|
26
29
|
type: 'active';
|
|
27
30
|
sessionId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
/** The connector's tunnel token expires within two weeks; emitted once a day until it does. */
|
|
34
|
+
| {
|
|
35
|
+
type: 'tokenExpiring';
|
|
36
|
+
expiresAt: string;
|
|
28
37
|
}
|
|
29
38
|
/** Another connector holds the name; emitted once per holder. */
|
|
30
39
|
| {
|
|
@@ -68,10 +77,12 @@ export interface TunnelConnector {
|
|
|
68
77
|
close: () => Promise<void>;
|
|
69
78
|
}
|
|
70
79
|
/**
|
|
71
|
-
* Serve a persistent tunnel: hold
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
80
|
+
* Serve a persistent tunnel: hold it through one control WebSocket to the
|
|
81
|
+
* Limrun API and open a destination tunnel to every instance created with
|
|
82
|
+
* `spec.tunnel` set to its name, with a short-lived token the API hands out
|
|
83
|
+
* for each. Instance tunnels stay up while the control channel reconnects,
|
|
84
|
+
* so an API outage never becomes a tunnel outage; one that drops re-dials
|
|
85
|
+
* once the API sends a fresh token.
|
|
75
86
|
*/
|
|
76
87
|
export declare function runTunnel(options: TunnelConnectorOptions): TunnelConnector;
|
|
77
88
|
//# sourceMappingURL=tunnel-connector.d.mts.map
|