@cimplify/cli 0.11.1 → 0.12.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/dist/{chunk-3NDTR4GA.mjs → chunk-EL34CORK.mjs} +1 -1
- package/dist/{chunk-OMAF2H6T.mjs → chunk-W64GWXPF.mjs} +1 -1
- package/dist/dev-3TPDHU67.mjs +1724 -0
- package/dist/dispatcher.mjs +7 -7
- package/dist/{doctor-LAKJKBFU.mjs → doctor-JQYXARYQ.mjs} +2 -2
- package/dist/{explain-EZ7DC4KE.mjs → explain-2I4MWMPY.mjs} +1 -1
- package/dist/{introspect-HVT3TMAM.mjs → introspect-TB2QFQMF.mjs} +2 -2
- package/dist/{preview-config-CHDYYWUY.mjs → preview-config-344CUZLO.mjs} +8 -4
- package/dist/{update-74UVN3R7.mjs → update-T5O6YCIC.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-B4YUYQ4I.mjs +0 -782
|
@@ -0,0 +1,1724 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseEnvFile, formatEnvFile } from './chunk-DBZ3UOQ2.mjs';
|
|
3
|
+
import { ENV_SCOPE, PUBLIC_ENV_PREFIX } from './chunk-MXYUAJEW.mjs';
|
|
4
|
+
import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
|
|
5
|
+
import { ApiClient } from './chunk-F4S5KETY.mjs';
|
|
6
|
+
import { readAuth, readProjectLink } from './chunk-UBAI443T.mjs';
|
|
7
|
+
import { info, CliError, CLI_ERROR_CODE, dim } from './chunk-E2T2SBP5.mjs';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { promises } from 'fs';
|
|
10
|
+
import path2 from 'path';
|
|
11
|
+
import http from 'http';
|
|
12
|
+
|
|
13
|
+
// src/tunnel/v2/codec.ts
|
|
14
|
+
var HEADER_LEN = 9;
|
|
15
|
+
var SUBPROTOCOL_V2 = "cimplify-tunnel.v2";
|
|
16
|
+
var DEFAULT_MAX_FRAME_PAYLOAD = 65536;
|
|
17
|
+
var DEFAULT_INITIAL_STREAM_WINDOW = 262144;
|
|
18
|
+
var DEFAULT_CONNECTION_WINDOW = 4194304;
|
|
19
|
+
var DEFAULT_MAX_CONCURRENT_STREAMS = 256;
|
|
20
|
+
var MAX_HEAD_BYTES = 65536;
|
|
21
|
+
var FRAME_TYPE_V2 = {
|
|
22
|
+
HELLO: 1,
|
|
23
|
+
OPEN: 2,
|
|
24
|
+
ACCEPT: 3,
|
|
25
|
+
DATA: 4,
|
|
26
|
+
END: 5,
|
|
27
|
+
CLOSE: 6,
|
|
28
|
+
RST: 7,
|
|
29
|
+
WINDOW_UPDATE: 8,
|
|
30
|
+
PING: 9,
|
|
31
|
+
PONG: 10,
|
|
32
|
+
GOAWAY: 11
|
|
33
|
+
};
|
|
34
|
+
var ERROR_CODE = {
|
|
35
|
+
PROTOCOL_ERROR: 1,
|
|
36
|
+
FLOW_CONTROL_ERROR: 3,
|
|
37
|
+
STREAM_CLOSED: 4,
|
|
38
|
+
REFUSED: 5,
|
|
39
|
+
UNSUPPORTED: 6};
|
|
40
|
+
var WS_DATA_PREFIX = {
|
|
41
|
+
TEXT: 0,
|
|
42
|
+
BINARY: 1,
|
|
43
|
+
TEXT_CONT: 2,
|
|
44
|
+
BINARY_CONT: 3
|
|
45
|
+
};
|
|
46
|
+
var CodecV2Error = class extends Error {
|
|
47
|
+
};
|
|
48
|
+
function encodeFrameV2(frame) {
|
|
49
|
+
const bytes = new Uint8Array(HEADER_LEN + frame.payload.length);
|
|
50
|
+
const view = new DataView(bytes.buffer);
|
|
51
|
+
view.setUint32(0, frame.streamId);
|
|
52
|
+
view.setUint8(4, frame.frameType);
|
|
53
|
+
view.setUint32(5, frame.payload.length);
|
|
54
|
+
bytes.set(frame.payload, HEADER_LEN);
|
|
55
|
+
return bytes;
|
|
56
|
+
}
|
|
57
|
+
function decodeFrameV2(bytes, maxPayload = DEFAULT_MAX_FRAME_PAYLOAD) {
|
|
58
|
+
if (bytes.length < HEADER_LEN) {
|
|
59
|
+
throw new CodecV2Error(`frame truncated: ${bytes.length} bytes`);
|
|
60
|
+
}
|
|
61
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
62
|
+
const streamId = view.getUint32(0);
|
|
63
|
+
const frameType = view.getUint8(4);
|
|
64
|
+
const declared = view.getUint32(5);
|
|
65
|
+
if (declared > maxPayload) {
|
|
66
|
+
throw new CodecV2Error(`payload length ${declared} exceeds negotiated max ${maxPayload}`);
|
|
67
|
+
}
|
|
68
|
+
const actual = bytes.length - HEADER_LEN;
|
|
69
|
+
if (declared !== actual) {
|
|
70
|
+
throw new CodecV2Error(`payload length field ${declared} does not match remaining ${actual} bytes`);
|
|
71
|
+
}
|
|
72
|
+
return { streamId, frameType, payload: bytes.subarray(HEADER_LEN) };
|
|
73
|
+
}
|
|
74
|
+
function payloadU32(frame) {
|
|
75
|
+
if (frame.payload.length !== 4) return null;
|
|
76
|
+
return new DataView(frame.payload.buffer, frame.payload.byteOffset, 4).getUint32(0);
|
|
77
|
+
}
|
|
78
|
+
function rstFrame(streamId, code) {
|
|
79
|
+
const payload = new Uint8Array(4);
|
|
80
|
+
new DataView(payload.buffer).setUint32(0, code);
|
|
81
|
+
return { streamId, frameType: FRAME_TYPE_V2.RST, payload };
|
|
82
|
+
}
|
|
83
|
+
function windowUpdateFrame(streamId, credit) {
|
|
84
|
+
const payload = new Uint8Array(4);
|
|
85
|
+
new DataView(payload.buffer).setUint32(0, credit);
|
|
86
|
+
return { streamId, frameType: FRAME_TYPE_V2.WINDOW_UPDATE, payload };
|
|
87
|
+
}
|
|
88
|
+
function defaultHello(build) {
|
|
89
|
+
return {
|
|
90
|
+
version: 2,
|
|
91
|
+
initial_stream_window: DEFAULT_INITIAL_STREAM_WINDOW,
|
|
92
|
+
connection_window: DEFAULT_CONNECTION_WINDOW,
|
|
93
|
+
max_frame_payload: DEFAULT_MAX_FRAME_PAYLOAD,
|
|
94
|
+
max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
95
|
+
capabilities: ["http", "ws", "control"],
|
|
96
|
+
...{}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseHello(payload) {
|
|
100
|
+
const raw = JSON.parse(new TextDecoder().decode(payload));
|
|
101
|
+
if (typeof raw.version !== "number") {
|
|
102
|
+
throw new CodecV2Error("HELLO missing version");
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
version: raw.version,
|
|
106
|
+
initial_stream_window: raw.initial_stream_window ?? DEFAULT_INITIAL_STREAM_WINDOW,
|
|
107
|
+
connection_window: raw.connection_window ?? DEFAULT_CONNECTION_WINDOW,
|
|
108
|
+
max_frame_payload: raw.max_frame_payload ?? DEFAULT_MAX_FRAME_PAYLOAD,
|
|
109
|
+
max_concurrent_streams: raw.max_concurrent_streams ?? DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
110
|
+
capabilities: raw.capabilities ?? [],
|
|
111
|
+
...raw.build ? { build: raw.build } : {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/tunnel/v2/ws-framing.ts
|
|
116
|
+
var textEncoder = new TextEncoder();
|
|
117
|
+
var textDecoder = new TextDecoder();
|
|
118
|
+
function encodeWsMessage(message, maxPayload) {
|
|
119
|
+
const body = message.type === "text" ? textEncoder.encode(message.data) : message.data;
|
|
120
|
+
const contPrefix = message.type === "text" ? WS_DATA_PREFIX.TEXT_CONT : WS_DATA_PREFIX.BINARY_CONT;
|
|
121
|
+
const finalPrefix = message.type === "text" ? WS_DATA_PREFIX.TEXT : WS_DATA_PREFIX.BINARY;
|
|
122
|
+
const chunkSize = Math.max(1, maxPayload - 1);
|
|
123
|
+
const frames = [];
|
|
124
|
+
if (body.length === 0) {
|
|
125
|
+
return [Uint8Array.of(finalPrefix)];
|
|
126
|
+
}
|
|
127
|
+
for (let offset = 0; offset < body.length; offset += chunkSize) {
|
|
128
|
+
const chunk = body.subarray(offset, offset + chunkSize);
|
|
129
|
+
const isLast = offset + chunkSize >= body.length;
|
|
130
|
+
const framed = new Uint8Array(chunk.length + 1);
|
|
131
|
+
framed[0] = isLast ? finalPrefix : contPrefix;
|
|
132
|
+
framed.set(chunk, 1);
|
|
133
|
+
frames.push(framed);
|
|
134
|
+
}
|
|
135
|
+
return frames;
|
|
136
|
+
}
|
|
137
|
+
var WsReassembler = class {
|
|
138
|
+
buffer = [];
|
|
139
|
+
bufferedType = null;
|
|
140
|
+
/** Feed one DATA payload; returns a completed message, or null mid-fragment. */
|
|
141
|
+
push(payload) {
|
|
142
|
+
if (payload.length === 0) return null;
|
|
143
|
+
const prefix = payload[0];
|
|
144
|
+
const chunk = payload.subarray(1);
|
|
145
|
+
switch (prefix) {
|
|
146
|
+
case WS_DATA_PREFIX.TEXT_CONT:
|
|
147
|
+
this.startOrAppend("text", chunk);
|
|
148
|
+
return null;
|
|
149
|
+
case WS_DATA_PREFIX.BINARY_CONT:
|
|
150
|
+
this.startOrAppend("binary", chunk);
|
|
151
|
+
return null;
|
|
152
|
+
case WS_DATA_PREFIX.TEXT:
|
|
153
|
+
return this.finish("text", chunk);
|
|
154
|
+
case WS_DATA_PREFIX.BINARY:
|
|
155
|
+
return this.finish("binary", chunk);
|
|
156
|
+
default:
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
startOrAppend(type, chunk) {
|
|
161
|
+
if (this.bufferedType === null) this.bufferedType = type;
|
|
162
|
+
this.buffer.push(chunk);
|
|
163
|
+
}
|
|
164
|
+
finish(type, chunk) {
|
|
165
|
+
if (this.buffer.length === 0) {
|
|
166
|
+
return this.materialize(type, [chunk]);
|
|
167
|
+
}
|
|
168
|
+
const resolvedType = this.bufferedType ?? type;
|
|
169
|
+
this.buffer.push(chunk);
|
|
170
|
+
const parts = this.buffer;
|
|
171
|
+
this.buffer = [];
|
|
172
|
+
this.bufferedType = null;
|
|
173
|
+
return this.materialize(resolvedType, parts);
|
|
174
|
+
}
|
|
175
|
+
materialize(type, parts) {
|
|
176
|
+
const total = parts.reduce((sum, p) => sum + p.length, 0);
|
|
177
|
+
const joined = new Uint8Array(total);
|
|
178
|
+
let offset = 0;
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
joined.set(part, offset);
|
|
181
|
+
offset += part.length;
|
|
182
|
+
}
|
|
183
|
+
return type === "text" ? { type: "text", data: textDecoder.decode(joined) } : { type: "binary", data: joined };
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/tunnel/v2/agent-kinds.ts
|
|
188
|
+
var LOCAL_HOST = "127.0.0.1";
|
|
189
|
+
var HEADER_SEC_WS_PROTOCOL = "sec-websocket-protocol";
|
|
190
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
191
|
+
"connection",
|
|
192
|
+
"keep-alive",
|
|
193
|
+
"proxy-authenticate",
|
|
194
|
+
"proxy-authorization",
|
|
195
|
+
"te",
|
|
196
|
+
"trailer",
|
|
197
|
+
"transfer-encoding",
|
|
198
|
+
"upgrade"
|
|
199
|
+
]);
|
|
200
|
+
var textEncoder2 = new TextEncoder();
|
|
201
|
+
var AgentDispatcher = class {
|
|
202
|
+
kinds = /* @__PURE__ */ new Map();
|
|
203
|
+
register(kind) {
|
|
204
|
+
this.kinds.set(kind.kind, kind);
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
dispatch(incoming) {
|
|
208
|
+
const handler = this.kinds.get(incoming.kind);
|
|
209
|
+
if (!handler) {
|
|
210
|
+
void incoming.stream.rst(ERROR_CODE.UNSUPPORTED);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
handler.handle(incoming);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var HttpProxyKind = class {
|
|
217
|
+
constructor(ctx) {
|
|
218
|
+
this.ctx = ctx;
|
|
219
|
+
}
|
|
220
|
+
ctx;
|
|
221
|
+
kind = "http";
|
|
222
|
+
handle(incoming) {
|
|
223
|
+
void this.proxy(incoming);
|
|
224
|
+
}
|
|
225
|
+
async proxy(incoming) {
|
|
226
|
+
const head = incoming.head;
|
|
227
|
+
const request = http.request({
|
|
228
|
+
host: LOCAL_HOST,
|
|
229
|
+
port: this.ctx.localPort,
|
|
230
|
+
method: head.method,
|
|
231
|
+
path: head.path,
|
|
232
|
+
headers: toNodeHeaders(head.headers)
|
|
233
|
+
});
|
|
234
|
+
request.on("response", (response) => void this.pumpResponse(incoming.stream, response));
|
|
235
|
+
request.on("error", (error) => void this.answerError(incoming.stream, error));
|
|
236
|
+
void this.pumpRequestBody(incoming.stream, request);
|
|
237
|
+
}
|
|
238
|
+
async pumpRequestBody(stream, request) {
|
|
239
|
+
for (; ; ) {
|
|
240
|
+
const event = await stream.recv();
|
|
241
|
+
if (event === null) {
|
|
242
|
+
request.destroy();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (event.type === "data") {
|
|
246
|
+
request.write(Buffer.from(event.payload));
|
|
247
|
+
} else if (event.type === "end") {
|
|
248
|
+
request.end();
|
|
249
|
+
return;
|
|
250
|
+
} else {
|
|
251
|
+
request.destroy();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async pumpResponse(stream, response) {
|
|
257
|
+
const headers = [];
|
|
258
|
+
for (let i = 0; i + 1 < response.rawHeaders.length; i += 2) {
|
|
259
|
+
headers.push([response.rawHeaders[i].toLowerCase(), response.rawHeaders[i + 1]]);
|
|
260
|
+
}
|
|
261
|
+
await stream.accept({ status: response.statusCode ?? 502, headers });
|
|
262
|
+
for await (const chunk of response) {
|
|
263
|
+
await stream.data(new Uint8Array(chunk));
|
|
264
|
+
}
|
|
265
|
+
await stream.end();
|
|
266
|
+
}
|
|
267
|
+
async answerError(stream, error) {
|
|
268
|
+
try {
|
|
269
|
+
await stream.accept({
|
|
270
|
+
status: 502,
|
|
271
|
+
headers: [["content-type", "text/plain; charset=utf-8"]]
|
|
272
|
+
});
|
|
273
|
+
await stream.data(textEncoder2.encode(`dev server unreachable: ${error.message}`));
|
|
274
|
+
await stream.end();
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var WsSpliceKind = class {
|
|
280
|
+
constructor(ctx) {
|
|
281
|
+
this.ctx = ctx;
|
|
282
|
+
}
|
|
283
|
+
ctx;
|
|
284
|
+
kind = "ws";
|
|
285
|
+
handle(incoming) {
|
|
286
|
+
void this.splice(incoming);
|
|
287
|
+
}
|
|
288
|
+
async splice(incoming) {
|
|
289
|
+
const head = incoming.head;
|
|
290
|
+
const protocols = head.headers.filter(([name]) => name === HEADER_SEC_WS_PROTOCOL).flatMap(([, value]) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
291
|
+
const local = this.ctx.createLocalSocket(
|
|
292
|
+
`ws://${LOCAL_HOST}:${this.ctx.localPort}${head.path}`,
|
|
293
|
+
protocols
|
|
294
|
+
);
|
|
295
|
+
local.binaryType = "arraybuffer";
|
|
296
|
+
let opened = false;
|
|
297
|
+
const reassembler = new WsReassembler();
|
|
298
|
+
local.addEventListener("open", () => {
|
|
299
|
+
opened = true;
|
|
300
|
+
const headers = local.protocol ? [[HEADER_SEC_WS_PROTOCOL, local.protocol]] : [];
|
|
301
|
+
void incoming.stream.accept({ status: 101, headers });
|
|
302
|
+
});
|
|
303
|
+
local.addEventListener("message", (event) => {
|
|
304
|
+
const message = toWsMessage(event.data);
|
|
305
|
+
if (!message) return;
|
|
306
|
+
for (const frame of encodeWsMessage(message, this.ctx.maxFramePayload)) {
|
|
307
|
+
void incoming.stream.data(frame);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
local.addEventListener("close", (event) => {
|
|
311
|
+
void incoming.stream.close({ code: sendableCloseCode(event.code), reason: event.reason });
|
|
312
|
+
});
|
|
313
|
+
local.addEventListener("error", () => {
|
|
314
|
+
if (opened) return;
|
|
315
|
+
void this.refuse(incoming.stream);
|
|
316
|
+
});
|
|
317
|
+
for (; ; ) {
|
|
318
|
+
const event = await incoming.stream.recv();
|
|
319
|
+
if (event === null) return;
|
|
320
|
+
if (event.type === "data") {
|
|
321
|
+
const message = reassembler.push(event.payload);
|
|
322
|
+
if (message) {
|
|
323
|
+
local.send(message.type === "text" ? message.data : message.data);
|
|
324
|
+
}
|
|
325
|
+
} else if (event.type === "close") {
|
|
326
|
+
const info2 = event.info;
|
|
327
|
+
local.close(sendableCloseCode(info2?.code), info2?.reason);
|
|
328
|
+
return;
|
|
329
|
+
} else if (event.type === "end" || event.type === "rst") {
|
|
330
|
+
local.close();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async refuse(stream) {
|
|
336
|
+
try {
|
|
337
|
+
await stream.accept({
|
|
338
|
+
status: 502,
|
|
339
|
+
headers: [["content-type", "text/plain; charset=utf-8"]]
|
|
340
|
+
});
|
|
341
|
+
await stream.data(textEncoder2.encode("dev server refused the websocket"));
|
|
342
|
+
await stream.end();
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function toNodeHeaders(pairs) {
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const [name, value] of pairs) {
|
|
350
|
+
if (HOP_BY_HOP_HEADERS.has(name)) continue;
|
|
351
|
+
const existing = out[name];
|
|
352
|
+
if (existing === void 0) out[name] = value;
|
|
353
|
+
else if (Array.isArray(existing)) existing.push(value);
|
|
354
|
+
else out[name] = [existing, value];
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
function toWsMessage(data) {
|
|
359
|
+
if (typeof data === "string") return { type: "text", data };
|
|
360
|
+
if (data instanceof ArrayBuffer) return { type: "binary", data: new Uint8Array(data) };
|
|
361
|
+
if (data instanceof Uint8Array) return { type: "binary", data };
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
function sendableCloseCode(code) {
|
|
365
|
+
if (code === void 0) return void 0;
|
|
366
|
+
const legal = code >= 1e3 && code <= 1003 || code >= 1007 && code <= 1014 || code >= 3e3 && code <= 4999;
|
|
367
|
+
return legal ? code : void 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/tunnel/protocol.ts
|
|
371
|
+
var MAX_PAYLOAD = 262144;
|
|
372
|
+
var HEADER_LEN2 = 9;
|
|
373
|
+
var SUBPROTOCOL = "cimplify-tunnel.v1";
|
|
374
|
+
var CONNECT_PATH = "/__cimplify/connect";
|
|
375
|
+
var FRAME_TYPE = {
|
|
376
|
+
OPEN_HTTP: 1,
|
|
377
|
+
OPEN_WS: 2,
|
|
378
|
+
RESP_HEAD: 3,
|
|
379
|
+
BODY: 4,
|
|
380
|
+
END: 5,
|
|
381
|
+
WS_TEXT: 6,
|
|
382
|
+
WS_BIN: 7,
|
|
383
|
+
CLOSE: 8
|
|
384
|
+
};
|
|
385
|
+
var KNOWN_FRAME_TYPES = new Set(Object.values(FRAME_TYPE));
|
|
386
|
+
var TunnelProtocolError = class extends Error {
|
|
387
|
+
};
|
|
388
|
+
function encodeFrame(frame) {
|
|
389
|
+
if (frame.payload.length > MAX_PAYLOAD) {
|
|
390
|
+
throw new TunnelProtocolError(`payload length ${frame.payload.length} exceeds ${MAX_PAYLOAD}`);
|
|
391
|
+
}
|
|
392
|
+
const bytes = new Uint8Array(HEADER_LEN2 + frame.payload.length);
|
|
393
|
+
const view = new DataView(bytes.buffer);
|
|
394
|
+
view.setUint32(0, frame.streamId);
|
|
395
|
+
view.setUint8(4, frame.frameType);
|
|
396
|
+
view.setUint32(5, frame.payload.length);
|
|
397
|
+
bytes.set(frame.payload, HEADER_LEN2);
|
|
398
|
+
return bytes;
|
|
399
|
+
}
|
|
400
|
+
function decodeFrame(bytes) {
|
|
401
|
+
if (bytes.length < HEADER_LEN2) {
|
|
402
|
+
throw new TunnelProtocolError(`frame truncated: ${bytes.length} bytes`);
|
|
403
|
+
}
|
|
404
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
405
|
+
const streamId = view.getUint32(0);
|
|
406
|
+
const rawType = view.getUint8(4);
|
|
407
|
+
if (!KNOWN_FRAME_TYPES.has(rawType)) {
|
|
408
|
+
throw new TunnelProtocolError(`unknown frame type 0x${rawType.toString(16).padStart(2, "0")}`);
|
|
409
|
+
}
|
|
410
|
+
const declared = view.getUint32(5);
|
|
411
|
+
if (declared > MAX_PAYLOAD) {
|
|
412
|
+
throw new TunnelProtocolError(`payload length ${declared} exceeds ${MAX_PAYLOAD}`);
|
|
413
|
+
}
|
|
414
|
+
const actual = bytes.length - HEADER_LEN2;
|
|
415
|
+
if (declared !== actual) {
|
|
416
|
+
throw new TunnelProtocolError(
|
|
417
|
+
`payload length field ${declared} does not match remaining ${actual} bytes`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return { streamId, frameType: rawType, payload: bytes.subarray(HEADER_LEN2) };
|
|
421
|
+
}
|
|
422
|
+
var textEncoder3 = new TextEncoder();
|
|
423
|
+
var textDecoder2 = new TextDecoder();
|
|
424
|
+
function encodeJsonPayload(value) {
|
|
425
|
+
return textEncoder3.encode(JSON.stringify(value));
|
|
426
|
+
}
|
|
427
|
+
function decodeJsonPayload(payload) {
|
|
428
|
+
return JSON.parse(textDecoder2.decode(payload));
|
|
429
|
+
}
|
|
430
|
+
function* chunkPayload(bytes) {
|
|
431
|
+
if (bytes.length === 0) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
for (let offset = 0; offset < bytes.length; offset += MAX_PAYLOAD) {
|
|
435
|
+
yield bytes.subarray(offset, Math.min(offset + MAX_PAYLOAD, bytes.length));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/tunnel/client.ts
|
|
440
|
+
var LOCAL_HOST2 = "127.0.0.1";
|
|
441
|
+
var SEND_HIGH_WATER_BYTES = 4 * 1024 * 1024;
|
|
442
|
+
var BACKPRESSURE_POLL_MS = 25;
|
|
443
|
+
var EMPTY_PAYLOAD = new Uint8Array(0);
|
|
444
|
+
var HEADER_SEC_WS_PROTOCOL2 = "sec-websocket-protocol";
|
|
445
|
+
var HEADER_CONTENT_TYPE_TEXT = ["content-type", "text/plain; charset=utf-8"];
|
|
446
|
+
var WS_CLOSE_NORMAL = 1e3;
|
|
447
|
+
var HOP_BY_HOP_HEADERS2 = /* @__PURE__ */ new Set([
|
|
448
|
+
"connection",
|
|
449
|
+
"keep-alive",
|
|
450
|
+
"proxy-authenticate",
|
|
451
|
+
"proxy-authorization",
|
|
452
|
+
"te",
|
|
453
|
+
"trailer",
|
|
454
|
+
"transfer-encoding",
|
|
455
|
+
"upgrade"
|
|
456
|
+
]);
|
|
457
|
+
var textEncoder4 = new TextEncoder();
|
|
458
|
+
var textDecoder3 = new TextDecoder();
|
|
459
|
+
function defaultSocketFactory(url, protocols) {
|
|
460
|
+
const ctor = globalThis.WebSocket;
|
|
461
|
+
if (!ctor) {
|
|
462
|
+
throw new Error("global WebSocket is unavailable; the preview tunnel requires Bun or Node 22+");
|
|
463
|
+
}
|
|
464
|
+
return new ctor(url, protocols);
|
|
465
|
+
}
|
|
466
|
+
function connectUrl(baseUrl, token) {
|
|
467
|
+
return `${baseUrl.replace(/\/+$/, "")}${CONNECT_PATH}?token=${encodeURIComponent(token)}`;
|
|
468
|
+
}
|
|
469
|
+
var TunnelClient = class {
|
|
470
|
+
constructor(options) {
|
|
471
|
+
this.options = options;
|
|
472
|
+
}
|
|
473
|
+
options;
|
|
474
|
+
streams = /* @__PURE__ */ new Map();
|
|
475
|
+
socket = null;
|
|
476
|
+
closed = false;
|
|
477
|
+
start() {
|
|
478
|
+
const factory = this.options.createSocket ?? defaultSocketFactory;
|
|
479
|
+
const socket = factory(connectUrl(this.options.url, this.options.token), [SUBPROTOCOL]);
|
|
480
|
+
socket.binaryType = "arraybuffer";
|
|
481
|
+
this.socket = socket;
|
|
482
|
+
socket.addEventListener("open", () => {
|
|
483
|
+
if (!this.closed) this.options.events?.onConnected?.();
|
|
484
|
+
});
|
|
485
|
+
socket.addEventListener("message", (event) => this.handleMessage(event.data));
|
|
486
|
+
socket.addEventListener(
|
|
487
|
+
"close",
|
|
488
|
+
(event) => this.teardown(`connection closed (${event.code ?? "unknown"})`, event.code)
|
|
489
|
+
);
|
|
490
|
+
socket.addEventListener("error", () => this.teardown("connection error"));
|
|
491
|
+
if (this.options.socketAlreadyOpen && !this.closed) this.options.events?.onConnected?.();
|
|
492
|
+
}
|
|
493
|
+
stop() {
|
|
494
|
+
if (this.closed) return;
|
|
495
|
+
try {
|
|
496
|
+
this.socket?.close(WS_CLOSE_NORMAL);
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
this.teardown("stopped");
|
|
500
|
+
}
|
|
501
|
+
teardown(reason, closeCode) {
|
|
502
|
+
if (this.closed) return;
|
|
503
|
+
this.closed = true;
|
|
504
|
+
for (const handler of this.streams.values()) {
|
|
505
|
+
handler.abort();
|
|
506
|
+
}
|
|
507
|
+
this.streams.clear();
|
|
508
|
+
this.socket = null;
|
|
509
|
+
this.options.events?.onClosed?.(reason, closeCode);
|
|
510
|
+
}
|
|
511
|
+
handleMessage(data) {
|
|
512
|
+
if (this.closed) return;
|
|
513
|
+
let bytes;
|
|
514
|
+
if (data instanceof ArrayBuffer) {
|
|
515
|
+
bytes = new Uint8Array(data);
|
|
516
|
+
} else if (data instanceof Uint8Array) {
|
|
517
|
+
bytes = data;
|
|
518
|
+
} else {
|
|
519
|
+
this.failConnection("non-binary message violates the tunnel protocol");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
let frame;
|
|
523
|
+
try {
|
|
524
|
+
frame = decodeFrame(bytes);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
this.failConnection(`undecodable frame: ${err.message}`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.dispatch(frame);
|
|
530
|
+
}
|
|
531
|
+
dispatch(frame) {
|
|
532
|
+
const handler = this.streams.get(frame.streamId);
|
|
533
|
+
if (handler) {
|
|
534
|
+
if (frame.frameType === FRAME_TYPE.CLOSE) this.streams.delete(frame.streamId);
|
|
535
|
+
handler.handleFrame(frame);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
switch (frame.frameType) {
|
|
539
|
+
case FRAME_TYPE.OPEN_HTTP:
|
|
540
|
+
this.openHttp(frame);
|
|
541
|
+
break;
|
|
542
|
+
case FRAME_TYPE.OPEN_WS:
|
|
543
|
+
this.openWs(frame);
|
|
544
|
+
break;
|
|
545
|
+
case FRAME_TYPE.CLOSE:
|
|
546
|
+
break;
|
|
547
|
+
default:
|
|
548
|
+
this.sendClose(frame.streamId);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
openHttp(frame) {
|
|
552
|
+
let head;
|
|
553
|
+
try {
|
|
554
|
+
head = decodeJsonPayload(frame.payload);
|
|
555
|
+
} catch {
|
|
556
|
+
this.sendClose(frame.streamId);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
this.streams.set(
|
|
560
|
+
frame.streamId,
|
|
561
|
+
new HttpProxyStream(this.contextFor(frame.streamId), head, this.options.localPort)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
openWs(frame) {
|
|
565
|
+
let head;
|
|
566
|
+
try {
|
|
567
|
+
head = decodeJsonPayload(frame.payload);
|
|
568
|
+
} catch {
|
|
569
|
+
this.sendClose(frame.streamId);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const factory = this.options.createLocalSocket ?? defaultSocketFactory;
|
|
573
|
+
this.streams.set(
|
|
574
|
+
frame.streamId,
|
|
575
|
+
new WsProxyStream(this.contextFor(frame.streamId), head, this.options.localPort, factory)
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
contextFor(streamId) {
|
|
579
|
+
return {
|
|
580
|
+
send: (frameType, payload) => this.sendFrame({ streamId, frameType, payload }),
|
|
581
|
+
sendJson: (frameType, value) => this.sendFrame({ streamId, frameType, payload: encodeJsonPayload(value) }),
|
|
582
|
+
finish: () => {
|
|
583
|
+
this.streams.delete(streamId);
|
|
584
|
+
},
|
|
585
|
+
fail: (info2) => {
|
|
586
|
+
this.streams.delete(streamId);
|
|
587
|
+
this.sendClose(streamId, info2);
|
|
588
|
+
},
|
|
589
|
+
bufferedAmount: () => this.socket?.bufferedAmount ?? 0
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
sendClose(streamId, info2) {
|
|
593
|
+
const payload = info2 && (info2.code !== void 0 || info2.reason !== void 0) ? encodeJsonPayload(info2) : EMPTY_PAYLOAD;
|
|
594
|
+
this.sendFrame({ streamId, frameType: FRAME_TYPE.CLOSE, payload });
|
|
595
|
+
}
|
|
596
|
+
sendFrame(frame) {
|
|
597
|
+
if (this.closed || !this.socket) return false;
|
|
598
|
+
try {
|
|
599
|
+
this.socket.send(encodeFrame(frame));
|
|
600
|
+
return true;
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
failConnection(reason) {
|
|
606
|
+
try {
|
|
607
|
+
this.socket?.close(WS_CLOSE_NORMAL);
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
this.teardown(reason);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
var HttpProxyStream = class {
|
|
614
|
+
constructor(ctx, head, localPort) {
|
|
615
|
+
this.ctx = ctx;
|
|
616
|
+
this.request = http.request({
|
|
617
|
+
host: LOCAL_HOST2,
|
|
618
|
+
port: localPort,
|
|
619
|
+
method: head.method,
|
|
620
|
+
path: head.path,
|
|
621
|
+
headers: toNodeHeaders2(head.headers)
|
|
622
|
+
});
|
|
623
|
+
this.request.on("response", (response) => this.pumpResponse(response));
|
|
624
|
+
this.request.on("error", (err) => this.handleRequestError(err));
|
|
625
|
+
}
|
|
626
|
+
ctx;
|
|
627
|
+
request;
|
|
628
|
+
responded = false;
|
|
629
|
+
done = false;
|
|
630
|
+
handleFrame(frame) {
|
|
631
|
+
switch (frame.frameType) {
|
|
632
|
+
case FRAME_TYPE.BODY:
|
|
633
|
+
this.request.write(Buffer.from(frame.payload));
|
|
634
|
+
break;
|
|
635
|
+
case FRAME_TYPE.END:
|
|
636
|
+
this.request.end();
|
|
637
|
+
break;
|
|
638
|
+
case FRAME_TYPE.CLOSE:
|
|
639
|
+
this.abort();
|
|
640
|
+
break;
|
|
641
|
+
default:
|
|
642
|
+
this.ctx.fail();
|
|
643
|
+
this.abort();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
abort() {
|
|
647
|
+
this.done = true;
|
|
648
|
+
this.request.destroy();
|
|
649
|
+
}
|
|
650
|
+
pumpResponse(response) {
|
|
651
|
+
this.responded = true;
|
|
652
|
+
const headers = [];
|
|
653
|
+
for (let i = 0; i + 1 < response.rawHeaders.length; i += 2) {
|
|
654
|
+
headers.push([response.rawHeaders[i].toLowerCase(), response.rawHeaders[i + 1]]);
|
|
655
|
+
}
|
|
656
|
+
const head = { status: response.statusCode ?? 502, headers };
|
|
657
|
+
if (!this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, head)) {
|
|
658
|
+
this.abort();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
response.on("data", (chunk) => {
|
|
662
|
+
for (const piece of chunkPayload(chunk)) {
|
|
663
|
+
if (!this.ctx.send(FRAME_TYPE.BODY, piece)) {
|
|
664
|
+
this.abort();
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
this.applyBackpressure(response);
|
|
669
|
+
});
|
|
670
|
+
response.on("end", () => {
|
|
671
|
+
if (this.done) return;
|
|
672
|
+
this.done = true;
|
|
673
|
+
this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
|
|
674
|
+
this.ctx.finish();
|
|
675
|
+
});
|
|
676
|
+
response.on("error", () => {
|
|
677
|
+
if (this.done) return;
|
|
678
|
+
this.done = true;
|
|
679
|
+
this.ctx.fail();
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/** The mux socket has no stream-level flow control; pause the local read
|
|
683
|
+
* side when the shared connection's send buffer backs up. */
|
|
684
|
+
applyBackpressure(response) {
|
|
685
|
+
if (this.ctx.bufferedAmount() <= SEND_HIGH_WATER_BYTES) return;
|
|
686
|
+
response.pause();
|
|
687
|
+
const poll = () => {
|
|
688
|
+
if (this.done) return;
|
|
689
|
+
if (this.ctx.bufferedAmount() <= SEND_HIGH_WATER_BYTES) {
|
|
690
|
+
response.resume();
|
|
691
|
+
} else {
|
|
692
|
+
setTimeout(poll, BACKPRESSURE_POLL_MS);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
setTimeout(poll, BACKPRESSURE_POLL_MS);
|
|
696
|
+
}
|
|
697
|
+
handleRequestError(err) {
|
|
698
|
+
if (this.done) return;
|
|
699
|
+
this.done = true;
|
|
700
|
+
if (this.responded) {
|
|
701
|
+
this.ctx.fail();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, {
|
|
705
|
+
status: 502,
|
|
706
|
+
headers: [HEADER_CONTENT_TYPE_TEXT]
|
|
707
|
+
});
|
|
708
|
+
this.ctx.send(FRAME_TYPE.BODY, textEncoder4.encode(`dev server unreachable: ${err.message}`));
|
|
709
|
+
this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
|
|
710
|
+
this.ctx.finish();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
var WsProxyStream = class {
|
|
714
|
+
constructor(ctx, head, localPort, factory) {
|
|
715
|
+
this.ctx = ctx;
|
|
716
|
+
const protocols = head.headers.filter(([name]) => name === HEADER_SEC_WS_PROTOCOL2).flatMap(([, value]) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
717
|
+
this.local = factory(`ws://${LOCAL_HOST2}:${localPort}${head.path}`, protocols);
|
|
718
|
+
this.local.binaryType = "arraybuffer";
|
|
719
|
+
this.local.addEventListener("open", () => {
|
|
720
|
+
this.opened = true;
|
|
721
|
+
const headers = this.local.protocol ? [[HEADER_SEC_WS_PROTOCOL2, this.local.protocol]] : [];
|
|
722
|
+
if (!this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, { status: 101, headers })) {
|
|
723
|
+
this.abort();
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
this.local.addEventListener("message", (event) => this.forward(event.data));
|
|
727
|
+
this.local.addEventListener("close", (event) => {
|
|
728
|
+
if (this.done) return;
|
|
729
|
+
this.done = true;
|
|
730
|
+
this.ctx.fail({ code: sendableCloseCode2(event.code), reason: event.reason || void 0 });
|
|
731
|
+
});
|
|
732
|
+
this.local.addEventListener("error", () => {
|
|
733
|
+
if (this.done || this.opened) return;
|
|
734
|
+
this.done = true;
|
|
735
|
+
this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, {
|
|
736
|
+
status: 502,
|
|
737
|
+
headers: [HEADER_CONTENT_TYPE_TEXT]
|
|
738
|
+
});
|
|
739
|
+
this.ctx.send(FRAME_TYPE.BODY, textEncoder4.encode("dev server refused the websocket"));
|
|
740
|
+
this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
|
|
741
|
+
this.ctx.finish();
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
ctx;
|
|
745
|
+
local;
|
|
746
|
+
opened = false;
|
|
747
|
+
done = false;
|
|
748
|
+
handleFrame(frame) {
|
|
749
|
+
switch (frame.frameType) {
|
|
750
|
+
case FRAME_TYPE.WS_TEXT:
|
|
751
|
+
this.local.send(textDecoder3.decode(frame.payload));
|
|
752
|
+
break;
|
|
753
|
+
case FRAME_TYPE.WS_BIN:
|
|
754
|
+
this.local.send(frame.payload);
|
|
755
|
+
break;
|
|
756
|
+
case FRAME_TYPE.CLOSE: {
|
|
757
|
+
this.done = true;
|
|
758
|
+
const info2 = parseCloseInfo(frame.payload);
|
|
759
|
+
this.local.close(sanitizeCloseCode(info2.code), info2.reason);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
default:
|
|
763
|
+
this.ctx.fail();
|
|
764
|
+
this.abort();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
abort() {
|
|
768
|
+
this.done = true;
|
|
769
|
+
try {
|
|
770
|
+
this.local.close();
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
forward(data) {
|
|
775
|
+
if (this.done) return;
|
|
776
|
+
let payload;
|
|
777
|
+
let frameType;
|
|
778
|
+
if (typeof data === "string") {
|
|
779
|
+
payload = textEncoder4.encode(data);
|
|
780
|
+
frameType = FRAME_TYPE.WS_TEXT;
|
|
781
|
+
} else if (data instanceof ArrayBuffer) {
|
|
782
|
+
payload = new Uint8Array(data);
|
|
783
|
+
frameType = FRAME_TYPE.WS_BIN;
|
|
784
|
+
} else if (data instanceof Uint8Array) {
|
|
785
|
+
payload = data;
|
|
786
|
+
frameType = FRAME_TYPE.WS_BIN;
|
|
787
|
+
} else {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (payload.length > MAX_PAYLOAD) {
|
|
791
|
+
this.done = true;
|
|
792
|
+
this.ctx.fail({ reason: "message exceeds tunnel frame cap" });
|
|
793
|
+
this.abort();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (!this.ctx.send(frameType, payload)) this.abort();
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
function toNodeHeaders2(pairs) {
|
|
800
|
+
const out = {};
|
|
801
|
+
for (const [name, value] of pairs) {
|
|
802
|
+
if (HOP_BY_HOP_HEADERS2.has(name)) continue;
|
|
803
|
+
const existing = out[name];
|
|
804
|
+
if (existing === void 0) {
|
|
805
|
+
out[name] = value;
|
|
806
|
+
} else if (Array.isArray(existing)) {
|
|
807
|
+
existing.push(value);
|
|
808
|
+
} else {
|
|
809
|
+
out[name] = [existing, value];
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return out;
|
|
813
|
+
}
|
|
814
|
+
function parseCloseInfo(payload) {
|
|
815
|
+
if (payload.length === 0) return {};
|
|
816
|
+
try {
|
|
817
|
+
return decodeJsonPayload(payload);
|
|
818
|
+
} catch {
|
|
819
|
+
return {};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function sanitizeCloseCode(code) {
|
|
823
|
+
if (code === void 0) return void 0;
|
|
824
|
+
return code === WS_CLOSE_NORMAL || code >= 3e3 && code <= 4999 ? code : void 0;
|
|
825
|
+
}
|
|
826
|
+
function sendableCloseCode2(code) {
|
|
827
|
+
if (code === void 0) return void 0;
|
|
828
|
+
const legal = code >= 1e3 && code <= 1003 || code >= 1007 && code <= 1014 || code >= 3e3 && code <= 4999;
|
|
829
|
+
return legal ? code : void 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/tunnel/v2/flow.ts
|
|
833
|
+
var SendWindow = class {
|
|
834
|
+
available;
|
|
835
|
+
constructor(initial) {
|
|
836
|
+
this.available = initial;
|
|
837
|
+
}
|
|
838
|
+
get availableCredit() {
|
|
839
|
+
return this.available;
|
|
840
|
+
}
|
|
841
|
+
grantable(wanted) {
|
|
842
|
+
return this.available <= 0 ? 0 : Math.min(this.available, wanted);
|
|
843
|
+
}
|
|
844
|
+
consume(n) {
|
|
845
|
+
this.available = Math.max(0, this.available - n);
|
|
846
|
+
}
|
|
847
|
+
/** Returns false on overflow past Number.MAX_SAFE_INTEGER (protocol error). */
|
|
848
|
+
increase(credit) {
|
|
849
|
+
const next = this.available + credit;
|
|
850
|
+
if (next > Number.MAX_SAFE_INTEGER) return false;
|
|
851
|
+
this.available = next;
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
var RecvWindow = class {
|
|
856
|
+
constructor(window) {
|
|
857
|
+
this.window = window;
|
|
858
|
+
}
|
|
859
|
+
window;
|
|
860
|
+
consumed = 0;
|
|
861
|
+
/** Credit to emit as WINDOW_UPDATE once half the window is consumed, else 0. */
|
|
862
|
+
recordConsumed(n) {
|
|
863
|
+
this.consumed += n;
|
|
864
|
+
if (this.window === 0 || this.consumed < Math.ceil(this.window / 2)) return 0;
|
|
865
|
+
const credit = this.consumed;
|
|
866
|
+
this.consumed = 0;
|
|
867
|
+
return credit;
|
|
868
|
+
}
|
|
869
|
+
outstandingCapacity() {
|
|
870
|
+
return Math.max(0, this.window - this.consumed);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// src/tunnel/v2/session.ts
|
|
875
|
+
var SessionV2Error = class extends Error {
|
|
876
|
+
};
|
|
877
|
+
var StreamHandleV2 = class {
|
|
878
|
+
constructor(session, streamId) {
|
|
879
|
+
this.session = session;
|
|
880
|
+
this.streamId = streamId;
|
|
881
|
+
}
|
|
882
|
+
session;
|
|
883
|
+
streamId;
|
|
884
|
+
accept(head) {
|
|
885
|
+
return this.session.sendHead(this.streamId, FRAME_TYPE_V2.ACCEPT, head);
|
|
886
|
+
}
|
|
887
|
+
data(bytes) {
|
|
888
|
+
return this.session.queueData(this.streamId, bytes);
|
|
889
|
+
}
|
|
890
|
+
end() {
|
|
891
|
+
return this.session.sendEnd(this.streamId);
|
|
892
|
+
}
|
|
893
|
+
close(info2) {
|
|
894
|
+
return this.session.closeStream(this.streamId, info2);
|
|
895
|
+
}
|
|
896
|
+
rst(code) {
|
|
897
|
+
return this.session.rstStream(this.streamId, code);
|
|
898
|
+
}
|
|
899
|
+
/** Next event; null when the stream is gone. Reads drive window replenish. */
|
|
900
|
+
recv() {
|
|
901
|
+
return this.session.nextEvent(this.streamId);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
var CONNECTION_STREAM_ID = 0;
|
|
905
|
+
var textEncoder5 = new TextEncoder();
|
|
906
|
+
var textDecoder4 = new TextDecoder();
|
|
907
|
+
var SessionV2 = class {
|
|
908
|
+
constructor(transport, options) {
|
|
909
|
+
this.transport = transport;
|
|
910
|
+
this.options = options;
|
|
911
|
+
this.nextStreamId = options.role === "service" ? 1 : 2;
|
|
912
|
+
this.connectionSend = new SendWindow(0);
|
|
913
|
+
this.connectionRecv = new RecvWindow(options.local.connection_window);
|
|
914
|
+
transport.onMessage((bytes) => this.onMessage(bytes));
|
|
915
|
+
transport.onClose((reason) => this.teardown(reason));
|
|
916
|
+
}
|
|
917
|
+
transport;
|
|
918
|
+
options;
|
|
919
|
+
streams = /* @__PURE__ */ new Map();
|
|
920
|
+
ready = [];
|
|
921
|
+
connectionSend;
|
|
922
|
+
connectionRecv;
|
|
923
|
+
peer = null;
|
|
924
|
+
nextStreamId;
|
|
925
|
+
draining = false;
|
|
926
|
+
closed = false;
|
|
927
|
+
sending = Promise.resolve();
|
|
928
|
+
/** Send our HELLO; the peer's must arrive as its first frame. */
|
|
929
|
+
async start() {
|
|
930
|
+
const payload = textEncoder5.encode(JSON.stringify(this.options.local));
|
|
931
|
+
await this.transport.send(
|
|
932
|
+
encodeFrameV2({ streamId: CONNECTION_STREAM_ID, frameType: FRAME_TYPE_V2.HELLO, payload })
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
get peerHello() {
|
|
936
|
+
return this.peer;
|
|
937
|
+
}
|
|
938
|
+
async open(kind, head) {
|
|
939
|
+
if (this.closed || this.draining) throw new SessionV2Error("session closed or draining");
|
|
940
|
+
const peer = this.requirePeer();
|
|
941
|
+
if (!peer.capabilities.includes(kind)) throw new SessionV2Error(`peer lacks ${kind}`);
|
|
942
|
+
const streamId = this.nextStreamId;
|
|
943
|
+
this.nextStreamId += 2;
|
|
944
|
+
this.registerStream(streamId);
|
|
945
|
+
await this.sendHead(streamId, FRAME_TYPE_V2.OPEN, { kind, head });
|
|
946
|
+
return new StreamHandleV2(this, streamId);
|
|
947
|
+
}
|
|
948
|
+
async goAway(code, reason) {
|
|
949
|
+
this.draining = true;
|
|
950
|
+
await this.sendHead(CONNECTION_STREAM_ID, FRAME_TYPE_V2.GOAWAY, {
|
|
951
|
+
code,
|
|
952
|
+
last_stream_id: this.nextStreamId,
|
|
953
|
+
...reason ? { reason } : {}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
requirePeer() {
|
|
957
|
+
if (!this.peer) throw new SessionV2Error("handshake incomplete");
|
|
958
|
+
return this.peer;
|
|
959
|
+
}
|
|
960
|
+
registerStream(streamId) {
|
|
961
|
+
const peer = this.requirePeer();
|
|
962
|
+
const state = {
|
|
963
|
+
events: [],
|
|
964
|
+
waiter: null,
|
|
965
|
+
sendWindow: new SendWindow(peer.initial_stream_window),
|
|
966
|
+
recvWindow: new RecvWindow(this.options.local.initial_stream_window),
|
|
967
|
+
pending: [],
|
|
968
|
+
localEnded: false,
|
|
969
|
+
remoteEnded: false,
|
|
970
|
+
finished: false
|
|
971
|
+
};
|
|
972
|
+
this.streams.set(streamId, state);
|
|
973
|
+
return state;
|
|
974
|
+
}
|
|
975
|
+
onMessage(bytes) {
|
|
976
|
+
if (this.closed) return;
|
|
977
|
+
let frame;
|
|
978
|
+
try {
|
|
979
|
+
frame = decodeFrameV2(bytes, this.options.local.max_frame_payload);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
this.fail(`undecodable frame: ${error.message}`);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (!this.peer) {
|
|
985
|
+
this.onHandshakeFrame(frame);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
void this.dispatch(frame);
|
|
989
|
+
}
|
|
990
|
+
onHandshakeFrame(frame) {
|
|
991
|
+
if (frame.frameType !== FRAME_TYPE_V2.HELLO || frame.streamId !== CONNECTION_STREAM_ID) {
|
|
992
|
+
this.fail("first frame was not HELLO");
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
const peer = parseHello(frame.payload);
|
|
997
|
+
if (peer.version !== this.options.local.version) {
|
|
998
|
+
this.fail(`peer version ${peer.version}`);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
this.peer = peer;
|
|
1002
|
+
this.connectionSend = new SendWindow(peer.connection_window);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
this.fail(error.message);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async dispatch(frame) {
|
|
1008
|
+
switch (frame.frameType) {
|
|
1009
|
+
case FRAME_TYPE_V2.HELLO:
|
|
1010
|
+
this.fail("HELLO after handshake");
|
|
1011
|
+
return;
|
|
1012
|
+
case FRAME_TYPE_V2.OPEN:
|
|
1013
|
+
this.onOpen(frame);
|
|
1014
|
+
return;
|
|
1015
|
+
case FRAME_TYPE_V2.ACCEPT:
|
|
1016
|
+
this.deliver(frame.streamId, { type: "accepted", head: parseJson(frame.payload) });
|
|
1017
|
+
return;
|
|
1018
|
+
case FRAME_TYPE_V2.DATA:
|
|
1019
|
+
this.onData(frame);
|
|
1020
|
+
return;
|
|
1021
|
+
case FRAME_TYPE_V2.END:
|
|
1022
|
+
this.onEnd(frame.streamId);
|
|
1023
|
+
return;
|
|
1024
|
+
case FRAME_TYPE_V2.CLOSE:
|
|
1025
|
+
this.finishStream(frame.streamId, { type: "close", info: parseJson(frame.payload) });
|
|
1026
|
+
return;
|
|
1027
|
+
case FRAME_TYPE_V2.RST:
|
|
1028
|
+
this.finishStream(frame.streamId, {
|
|
1029
|
+
type: "rst",
|
|
1030
|
+
code: payloadU32(frame) ?? ERROR_CODE.PROTOCOL_ERROR
|
|
1031
|
+
});
|
|
1032
|
+
return;
|
|
1033
|
+
case FRAME_TYPE_V2.WINDOW_UPDATE:
|
|
1034
|
+
this.onWindowUpdate(frame);
|
|
1035
|
+
return;
|
|
1036
|
+
case FRAME_TYPE_V2.PING:
|
|
1037
|
+
this.enqueueSend({ streamId: 0, frameType: FRAME_TYPE_V2.PONG, payload: frame.payload });
|
|
1038
|
+
return;
|
|
1039
|
+
case FRAME_TYPE_V2.GOAWAY:
|
|
1040
|
+
this.draining = true;
|
|
1041
|
+
return;
|
|
1042
|
+
default:
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
onOpen(frame) {
|
|
1047
|
+
const streamId = frame.streamId;
|
|
1048
|
+
const allocatesLocally = this.options.role === "service" ? streamId % 2 === 1 : streamId % 2 === 0;
|
|
1049
|
+
if (streamId === CONNECTION_STREAM_ID || allocatesLocally || this.streams.has(streamId)) {
|
|
1050
|
+
this.fail(`peer opened stream ${streamId}`);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (this.streams.size >= this.options.local.max_concurrent_streams || this.draining) {
|
|
1054
|
+
this.enqueueSend(rstFrame(streamId, ERROR_CODE.REFUSED));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const open = parseJson(frame.payload);
|
|
1058
|
+
if (!open || typeof open.kind !== "string") {
|
|
1059
|
+
this.enqueueSend(rstFrame(streamId, ERROR_CODE.PROTOCOL_ERROR));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (!this.options.local.capabilities.includes(open.kind)) {
|
|
1063
|
+
this.enqueueSend(rstFrame(streamId, ERROR_CODE.UNSUPPORTED));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
this.registerStream(streamId);
|
|
1067
|
+
this.options.onIncoming({
|
|
1068
|
+
kind: open.kind,
|
|
1069
|
+
head: open.head ?? null,
|
|
1070
|
+
stream: new StreamHandleV2(this, streamId)
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
onData(frame) {
|
|
1074
|
+
const len = frame.payload.length;
|
|
1075
|
+
if (len > this.connectionRecv.outstandingCapacity()) {
|
|
1076
|
+
this.fail("connection window overrun");
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const state = this.streams.get(frame.streamId);
|
|
1080
|
+
if (!state) {
|
|
1081
|
+
this.enqueueSend(rstFrame(frame.streamId, ERROR_CODE.STREAM_CLOSED));
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (len > state.recvWindow.outstandingCapacity()) {
|
|
1085
|
+
this.enqueueSend(rstFrame(frame.streamId, ERROR_CODE.FLOW_CONTROL_ERROR));
|
|
1086
|
+
this.dropStream(frame.streamId);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
this.deliver(frame.streamId, { type: "data", payload: frame.payload });
|
|
1090
|
+
}
|
|
1091
|
+
onEnd(streamId) {
|
|
1092
|
+
const state = this.streams.get(streamId);
|
|
1093
|
+
if (!state) return;
|
|
1094
|
+
state.remoteEnded = true;
|
|
1095
|
+
this.deliver(streamId, { type: "end" });
|
|
1096
|
+
if (state.localEnded) this.dropStream(streamId);
|
|
1097
|
+
}
|
|
1098
|
+
onWindowUpdate(frame) {
|
|
1099
|
+
const credit = payloadU32(frame);
|
|
1100
|
+
if (!credit) {
|
|
1101
|
+
this.fail("bad WINDOW_UPDATE");
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const grew = frame.streamId === CONNECTION_STREAM_ID ? this.connectionSend.increase(credit) : this.streams.get(frame.streamId)?.sendWindow.increase(credit) ?? true;
|
|
1105
|
+
if (!grew) {
|
|
1106
|
+
this.fail("window overflow");
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
this.drainReady();
|
|
1110
|
+
}
|
|
1111
|
+
// --- outbound path -------------------------------------------------------
|
|
1112
|
+
queueData(streamId, bytes) {
|
|
1113
|
+
const state = this.streams.get(streamId);
|
|
1114
|
+
if (!state || this.closed) return Promise.reject(new SessionV2Error("stream closed"));
|
|
1115
|
+
return new Promise((resolve, reject) => {
|
|
1116
|
+
state.pending.push({ bytes, resolve, reject });
|
|
1117
|
+
if (!this.ready.includes(streamId)) this.ready.push(streamId);
|
|
1118
|
+
this.drainReady();
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
/** Round-robin: one window-and-frame-capped chunk per ready stream per turn. */
|
|
1122
|
+
drainReady() {
|
|
1123
|
+
const peer = this.peer;
|
|
1124
|
+
if (!peer) return;
|
|
1125
|
+
let turns = this.ready.length;
|
|
1126
|
+
while (turns > 0) {
|
|
1127
|
+
turns -= 1;
|
|
1128
|
+
const streamId = this.ready.shift();
|
|
1129
|
+
if (streamId === void 0) break;
|
|
1130
|
+
const state = this.streams.get(streamId);
|
|
1131
|
+
if (!state) continue;
|
|
1132
|
+
const item = state.pending[0];
|
|
1133
|
+
if (!item) continue;
|
|
1134
|
+
const wanted = Math.min(item.bytes.length, peer.max_frame_payload);
|
|
1135
|
+
const grant = this.connectionSend.grantable(state.sendWindow.grantable(wanted));
|
|
1136
|
+
if (grant === 0) {
|
|
1137
|
+
this.ready.push(streamId);
|
|
1138
|
+
if (this.connectionSend.availableCredit <= 0) break;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
const chunk = item.bytes.subarray(0, grant);
|
|
1142
|
+
item.bytes = item.bytes.subarray(grant);
|
|
1143
|
+
state.sendWindow.consume(grant);
|
|
1144
|
+
this.connectionSend.consume(grant);
|
|
1145
|
+
const finished = item.bytes.length === 0;
|
|
1146
|
+
if (finished) state.pending.shift();
|
|
1147
|
+
if (state.pending.length > 0) {
|
|
1148
|
+
this.ready.push(streamId);
|
|
1149
|
+
turns += 1;
|
|
1150
|
+
}
|
|
1151
|
+
this.enqueueSend({ streamId, frameType: FRAME_TYPE_V2.DATA, payload: chunk });
|
|
1152
|
+
if (finished) item.resolve();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
sendHead(streamId, frameType, value) {
|
|
1156
|
+
const payload = textEncoder5.encode(JSON.stringify(value ?? null));
|
|
1157
|
+
if (payload.length > MAX_HEAD_BYTES) {
|
|
1158
|
+
return Promise.reject(new SessionV2Error("head exceeds MAX_HEAD_BYTES"));
|
|
1159
|
+
}
|
|
1160
|
+
return this.enqueueSend({ streamId, frameType, payload });
|
|
1161
|
+
}
|
|
1162
|
+
sendEnd(streamId) {
|
|
1163
|
+
const state = this.streams.get(streamId);
|
|
1164
|
+
const result = this.enqueueSend({
|
|
1165
|
+
streamId,
|
|
1166
|
+
frameType: FRAME_TYPE_V2.END,
|
|
1167
|
+
payload: new Uint8Array(0)
|
|
1168
|
+
});
|
|
1169
|
+
if (state) {
|
|
1170
|
+
state.localEnded = true;
|
|
1171
|
+
if (state.remoteEnded) this.dropStream(streamId);
|
|
1172
|
+
}
|
|
1173
|
+
return result;
|
|
1174
|
+
}
|
|
1175
|
+
async closeStream(streamId, info2) {
|
|
1176
|
+
await this.sendHead(streamId, FRAME_TYPE_V2.CLOSE, info2);
|
|
1177
|
+
this.dropStream(streamId);
|
|
1178
|
+
}
|
|
1179
|
+
async rstStream(streamId, code) {
|
|
1180
|
+
await this.enqueueSend(rstFrame(streamId, code));
|
|
1181
|
+
this.dropStream(streamId);
|
|
1182
|
+
}
|
|
1183
|
+
/** Serialize transport writes so frames never interleave mid-encode. */
|
|
1184
|
+
enqueueSend(frame) {
|
|
1185
|
+
const encoded = encodeFrameV2(frame);
|
|
1186
|
+
const next = this.sending.then(() => this.transport.send(encoded));
|
|
1187
|
+
this.sending = next.catch(() => {
|
|
1188
|
+
});
|
|
1189
|
+
return next;
|
|
1190
|
+
}
|
|
1191
|
+
// --- delivery ------------------------------------------------------------
|
|
1192
|
+
nextEvent(streamId) {
|
|
1193
|
+
const state = this.streams.get(streamId);
|
|
1194
|
+
if (!state) return Promise.resolve(null);
|
|
1195
|
+
const queued = state.events.shift();
|
|
1196
|
+
if (queued) {
|
|
1197
|
+
this.recordConsumption(streamId, queued);
|
|
1198
|
+
if (state.finished && state.events.length === 0) this.streams.delete(streamId);
|
|
1199
|
+
return Promise.resolve(queued);
|
|
1200
|
+
}
|
|
1201
|
+
if (state.finished) {
|
|
1202
|
+
this.streams.delete(streamId);
|
|
1203
|
+
return Promise.resolve(null);
|
|
1204
|
+
}
|
|
1205
|
+
return new Promise((resolve) => {
|
|
1206
|
+
state.waiter = (event) => {
|
|
1207
|
+
if (event) this.recordConsumption(streamId, event);
|
|
1208
|
+
resolve(event);
|
|
1209
|
+
};
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
recordConsumption(streamId, event) {
|
|
1213
|
+
if (event.type !== "data") return;
|
|
1214
|
+
const bytes = event.payload.length;
|
|
1215
|
+
const connectionCredit = this.connectionRecv.recordConsumed(bytes);
|
|
1216
|
+
if (connectionCredit > 0) {
|
|
1217
|
+
this.enqueueSend(windowUpdateFrame(CONNECTION_STREAM_ID, connectionCredit));
|
|
1218
|
+
}
|
|
1219
|
+
const state = this.streams.get(streamId);
|
|
1220
|
+
if (state) {
|
|
1221
|
+
const streamCredit = state.recvWindow.recordConsumed(bytes);
|
|
1222
|
+
if (streamCredit > 0) this.enqueueSend(windowUpdateFrame(streamId, streamCredit));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
deliver(streamId, event) {
|
|
1226
|
+
const state = this.streams.get(streamId);
|
|
1227
|
+
if (!state) return;
|
|
1228
|
+
if (state.waiter) {
|
|
1229
|
+
const waiter = state.waiter;
|
|
1230
|
+
state.waiter = null;
|
|
1231
|
+
waiter(event);
|
|
1232
|
+
} else {
|
|
1233
|
+
state.events.push(event);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
finishStream(streamId, event) {
|
|
1237
|
+
this.deliver(streamId, event);
|
|
1238
|
+
this.dropStream(streamId);
|
|
1239
|
+
}
|
|
1240
|
+
dropStream(streamId) {
|
|
1241
|
+
const state = this.streams.get(streamId);
|
|
1242
|
+
if (!state) return;
|
|
1243
|
+
state.finished = true;
|
|
1244
|
+
for (const pending of state.pending) {
|
|
1245
|
+
pending.reject(new SessionV2Error("stream closed"));
|
|
1246
|
+
}
|
|
1247
|
+
state.pending = [];
|
|
1248
|
+
const readyIndex = this.ready.indexOf(streamId);
|
|
1249
|
+
if (readyIndex !== -1) this.ready.splice(readyIndex, 1);
|
|
1250
|
+
if (state.waiter) {
|
|
1251
|
+
const waiter = state.waiter;
|
|
1252
|
+
state.waiter = null;
|
|
1253
|
+
this.streams.delete(streamId);
|
|
1254
|
+
waiter(null);
|
|
1255
|
+
} else if (state.events.length === 0) {
|
|
1256
|
+
this.streams.delete(streamId);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
fail(reason) {
|
|
1260
|
+
void this.sendHead(CONNECTION_STREAM_ID, FRAME_TYPE_V2.GOAWAY, {
|
|
1261
|
+
code: ERROR_CODE.PROTOCOL_ERROR,
|
|
1262
|
+
last_stream_id: this.nextStreamId,
|
|
1263
|
+
reason
|
|
1264
|
+
});
|
|
1265
|
+
this.teardown(reason);
|
|
1266
|
+
this.transport.close(1002, "protocol error");
|
|
1267
|
+
}
|
|
1268
|
+
teardown(reason) {
|
|
1269
|
+
if (this.closed) return;
|
|
1270
|
+
this.closed = true;
|
|
1271
|
+
for (const streamId of this.streams.keys()) {
|
|
1272
|
+
this.dropStream(streamId);
|
|
1273
|
+
}
|
|
1274
|
+
this.options.onClosed?.(reason);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
function parseJson(payload) {
|
|
1278
|
+
if (payload.length === 0) return null;
|
|
1279
|
+
try {
|
|
1280
|
+
return JSON.parse(textDecoder4.decode(payload));
|
|
1281
|
+
} catch {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/tunnel/v2/ws-transport.ts
|
|
1287
|
+
var WsTransportV2 = class {
|
|
1288
|
+
constructor(socket) {
|
|
1289
|
+
this.socket = socket;
|
|
1290
|
+
socket.binaryType = "arraybuffer";
|
|
1291
|
+
socket.addEventListener("message", (event) => {
|
|
1292
|
+
const data = event.data;
|
|
1293
|
+
if (data instanceof ArrayBuffer) {
|
|
1294
|
+
this.messageListener?.(new Uint8Array(data));
|
|
1295
|
+
} else if (data instanceof Uint8Array) {
|
|
1296
|
+
this.messageListener?.(data);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
socket.addEventListener(
|
|
1300
|
+
"close",
|
|
1301
|
+
(event) => this.closeListener?.(`connection closed (${event.code ?? "unknown"})`)
|
|
1302
|
+
);
|
|
1303
|
+
socket.addEventListener("error", () => this.closeListener?.("connection error"));
|
|
1304
|
+
}
|
|
1305
|
+
socket;
|
|
1306
|
+
messageListener = null;
|
|
1307
|
+
closeListener = null;
|
|
1308
|
+
send(frame) {
|
|
1309
|
+
this.socket.send(frame);
|
|
1310
|
+
return Promise.resolve();
|
|
1311
|
+
}
|
|
1312
|
+
onMessage(listener) {
|
|
1313
|
+
this.messageListener = listener;
|
|
1314
|
+
}
|
|
1315
|
+
onClose(listener) {
|
|
1316
|
+
this.closeListener = listener;
|
|
1317
|
+
}
|
|
1318
|
+
close(code, reason) {
|
|
1319
|
+
try {
|
|
1320
|
+
this.socket.close(code, reason);
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
// src/tunnel/negotiating-client.ts
|
|
1327
|
+
var NegotiatingAgentClient = class {
|
|
1328
|
+
constructor(options) {
|
|
1329
|
+
this.options = options;
|
|
1330
|
+
}
|
|
1331
|
+
options;
|
|
1332
|
+
socket = null;
|
|
1333
|
+
driver = null;
|
|
1334
|
+
closed = false;
|
|
1335
|
+
start() {
|
|
1336
|
+
const factory = this.options.createSocket ?? defaultSocketFactory;
|
|
1337
|
+
const socket = factory(connectUrl(this.options.url, this.options.token), [
|
|
1338
|
+
SUBPROTOCOL_V2,
|
|
1339
|
+
SUBPROTOCOL
|
|
1340
|
+
]);
|
|
1341
|
+
socket.binaryType = "arraybuffer";
|
|
1342
|
+
this.socket = socket;
|
|
1343
|
+
socket.addEventListener("open", () => {
|
|
1344
|
+
if (this.closed || this.driver) return;
|
|
1345
|
+
this.driver = socket.protocol === SUBPROTOCOL_V2 ? this.startV2(socket) : this.startV1(socket);
|
|
1346
|
+
});
|
|
1347
|
+
socket.addEventListener(
|
|
1348
|
+
"close",
|
|
1349
|
+
(event) => this.teardown(`connection closed (${event.code ?? "unknown"})`, event.code)
|
|
1350
|
+
);
|
|
1351
|
+
socket.addEventListener("error", () => this.teardown("connection error"));
|
|
1352
|
+
}
|
|
1353
|
+
stop() {
|
|
1354
|
+
if (this.closed) return;
|
|
1355
|
+
this.closed = true;
|
|
1356
|
+
this.driver?.stop();
|
|
1357
|
+
this.driver = null;
|
|
1358
|
+
try {
|
|
1359
|
+
this.socket?.close(1e3);
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
this.socket = null;
|
|
1363
|
+
}
|
|
1364
|
+
teardown(reason, closeCode) {
|
|
1365
|
+
if (this.closed) return;
|
|
1366
|
+
this.closed = true;
|
|
1367
|
+
this.driver?.stop();
|
|
1368
|
+
this.driver = null;
|
|
1369
|
+
this.socket = null;
|
|
1370
|
+
this.options.events?.onClosed?.(reason, closeCode);
|
|
1371
|
+
}
|
|
1372
|
+
startV2(socket) {
|
|
1373
|
+
const hello = defaultHello();
|
|
1374
|
+
const transport = new WsTransportV2(socket);
|
|
1375
|
+
const createLocalSocket = this.options.createLocalSocket ?? defaultSocketFactory;
|
|
1376
|
+
const dispatcher = new AgentDispatcher().register(new HttpProxyKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(new WsSpliceKind(makeAgentContext(this.options.localPort, hello, createLocalSocket)));
|
|
1377
|
+
const session = new SessionV2(transport, {
|
|
1378
|
+
role: "agent",
|
|
1379
|
+
local: hello,
|
|
1380
|
+
onIncoming: (incoming) => dispatcher.dispatch(incoming),
|
|
1381
|
+
onClosed: (reason) => this.teardown(`v2 session closed \u2014 ${reason}`)
|
|
1382
|
+
});
|
|
1383
|
+
void session.start();
|
|
1384
|
+
this.options.events?.onConnected?.();
|
|
1385
|
+
return { stop: () => transport.close(1e3, "stopped") };
|
|
1386
|
+
}
|
|
1387
|
+
startV1(socket) {
|
|
1388
|
+
const client = new TunnelClient({
|
|
1389
|
+
url: this.options.url,
|
|
1390
|
+
token: this.options.token,
|
|
1391
|
+
localPort: this.options.localPort,
|
|
1392
|
+
createSocket: () => socket,
|
|
1393
|
+
createLocalSocket: this.options.createLocalSocket,
|
|
1394
|
+
events: this.options.events,
|
|
1395
|
+
socketAlreadyOpen: true
|
|
1396
|
+
});
|
|
1397
|
+
client.start();
|
|
1398
|
+
return { stop: () => client.stop() };
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
function makeAgentContext(localPort, hello, createLocalSocket) {
|
|
1402
|
+
return {
|
|
1403
|
+
localPort,
|
|
1404
|
+
maxFramePayload: hello.max_frame_payload,
|
|
1405
|
+
createLocalSocket: (url, protocols) => createLocalSocket(url, protocols)
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
var TUNNEL_URL_ENV = "CIMPLIFY_TUNNEL_URL";
|
|
1409
|
+
var TUNNEL_TOKEN_ENV = "CIMPLIFY_TUNNEL_TOKEN";
|
|
1410
|
+
var TUNNEL_LOCAL_PORT_ENV = "CIMPLIFY_TUNNEL_LOCAL_PORT";
|
|
1411
|
+
var TUNNEL_CONFIG_DIR = ".cimplify";
|
|
1412
|
+
var TUNNEL_CONFIG_FILE = "tunnel.json";
|
|
1413
|
+
var DEFAULT_DEV_SERVER_PORT = 3e3;
|
|
1414
|
+
function tunnelConfigPath(cwd) {
|
|
1415
|
+
return path2.join(cwd, TUNNEL_CONFIG_DIR, TUNNEL_CONFIG_FILE);
|
|
1416
|
+
}
|
|
1417
|
+
async function readTunnelConfig(cwd, env = process.env) {
|
|
1418
|
+
const url = env[TUNNEL_URL_ENV];
|
|
1419
|
+
const token = env[TUNNEL_TOKEN_ENV];
|
|
1420
|
+
if (url && token) {
|
|
1421
|
+
return { url, token, localPort: envPort(env) ?? DEFAULT_DEV_SERVER_PORT };
|
|
1422
|
+
}
|
|
1423
|
+
return readTunnelConfigFile(cwd);
|
|
1424
|
+
}
|
|
1425
|
+
function envPort(env) {
|
|
1426
|
+
const raw = env[TUNNEL_LOCAL_PORT_ENV];
|
|
1427
|
+
if (!raw) return null;
|
|
1428
|
+
const port = Number.parseInt(raw, 10);
|
|
1429
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
|
|
1430
|
+
}
|
|
1431
|
+
async function readTunnelConfigFile(cwd) {
|
|
1432
|
+
let raw;
|
|
1433
|
+
try {
|
|
1434
|
+
raw = await promises.readFile(tunnelConfigPath(cwd), "utf8");
|
|
1435
|
+
} catch {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
let parsed;
|
|
1439
|
+
try {
|
|
1440
|
+
parsed = JSON.parse(raw);
|
|
1441
|
+
} catch {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1445
|
+
const record = parsed;
|
|
1446
|
+
if (typeof record.url !== "string" || record.url.length === 0) return null;
|
|
1447
|
+
if (typeof record.token !== "string" || record.token.length === 0) return null;
|
|
1448
|
+
const localPort = typeof record.port === "number" && Number.isInteger(record.port) && record.port > 0 ? record.port : DEFAULT_DEV_SERVER_PORT;
|
|
1449
|
+
return { url: record.url, token: record.token, localPort };
|
|
1450
|
+
}
|
|
1451
|
+
function sameTunnelConfig(a, b) {
|
|
1452
|
+
if (a === null || b === null) return a === b;
|
|
1453
|
+
return a.url === b.url && a.token === b.token && a.localPort === b.localPort;
|
|
1454
|
+
}
|
|
1455
|
+
function sameTunnelEndpoint(a, b) {
|
|
1456
|
+
return a.url === b.url && a.localPort === b.localPort;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/tunnel/manager.ts
|
|
1460
|
+
var CONFIG_POLL_MS = 2e3;
|
|
1461
|
+
var RECONNECT_MIN_MS = 1e3;
|
|
1462
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
1463
|
+
var CLOSE_CODE_REPLACED = 4e3;
|
|
1464
|
+
function defaultCreateClient(config, events) {
|
|
1465
|
+
return new NegotiatingAgentClient({
|
|
1466
|
+
url: config.url,
|
|
1467
|
+
token: config.token,
|
|
1468
|
+
localPort: config.localPort,
|
|
1469
|
+
events
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
function startTunnelRuntime(cwd, options = {}) {
|
|
1473
|
+
const pollIntervalMs = options.pollIntervalMs ?? CONFIG_POLL_MS;
|
|
1474
|
+
const createClient = options.createClient ?? defaultCreateClient;
|
|
1475
|
+
const onStatus = options.onStatus ?? ((message) => info(dim(message)));
|
|
1476
|
+
let stopped = false;
|
|
1477
|
+
let generation = 0;
|
|
1478
|
+
let activeConfig = null;
|
|
1479
|
+
let activeClient = null;
|
|
1480
|
+
let reconnectDelay = RECONNECT_MIN_MS;
|
|
1481
|
+
let reconnectTimer = null;
|
|
1482
|
+
let polling = false;
|
|
1483
|
+
let connected = false;
|
|
1484
|
+
let standDown = false;
|
|
1485
|
+
const clearReconnect = () => {
|
|
1486
|
+
if (reconnectTimer !== null) {
|
|
1487
|
+
clearTimeout(reconnectTimer);
|
|
1488
|
+
reconnectTimer = null;
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
const startClient = (config) => {
|
|
1492
|
+
generation += 1;
|
|
1493
|
+
standDown = false;
|
|
1494
|
+
const myGeneration = generation;
|
|
1495
|
+
const client = createClient(config, {
|
|
1496
|
+
onConnected: () => {
|
|
1497
|
+
if (stopped || myGeneration !== generation) return;
|
|
1498
|
+
connected = true;
|
|
1499
|
+
reconnectDelay = RECONNECT_MIN_MS;
|
|
1500
|
+
onStatus("(preview tunnel: connected)");
|
|
1501
|
+
},
|
|
1502
|
+
onClosed: (reason, closeCode) => {
|
|
1503
|
+
if (stopped || myGeneration !== generation) return;
|
|
1504
|
+
connected = false;
|
|
1505
|
+
if (closeCode === CLOSE_CODE_REPLACED) {
|
|
1506
|
+
standDown = true;
|
|
1507
|
+
onStatus("(preview tunnel: replaced by a newer agent; standing down)");
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
onStatus(`(preview tunnel: disconnected \u2014 ${reason})`);
|
|
1511
|
+
scheduleReconnect();
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
activeConfig = config;
|
|
1515
|
+
activeClient = client;
|
|
1516
|
+
client.start();
|
|
1517
|
+
};
|
|
1518
|
+
const retireClient = () => {
|
|
1519
|
+
generation += 1;
|
|
1520
|
+
activeClient?.stop();
|
|
1521
|
+
activeClient = null;
|
|
1522
|
+
};
|
|
1523
|
+
const scheduleReconnect = () => {
|
|
1524
|
+
if (reconnectTimer !== null) return;
|
|
1525
|
+
const delay = reconnectDelay + Math.floor(Math.random() * (reconnectDelay / 4));
|
|
1526
|
+
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
1527
|
+
reconnectTimer = setTimeout(() => {
|
|
1528
|
+
reconnectTimer = null;
|
|
1529
|
+
if (stopped || activeConfig === null) return;
|
|
1530
|
+
retireClient();
|
|
1531
|
+
startClient(activeConfig);
|
|
1532
|
+
}, delay);
|
|
1533
|
+
};
|
|
1534
|
+
const stopClient = () => {
|
|
1535
|
+
clearReconnect();
|
|
1536
|
+
retireClient();
|
|
1537
|
+
activeConfig = null;
|
|
1538
|
+
connected = false;
|
|
1539
|
+
};
|
|
1540
|
+
const poll = async () => {
|
|
1541
|
+
if (polling || stopped) return;
|
|
1542
|
+
polling = true;
|
|
1543
|
+
try {
|
|
1544
|
+
const config = await readTunnelConfig(cwd, options.env);
|
|
1545
|
+
if (stopped || sameTunnelConfig(config, activeConfig)) return;
|
|
1546
|
+
if (config === null) {
|
|
1547
|
+
stopClient();
|
|
1548
|
+
onStatus("(preview tunnel: credentials removed, stopped)");
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if (activeConfig && sameTunnelEndpoint(config, activeConfig) && connected && !standDown) {
|
|
1552
|
+
activeConfig = config;
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
stopClient();
|
|
1556
|
+
onStatus("(preview tunnel: credentials found, connecting)");
|
|
1557
|
+
reconnectDelay = RECONNECT_MIN_MS;
|
|
1558
|
+
startClient(config);
|
|
1559
|
+
} finally {
|
|
1560
|
+
polling = false;
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
const interval = setInterval(() => {
|
|
1564
|
+
void poll();
|
|
1565
|
+
}, pollIntervalMs);
|
|
1566
|
+
void poll();
|
|
1567
|
+
return () => {
|
|
1568
|
+
if (stopped) return;
|
|
1569
|
+
stopped = true;
|
|
1570
|
+
clearInterval(interval);
|
|
1571
|
+
stopClient();
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/commands/dev.ts
|
|
1576
|
+
var FLAG_REMOTE = "remote";
|
|
1577
|
+
var PACKAGE_JSON = "package.json";
|
|
1578
|
+
var ENV_LOCAL_FILE = ".env.local";
|
|
1579
|
+
var PUBLIC_API_URL_KEY = "NEXT_PUBLIC_CIMPLIFY_API_URL";
|
|
1580
|
+
var SCRIPT_DEV = "dev";
|
|
1581
|
+
var PREVIEW_ORIGIN_ENV = "CIMPLIFY_PREVIEW_ORIGIN";
|
|
1582
|
+
async function applyPreviewOrigin(cwd, originList) {
|
|
1583
|
+
const { ensurePreviewOrigin } = await import('./preview-config-344CUZLO.mjs');
|
|
1584
|
+
const origins = originList.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
1585
|
+
if (origins.length === 0) return;
|
|
1586
|
+
try {
|
|
1587
|
+
const result = await ensurePreviewOrigin(cwd, origins);
|
|
1588
|
+
if (result.status === "unrecognized") {
|
|
1589
|
+
info(
|
|
1590
|
+
dim(
|
|
1591
|
+
`(preview: could not resolve ${result.file} config shape; hot-reload may be limited)`
|
|
1592
|
+
)
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
info(dim(`(preview: dev-origin setup skipped: ${err.message})`));
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
var PM_BUN = "bun";
|
|
1600
|
+
var PM_PNPM = "pnpm";
|
|
1601
|
+
var PM_YARN = "yarn";
|
|
1602
|
+
var PM_NPM = "npm";
|
|
1603
|
+
var LOCKFILE_BUN = "bun.lock";
|
|
1604
|
+
var LOCKFILE_BUN_BINARY = "bun.lockb";
|
|
1605
|
+
var LOCKFILE_PNPM = "pnpm-lock.yaml";
|
|
1606
|
+
var LOCKFILE_YARN = "yarn.lock";
|
|
1607
|
+
var BUNFIG = "bunfig.toml";
|
|
1608
|
+
function envEndpoint(businessId, projectId) {
|
|
1609
|
+
return `/v1/businesses/${encodeURIComponent(businessId)}/projects/${encodeURIComponent(projectId)}/env-vars`;
|
|
1610
|
+
}
|
|
1611
|
+
async function detectPackageManager(cwd) {
|
|
1612
|
+
if (await pathExists(path2.join(cwd, LOCKFILE_BUN))) return PM_BUN;
|
|
1613
|
+
if (await pathExists(path2.join(cwd, LOCKFILE_BUN_BINARY))) return PM_BUN;
|
|
1614
|
+
if (await pathExists(path2.join(cwd, BUNFIG))) return PM_BUN;
|
|
1615
|
+
if (await pathExists(path2.join(cwd, LOCKFILE_PNPM))) return PM_PNPM;
|
|
1616
|
+
if (await pathExists(path2.join(cwd, LOCKFILE_YARN))) return PM_YARN;
|
|
1617
|
+
return PM_NPM;
|
|
1618
|
+
}
|
|
1619
|
+
async function pathExists(target) {
|
|
1620
|
+
try {
|
|
1621
|
+
await promises.stat(target);
|
|
1622
|
+
return true;
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
if (err.code === "ENOENT") return false;
|
|
1625
|
+
throw err;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async function readPackageJson(cwd) {
|
|
1629
|
+
const pkgPath = path2.join(cwd, PACKAGE_JSON);
|
|
1630
|
+
let raw;
|
|
1631
|
+
try {
|
|
1632
|
+
raw = await promises.readFile(pkgPath, "utf8");
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
if (err.code === "ENOENT") {
|
|
1635
|
+
throw new CliError(
|
|
1636
|
+
CLI_ERROR_CODE.INVALID_INPUT,
|
|
1637
|
+
`No package.json in ${cwd}. \`cimplify dev\` runs your project's dev script.`
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
throw err;
|
|
1641
|
+
}
|
|
1642
|
+
return JSON.parse(raw);
|
|
1643
|
+
}
|
|
1644
|
+
function mergeEnvEntries(existing, incoming) {
|
|
1645
|
+
const seen = new Set(existing.map((e) => e.key));
|
|
1646
|
+
const merged = [...existing];
|
|
1647
|
+
for (const entry of incoming) {
|
|
1648
|
+
if (seen.has(entry.key)) continue;
|
|
1649
|
+
merged.push(entry);
|
|
1650
|
+
seen.add(entry.key);
|
|
1651
|
+
}
|
|
1652
|
+
return merged;
|
|
1653
|
+
}
|
|
1654
|
+
async function applyRemoteEnv(client, businessId, projectId, apiBaseUrl, cwd) {
|
|
1655
|
+
const records = await client.get(envEndpoint(businessId, projectId), {
|
|
1656
|
+
query: { scope: ENV_SCOPE.PRODUCTION }
|
|
1657
|
+
});
|
|
1658
|
+
const incoming = records.filter((r) => r.value !== null && !r.is_secret).map((r) => ({ key: r.key, value: r.value }));
|
|
1659
|
+
incoming.push({ key: PUBLIC_API_URL_KEY, value: apiBaseUrl });
|
|
1660
|
+
const envPath = path2.join(cwd, ENV_LOCAL_FILE);
|
|
1661
|
+
let existing = [];
|
|
1662
|
+
try {
|
|
1663
|
+
const raw = await promises.readFile(envPath, "utf8");
|
|
1664
|
+
existing = parseEnvFile(raw);
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
if (err.code !== "ENOENT") throw err;
|
|
1667
|
+
}
|
|
1668
|
+
const merged = mergeEnvEntries(existing, incoming);
|
|
1669
|
+
await promises.writeFile(envPath, formatEnvFile(merged), "utf8");
|
|
1670
|
+
const publicCount = incoming.filter((e) => e.key.startsWith(PUBLIC_ENV_PREFIX)).length;
|
|
1671
|
+
return publicCount;
|
|
1672
|
+
}
|
|
1673
|
+
async function run(argv) {
|
|
1674
|
+
const args = parseArgs(argv);
|
|
1675
|
+
const remote = flagBool(args, FLAG_REMOTE);
|
|
1676
|
+
const cwd = process.cwd();
|
|
1677
|
+
const previewOrigin = process.env[PREVIEW_ORIGIN_ENV];
|
|
1678
|
+
if (remote) {
|
|
1679
|
+
const auth = await readAuth();
|
|
1680
|
+
const link = await readProjectLink();
|
|
1681
|
+
const client = ApiClient.fromAuth(auth);
|
|
1682
|
+
const publicCount = await applyRemoteEnv(
|
|
1683
|
+
client,
|
|
1684
|
+
link.businessId,
|
|
1685
|
+
link.projectId,
|
|
1686
|
+
auth.apiBaseUrl,
|
|
1687
|
+
cwd
|
|
1688
|
+
);
|
|
1689
|
+
const label = link.businessHandle ?? auth.businessHandle ?? auth.businessId;
|
|
1690
|
+
info(`Dev mode: connected to ${label} production data (${publicCount} public var${publicCount === 1 ? "" : "s"} merged)`);
|
|
1691
|
+
} else if (previewOrigin) {
|
|
1692
|
+
info("Dev mode: hosted preview");
|
|
1693
|
+
} else {
|
|
1694
|
+
info("Dev mode: using local mock (`cimplify mock`)");
|
|
1695
|
+
}
|
|
1696
|
+
const pkg = await readPackageJson(cwd);
|
|
1697
|
+
if (!pkg.scripts || !pkg.scripts[SCRIPT_DEV]) {
|
|
1698
|
+
throw new CliError(
|
|
1699
|
+
CLI_ERROR_CODE.INVALID_INPUT,
|
|
1700
|
+
`No "${SCRIPT_DEV}" script in package.json. Add one (e.g. "next dev") and re-run.`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
let stopTunnel = null;
|
|
1704
|
+
if (previewOrigin) {
|
|
1705
|
+
await applyPreviewOrigin(cwd, previewOrigin);
|
|
1706
|
+
stopTunnel = startTunnelRuntime(cwd);
|
|
1707
|
+
}
|
|
1708
|
+
const pm = await detectPackageManager(cwd);
|
|
1709
|
+
info(dim(`(running: ${pm} run ${SCRIPT_DEV})`));
|
|
1710
|
+
const child = spawn(pm, ["run", SCRIPT_DEV], { cwd, stdio: "inherit" });
|
|
1711
|
+
try {
|
|
1712
|
+
await new Promise((resolve, reject) => {
|
|
1713
|
+
child.on("error", (err) => reject(new CliError(CLI_ERROR_CODE.INVALID_INPUT, `Failed to spawn ${pm}: ${err.message}`)));
|
|
1714
|
+
child.on("close", (code) => {
|
|
1715
|
+
process.exitCode = code ?? 0;
|
|
1716
|
+
resolve();
|
|
1717
|
+
});
|
|
1718
|
+
});
|
|
1719
|
+
} finally {
|
|
1720
|
+
stopTunnel?.();
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
export { run as default, detectPackageManager, mergeEnvEntries };
|