@cimplify/cli 0.13.3 → 0.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/{add-F7GKDXAD.mjs → add-NZ47GUMD.mjs} +4 -5
- package/dist/{assets-Y4JE6MSG.mjs → assets-OEY6FHHZ.mjs} +4 -5
- package/dist/{auth-step-up-EHL5PM2L.mjs → auth-step-up-ALEUQUCE.mjs} +5 -6
- package/dist/{cancel-QSZE2XC2.mjs → cancel-FD66LTTS.mjs} +4 -5
- package/dist/chunk-4O4G2CDE.mjs +162 -0
- package/dist/{chunk-C4M3DXKC.mjs → chunk-5KYR6WJB.mjs} +10 -2
- package/dist/{chunk-QKDCMUWP.mjs → chunk-5YVMA7A5.mjs} +3 -2
- package/dist/{chunk-4YIFBYBV.mjs → chunk-5ZQURRIM.mjs} +1 -2
- package/dist/{chunk-QA36VSLX.mjs → chunk-6CHBU7O4.mjs} +1 -1
- package/dist/{chunk-SVJPU4K7.mjs → chunk-BDC7XQIL.mjs} +51 -43
- package/dist/{chunk-72VM62IN.mjs → chunk-GOTWRZVM.mjs} +6 -6
- package/dist/{chunk-UP7N53C4.mjs → chunk-IK6NQHTG.mjs} +1 -1
- package/dist/{chunk-OVHZV57L.mjs → chunk-MLVBE5V2.mjs} +1 -1
- package/dist/{chunk-RR4NOZGY.mjs → chunk-OE7YRDLH.mjs} +5 -6
- package/dist/{chunk-VJJDGHZC.mjs → chunk-PDGKBATF.mjs} +1 -2
- package/dist/{chunk-5U5OXOPN.mjs → chunk-RU53TQLZ.mjs} +1 -1
- package/dist/{chunk-JPXK5BLA.mjs → chunk-TOQGLBWF.mjs} +1 -1
- package/dist/{deploy-UTAYLZOM.mjs → deploy-SLRFPGTH.mjs} +8 -9
- package/dist/{dev-SYFBDJSY.mjs → dev-L3W6UXYN.mjs} +19 -26
- package/dist/dispatcher.mjs +41 -33
- package/dist/{doctor-QPCWLW4L.mjs → doctor-6NI2XNTD.mjs} +8 -9
- package/dist/{domains-OS3DXSDN.mjs → domains-7D7WJAMC.mjs} +5 -6
- package/dist/{env-UVO4OIEO.mjs → env-PAENNABF.mjs} +6 -7
- package/dist/{explain-XB67PYQQ.mjs → explain-HPRJBFHS.mjs} +3 -4
- package/dist/{inspect-ULWQHT7J.mjs → inspect-MGVG4Z44.mjs} +3 -4
- package/dist/introspect-3OYRCVUF.mjs +8 -0
- package/dist/{link-VQN2WPZO.mjs → link-KGNQWCSZ.mjs} +4 -5
- package/dist/lint-EYKIQ6RO.mjs +1331 -0
- package/dist/{list-DZTPWNU3.mjs → list-QNEUCCP5.mjs} +3 -4
- package/dist/{login-UZ5Y7GIO.mjs → login-NCO4P5G5.mjs} +5 -6
- package/dist/{logout-XYWZR6QJ.mjs → logout-MF6SOLPE.mjs} +2 -3
- package/dist/{logs-YNXDXAF7.mjs → logs-76TCJBIQ.mjs} +4 -5
- package/dist/{mock-HFVAAYK2.mjs → mock-CAWNXSGC.mjs} +2 -3
- package/dist/{projects-RJ44DJKZ.mjs → projects-U75DOOR7.mjs} +4 -5
- package/dist/repo-IE5IOLR5.mjs +8 -0
- package/dist/{rollback-Z2PRL7FS.mjs → rollback-ZL3V7SYH.mjs} +5 -6
- package/dist/{status-RR3GGTVU.mjs → status-52HG63FE.mjs} +4 -5
- package/dist/{unlink-JBVZYOA3.mjs → unlink-TCG3NBAR.mjs} +2 -3
- package/dist/{update-B5HANRBK.mjs → update-47HG7LN7.mjs} +4 -5
- package/dist/{whoami-5G32NNEP.mjs → whoami-B5Y5KSOZ.mjs} +3 -4
- package/package.json +3 -2
- package/templates/storefront-auto/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-auto/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-auto/app/page.tsx +4 -1
- package/templates/storefront-auto/app/products/[slug]/page.tsx +1 -0
- package/templates/storefront-auto/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-bakery/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-bakery/app/page.tsx +1 -1
- package/templates/storefront-bakery/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-fashion/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-fashion/app/page.tsx +1 -1
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +1 -0
- package/templates/storefront-fashion/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-grocery/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-grocery/app/page.tsx +1 -1
- package/templates/storefront-grocery/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-pharmacy/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-pharmacy/app/globals.css +11 -3
- package/templates/storefront-pharmacy/app/page.tsx +4 -1
- package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +1 -0
- package/templates/storefront-pharmacy/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-pharmacy/components/pharmacy-hero.tsx +10 -3
- package/templates/storefront-pharmacy/components/urgent-ctas.tsx +30 -5
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-restaurant/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-restaurant/app/page.tsx +1 -1
- package/templates/storefront-restaurant/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-retail/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-retail/app/page.tsx +1 -1
- package/templates/storefront-retail/app/products/[slug]/page.tsx +1 -0
- package/templates/storefront-retail/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/app/book/book-client.tsx +3 -5
- package/templates/storefront-services/app/categories/[slug]/page.tsx +1 -0
- package/templates/storefront-services/app/collections/[slug]/page.tsx +1 -0
- package/templates/storefront-services/app/page.tsx +1 -1
- package/templates/storefront-services/components/auth-error-toast.tsx +2 -2
- package/templates/storefront-services/package.json +1 -1
- package/dist/agent-CEYZNYXA.mjs +0 -25
- package/dist/chunk-KNTFZ6LD.mjs +0 -2090
- package/dist/chunk-YPC4VU65.mjs +0 -76
- package/dist/chunk-ZPVN6UXL.mjs +0 -74
- package/dist/introspect-D4473WPV.mjs +0 -9
- package/dist/repo-OK6CVG3G.mjs +0 -9
package/dist/chunk-KNTFZ6LD.mjs
DELETED
|
@@ -1,2090 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { info, dim } from './chunk-ZPVN6UXL.mjs';
|
|
3
|
-
import { promises, watch } from 'fs';
|
|
4
|
-
import path2 from 'path';
|
|
5
|
-
import http from 'http';
|
|
6
|
-
import { spawn } from 'child_process';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
8
|
-
|
|
9
|
-
var TUNNEL_URL_ENV = "CIMPLIFY_TUNNEL_URL";
|
|
10
|
-
var TUNNEL_TOKEN_ENV = "CIMPLIFY_TUNNEL_TOKEN";
|
|
11
|
-
var TUNNEL_LOCAL_PORT_ENV = "CIMPLIFY_TUNNEL_LOCAL_PORT";
|
|
12
|
-
var TUNNEL_CONFIG_DIR = ".cimplify";
|
|
13
|
-
var TUNNEL_CONFIG_FILE = "tunnel.json";
|
|
14
|
-
var DEFAULT_DEV_SERVER_PORT = 3e3;
|
|
15
|
-
function tunnelConfigPath(cwd) {
|
|
16
|
-
return path2.join(cwd, TUNNEL_CONFIG_DIR, TUNNEL_CONFIG_FILE);
|
|
17
|
-
}
|
|
18
|
-
async function readTunnelConfig(cwd, env = process.env) {
|
|
19
|
-
const url = env[TUNNEL_URL_ENV];
|
|
20
|
-
const token = env[TUNNEL_TOKEN_ENV];
|
|
21
|
-
if (url && token) {
|
|
22
|
-
return { url, token, localPort: envPort(env) ?? DEFAULT_DEV_SERVER_PORT };
|
|
23
|
-
}
|
|
24
|
-
return readTunnelConfigFile(cwd);
|
|
25
|
-
}
|
|
26
|
-
function envPort(env) {
|
|
27
|
-
const raw = env[TUNNEL_LOCAL_PORT_ENV];
|
|
28
|
-
if (!raw) return null;
|
|
29
|
-
const port = Number.parseInt(raw, 10);
|
|
30
|
-
return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
|
|
31
|
-
}
|
|
32
|
-
async function readTunnelConfigFile(cwd) {
|
|
33
|
-
let raw;
|
|
34
|
-
try {
|
|
35
|
-
raw = await promises.readFile(tunnelConfigPath(cwd), "utf8");
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
let parsed;
|
|
40
|
-
try {
|
|
41
|
-
parsed = JSON.parse(raw);
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
46
|
-
const record = parsed;
|
|
47
|
-
if (typeof record.url !== "string" || record.url.length === 0) return null;
|
|
48
|
-
if (typeof record.token !== "string" || record.token.length === 0) return null;
|
|
49
|
-
const localPort = typeof record.port === "number" && Number.isInteger(record.port) && record.port > 0 ? record.port : DEFAULT_DEV_SERVER_PORT;
|
|
50
|
-
return { url: record.url, token: record.token, localPort };
|
|
51
|
-
}
|
|
52
|
-
function sameTunnelConfig(a, b) {
|
|
53
|
-
if (a === null || b === null) return a === b;
|
|
54
|
-
return a.url === b.url && a.token === b.token && a.localPort === b.localPort;
|
|
55
|
-
}
|
|
56
|
-
function sameTunnelEndpoint(a, b) {
|
|
57
|
-
return a.url === b.url && a.localPort === b.localPort;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/tunnel/v2/codec.ts
|
|
61
|
-
var HEADER_LEN = 9;
|
|
62
|
-
var SUBPROTOCOL_V2 = "cimplify-tunnel.v2";
|
|
63
|
-
var DEFAULT_MAX_FRAME_PAYLOAD = 65536;
|
|
64
|
-
var DEFAULT_INITIAL_STREAM_WINDOW = 262144;
|
|
65
|
-
var DEFAULT_CONNECTION_WINDOW = 4194304;
|
|
66
|
-
var DEFAULT_MAX_CONCURRENT_STREAMS = 256;
|
|
67
|
-
var MAX_HEAD_BYTES = 65536;
|
|
68
|
-
var FRAME_TYPE_V2 = {
|
|
69
|
-
HELLO: 1,
|
|
70
|
-
OPEN: 2,
|
|
71
|
-
ACCEPT: 3,
|
|
72
|
-
DATA: 4,
|
|
73
|
-
END: 5,
|
|
74
|
-
CLOSE: 6,
|
|
75
|
-
RST: 7,
|
|
76
|
-
WINDOW_UPDATE: 8,
|
|
77
|
-
PING: 9,
|
|
78
|
-
PONG: 10,
|
|
79
|
-
GOAWAY: 11
|
|
80
|
-
};
|
|
81
|
-
var ERROR_CODE = {
|
|
82
|
-
PROTOCOL_ERROR: 1,
|
|
83
|
-
FLOW_CONTROL_ERROR: 3,
|
|
84
|
-
STREAM_CLOSED: 4,
|
|
85
|
-
REFUSED: 5,
|
|
86
|
-
UNSUPPORTED: 6};
|
|
87
|
-
var WS_DATA_PREFIX = {
|
|
88
|
-
TEXT: 0,
|
|
89
|
-
BINARY: 1,
|
|
90
|
-
TEXT_CONT: 2,
|
|
91
|
-
BINARY_CONT: 3
|
|
92
|
-
};
|
|
93
|
-
var CodecV2Error = class extends Error {
|
|
94
|
-
};
|
|
95
|
-
function encodeFrameV2(frame) {
|
|
96
|
-
const bytes = new Uint8Array(HEADER_LEN + frame.payload.length);
|
|
97
|
-
const view = new DataView(bytes.buffer);
|
|
98
|
-
view.setUint32(0, frame.streamId);
|
|
99
|
-
view.setUint8(4, frame.frameType);
|
|
100
|
-
view.setUint32(5, frame.payload.length);
|
|
101
|
-
bytes.set(frame.payload, HEADER_LEN);
|
|
102
|
-
return bytes;
|
|
103
|
-
}
|
|
104
|
-
function decodeFrameV2(bytes, maxPayload = DEFAULT_MAX_FRAME_PAYLOAD) {
|
|
105
|
-
if (bytes.length < HEADER_LEN) {
|
|
106
|
-
throw new CodecV2Error(`frame truncated: ${bytes.length} bytes`);
|
|
107
|
-
}
|
|
108
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
109
|
-
const streamId = view.getUint32(0);
|
|
110
|
-
const frameType = view.getUint8(4);
|
|
111
|
-
const declared = view.getUint32(5);
|
|
112
|
-
if (declared > maxPayload) {
|
|
113
|
-
throw new CodecV2Error(`payload length ${declared} exceeds negotiated max ${maxPayload}`);
|
|
114
|
-
}
|
|
115
|
-
const actual = bytes.length - HEADER_LEN;
|
|
116
|
-
if (declared !== actual) {
|
|
117
|
-
throw new CodecV2Error(`payload length field ${declared} does not match remaining ${actual} bytes`);
|
|
118
|
-
}
|
|
119
|
-
return { streamId, frameType, payload: bytes.subarray(HEADER_LEN) };
|
|
120
|
-
}
|
|
121
|
-
function payloadU32(frame) {
|
|
122
|
-
if (frame.payload.length !== 4) return null;
|
|
123
|
-
return new DataView(frame.payload.buffer, frame.payload.byteOffset, 4).getUint32(0);
|
|
124
|
-
}
|
|
125
|
-
function rstFrame(streamId, code) {
|
|
126
|
-
const payload = new Uint8Array(4);
|
|
127
|
-
new DataView(payload.buffer).setUint32(0, code);
|
|
128
|
-
return { streamId, frameType: FRAME_TYPE_V2.RST, payload };
|
|
129
|
-
}
|
|
130
|
-
function windowUpdateFrame(streamId, credit) {
|
|
131
|
-
const payload = new Uint8Array(4);
|
|
132
|
-
new DataView(payload.buffer).setUint32(0, credit);
|
|
133
|
-
return { streamId, frameType: FRAME_TYPE_V2.WINDOW_UPDATE, payload };
|
|
134
|
-
}
|
|
135
|
-
function defaultHello(build) {
|
|
136
|
-
return {
|
|
137
|
-
version: 2,
|
|
138
|
-
initial_stream_window: DEFAULT_INITIAL_STREAM_WINDOW,
|
|
139
|
-
connection_window: DEFAULT_CONNECTION_WINDOW,
|
|
140
|
-
max_frame_payload: DEFAULT_MAX_FRAME_PAYLOAD,
|
|
141
|
-
max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
142
|
-
capabilities: ["http", "ws", "control"],
|
|
143
|
-
...{}
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function parseHello(payload) {
|
|
147
|
-
const raw = JSON.parse(new TextDecoder().decode(payload));
|
|
148
|
-
if (typeof raw.version !== "number") {
|
|
149
|
-
throw new CodecV2Error("HELLO missing version");
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
version: raw.version,
|
|
153
|
-
initial_stream_window: raw.initial_stream_window ?? DEFAULT_INITIAL_STREAM_WINDOW,
|
|
154
|
-
connection_window: raw.connection_window ?? DEFAULT_CONNECTION_WINDOW,
|
|
155
|
-
max_frame_payload: raw.max_frame_payload ?? DEFAULT_MAX_FRAME_PAYLOAD,
|
|
156
|
-
max_concurrent_streams: raw.max_concurrent_streams ?? DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
157
|
-
capabilities: raw.capabilities ?? [],
|
|
158
|
-
...raw.build ? { build: raw.build } : {}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// src/tunnel/v2/ws-framing.ts
|
|
163
|
-
var textEncoder = new TextEncoder();
|
|
164
|
-
var textDecoder = new TextDecoder();
|
|
165
|
-
function encodeWsMessage(message, maxPayload) {
|
|
166
|
-
const body = message.type === "text" ? textEncoder.encode(message.data) : message.data;
|
|
167
|
-
const contPrefix = message.type === "text" ? WS_DATA_PREFIX.TEXT_CONT : WS_DATA_PREFIX.BINARY_CONT;
|
|
168
|
-
const finalPrefix = message.type === "text" ? WS_DATA_PREFIX.TEXT : WS_DATA_PREFIX.BINARY;
|
|
169
|
-
const chunkSize = Math.max(1, maxPayload - 1);
|
|
170
|
-
const frames = [];
|
|
171
|
-
if (body.length === 0) {
|
|
172
|
-
return [Uint8Array.of(finalPrefix)];
|
|
173
|
-
}
|
|
174
|
-
for (let offset = 0; offset < body.length; offset += chunkSize) {
|
|
175
|
-
const chunk = body.subarray(offset, offset + chunkSize);
|
|
176
|
-
const isLast = offset + chunkSize >= body.length;
|
|
177
|
-
const framed = new Uint8Array(chunk.length + 1);
|
|
178
|
-
framed[0] = isLast ? finalPrefix : contPrefix;
|
|
179
|
-
framed.set(chunk, 1);
|
|
180
|
-
frames.push(framed);
|
|
181
|
-
}
|
|
182
|
-
return frames;
|
|
183
|
-
}
|
|
184
|
-
var WsReassembler = class {
|
|
185
|
-
buffer = [];
|
|
186
|
-
bufferedType = null;
|
|
187
|
-
/** Feed one DATA payload; returns a completed message, or null mid-fragment. */
|
|
188
|
-
push(payload) {
|
|
189
|
-
if (payload.length === 0) return null;
|
|
190
|
-
const prefix = payload[0];
|
|
191
|
-
const chunk = payload.subarray(1);
|
|
192
|
-
switch (prefix) {
|
|
193
|
-
case WS_DATA_PREFIX.TEXT_CONT:
|
|
194
|
-
this.startOrAppend("text", chunk);
|
|
195
|
-
return null;
|
|
196
|
-
case WS_DATA_PREFIX.BINARY_CONT:
|
|
197
|
-
this.startOrAppend("binary", chunk);
|
|
198
|
-
return null;
|
|
199
|
-
case WS_DATA_PREFIX.TEXT:
|
|
200
|
-
return this.finish("text", chunk);
|
|
201
|
-
case WS_DATA_PREFIX.BINARY:
|
|
202
|
-
return this.finish("binary", chunk);
|
|
203
|
-
default:
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
startOrAppend(type, chunk) {
|
|
208
|
-
if (this.bufferedType === null) this.bufferedType = type;
|
|
209
|
-
this.buffer.push(chunk);
|
|
210
|
-
}
|
|
211
|
-
finish(type, chunk) {
|
|
212
|
-
if (this.buffer.length === 0) {
|
|
213
|
-
return this.materialize(type, [chunk]);
|
|
214
|
-
}
|
|
215
|
-
const resolvedType = this.bufferedType ?? type;
|
|
216
|
-
this.buffer.push(chunk);
|
|
217
|
-
const parts = this.buffer;
|
|
218
|
-
this.buffer = [];
|
|
219
|
-
this.bufferedType = null;
|
|
220
|
-
return this.materialize(resolvedType, parts);
|
|
221
|
-
}
|
|
222
|
-
materialize(type, parts) {
|
|
223
|
-
const total = parts.reduce((sum, p) => sum + p.length, 0);
|
|
224
|
-
const joined = new Uint8Array(total);
|
|
225
|
-
let offset = 0;
|
|
226
|
-
for (const part of parts) {
|
|
227
|
-
joined.set(part, offset);
|
|
228
|
-
offset += part.length;
|
|
229
|
-
}
|
|
230
|
-
return type === "text" ? { type: "text", data: textDecoder.decode(joined) } : { type: "binary", data: joined };
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// src/tunnel/v2/agent-kinds.ts
|
|
235
|
-
var LOCAL_HOST = "127.0.0.1";
|
|
236
|
-
var HEADER_SEC_WS_PROTOCOL = "sec-websocket-protocol";
|
|
237
|
-
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
238
|
-
"connection",
|
|
239
|
-
"keep-alive",
|
|
240
|
-
"proxy-authenticate",
|
|
241
|
-
"proxy-authorization",
|
|
242
|
-
"te",
|
|
243
|
-
"trailer",
|
|
244
|
-
"transfer-encoding",
|
|
245
|
-
"upgrade"
|
|
246
|
-
]);
|
|
247
|
-
var textEncoder2 = new TextEncoder();
|
|
248
|
-
var AgentDispatcher = class {
|
|
249
|
-
kinds = /* @__PURE__ */ new Map();
|
|
250
|
-
register(kind) {
|
|
251
|
-
this.kinds.set(kind.kind, kind);
|
|
252
|
-
return this;
|
|
253
|
-
}
|
|
254
|
-
dispatch(incoming) {
|
|
255
|
-
const handler = this.kinds.get(incoming.kind);
|
|
256
|
-
if (!handler) {
|
|
257
|
-
void incoming.stream.rst(ERROR_CODE.UNSUPPORTED);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
handler.handle(incoming);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
var HttpProxyKind = class {
|
|
264
|
-
constructor(ctx) {
|
|
265
|
-
this.ctx = ctx;
|
|
266
|
-
}
|
|
267
|
-
ctx;
|
|
268
|
-
kind = "http";
|
|
269
|
-
handle(incoming) {
|
|
270
|
-
void this.proxy(incoming);
|
|
271
|
-
}
|
|
272
|
-
async proxy(incoming) {
|
|
273
|
-
const head = incoming.head;
|
|
274
|
-
const request = http.request({
|
|
275
|
-
host: LOCAL_HOST,
|
|
276
|
-
port: this.ctx.localPort,
|
|
277
|
-
method: head.method,
|
|
278
|
-
path: head.path,
|
|
279
|
-
headers: toNodeHeaders(head.headers)
|
|
280
|
-
});
|
|
281
|
-
request.on("response", (response) => void this.pumpResponse(incoming.stream, response));
|
|
282
|
-
request.on("error", (error) => void this.answerError(incoming.stream, error));
|
|
283
|
-
void this.pumpRequestBody(incoming.stream, request);
|
|
284
|
-
}
|
|
285
|
-
async pumpRequestBody(stream, request) {
|
|
286
|
-
for (; ; ) {
|
|
287
|
-
const event = await stream.recv();
|
|
288
|
-
if (event === null) {
|
|
289
|
-
request.destroy();
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (event.type === "data") {
|
|
293
|
-
request.write(Buffer.from(event.payload));
|
|
294
|
-
} else if (event.type === "end") {
|
|
295
|
-
request.end();
|
|
296
|
-
return;
|
|
297
|
-
} else {
|
|
298
|
-
request.destroy();
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
async pumpResponse(stream, response) {
|
|
304
|
-
const headers = [];
|
|
305
|
-
for (let i = 0; i + 1 < response.rawHeaders.length; i += 2) {
|
|
306
|
-
headers.push([response.rawHeaders[i].toLowerCase(), response.rawHeaders[i + 1]]);
|
|
307
|
-
}
|
|
308
|
-
await stream.accept({ status: response.statusCode ?? 502, headers });
|
|
309
|
-
for await (const chunk of response) {
|
|
310
|
-
await stream.data(new Uint8Array(chunk));
|
|
311
|
-
}
|
|
312
|
-
await stream.end();
|
|
313
|
-
}
|
|
314
|
-
async answerError(stream, error) {
|
|
315
|
-
try {
|
|
316
|
-
await stream.accept({
|
|
317
|
-
status: 502,
|
|
318
|
-
headers: [["content-type", "text/plain; charset=utf-8"]]
|
|
319
|
-
});
|
|
320
|
-
await stream.data(textEncoder2.encode(`dev server unreachable: ${error.message}`));
|
|
321
|
-
await stream.end();
|
|
322
|
-
} catch {
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
var WsSpliceKind = class {
|
|
327
|
-
constructor(ctx) {
|
|
328
|
-
this.ctx = ctx;
|
|
329
|
-
}
|
|
330
|
-
ctx;
|
|
331
|
-
kind = "ws";
|
|
332
|
-
handle(incoming) {
|
|
333
|
-
void this.splice(incoming);
|
|
334
|
-
}
|
|
335
|
-
async splice(incoming) {
|
|
336
|
-
const head = incoming.head;
|
|
337
|
-
const protocols = head.headers.filter(([name]) => name === HEADER_SEC_WS_PROTOCOL).flatMap(([, value]) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
338
|
-
const local = this.ctx.createLocalSocket(
|
|
339
|
-
`ws://${LOCAL_HOST}:${this.ctx.localPort}${head.path}`,
|
|
340
|
-
protocols
|
|
341
|
-
);
|
|
342
|
-
local.binaryType = "arraybuffer";
|
|
343
|
-
let opened = false;
|
|
344
|
-
const reassembler = new WsReassembler();
|
|
345
|
-
local.addEventListener("open", () => {
|
|
346
|
-
opened = true;
|
|
347
|
-
const headers = local.protocol ? [[HEADER_SEC_WS_PROTOCOL, local.protocol]] : [];
|
|
348
|
-
void incoming.stream.accept({ status: 101, headers });
|
|
349
|
-
});
|
|
350
|
-
local.addEventListener("message", (event) => {
|
|
351
|
-
const message = toWsMessage(event.data);
|
|
352
|
-
if (!message) return;
|
|
353
|
-
for (const frame of encodeWsMessage(message, this.ctx.maxFramePayload)) {
|
|
354
|
-
void incoming.stream.data(frame);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
local.addEventListener("close", (event) => {
|
|
358
|
-
void incoming.stream.close({ code: sendableCloseCode(event.code), reason: event.reason });
|
|
359
|
-
});
|
|
360
|
-
local.addEventListener("error", () => {
|
|
361
|
-
if (opened) return;
|
|
362
|
-
void this.refuse(incoming.stream);
|
|
363
|
-
});
|
|
364
|
-
for (; ; ) {
|
|
365
|
-
const event = await incoming.stream.recv();
|
|
366
|
-
if (event === null) return;
|
|
367
|
-
if (event.type === "data") {
|
|
368
|
-
const message = reassembler.push(event.payload);
|
|
369
|
-
if (message) {
|
|
370
|
-
local.send(message.type === "text" ? message.data : message.data);
|
|
371
|
-
}
|
|
372
|
-
} else if (event.type === "close") {
|
|
373
|
-
const info2 = event.info;
|
|
374
|
-
local.close(sendableCloseCode(info2?.code), info2?.reason);
|
|
375
|
-
return;
|
|
376
|
-
} else if (event.type === "end" || event.type === "rst") {
|
|
377
|
-
local.close();
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
async refuse(stream) {
|
|
383
|
-
try {
|
|
384
|
-
await stream.accept({
|
|
385
|
-
status: 502,
|
|
386
|
-
headers: [["content-type", "text/plain; charset=utf-8"]]
|
|
387
|
-
});
|
|
388
|
-
await stream.data(textEncoder2.encode("dev server refused the websocket"));
|
|
389
|
-
await stream.end();
|
|
390
|
-
} catch {
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
function toNodeHeaders(pairs) {
|
|
395
|
-
const out = {};
|
|
396
|
-
for (const [name, value] of pairs) {
|
|
397
|
-
if (HOP_BY_HOP_HEADERS.has(name)) continue;
|
|
398
|
-
const existing = out[name];
|
|
399
|
-
if (existing === void 0) out[name] = value;
|
|
400
|
-
else if (Array.isArray(existing)) existing.push(value);
|
|
401
|
-
else out[name] = [existing, value];
|
|
402
|
-
}
|
|
403
|
-
return out;
|
|
404
|
-
}
|
|
405
|
-
function toWsMessage(data) {
|
|
406
|
-
if (typeof data === "string") return { type: "text", data };
|
|
407
|
-
if (data instanceof ArrayBuffer) return { type: "binary", data: new Uint8Array(data) };
|
|
408
|
-
if (data instanceof Uint8Array) return { type: "binary", data };
|
|
409
|
-
return null;
|
|
410
|
-
}
|
|
411
|
-
function sendableCloseCode(code) {
|
|
412
|
-
if (code === void 0) return void 0;
|
|
413
|
-
const legal = code >= 1e3 && code <= 1003 || code >= 1007 && code <= 1014 || code >= 3e3 && code <= 4999;
|
|
414
|
-
return legal ? code : void 0;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// src/tunnel/v2/control-protocol.ts
|
|
418
|
-
var CONTROL_KIND = "control";
|
|
419
|
-
var LENGTH_PREFIX_BYTES = 4;
|
|
420
|
-
var MAX_MESSAGE_BYTES = 32 * 1024 * 1024;
|
|
421
|
-
var CONTROL_OP = {
|
|
422
|
-
READ: "read",
|
|
423
|
-
WRITE: "write",
|
|
424
|
-
DELETE: "delete",
|
|
425
|
-
LIST: "list",
|
|
426
|
-
LIST_TREE: "list_tree",
|
|
427
|
-
STAT: "stat",
|
|
428
|
-
MKDIR: "mkdir",
|
|
429
|
-
RENAME: "rename",
|
|
430
|
-
WATCH_START: "watch_start",
|
|
431
|
-
WATCH_STOP: "watch_stop",
|
|
432
|
-
EXEC: "exec"
|
|
433
|
-
};
|
|
434
|
-
var CONTROL_ERROR = {
|
|
435
|
-
NOT_FOUND: "not_found",
|
|
436
|
-
INVALID_PATH: "invalid_path",
|
|
437
|
-
PERMISSION_DENIED: "permission_denied",
|
|
438
|
-
IO: "io",
|
|
439
|
-
UNSUPPORTED: "unsupported"};
|
|
440
|
-
var WATCH_EVENT = {
|
|
441
|
-
ADD: "add",
|
|
442
|
-
CHANGE: "change",
|
|
443
|
-
REMOVE: "remove",
|
|
444
|
-
RESYNC: "resync"
|
|
445
|
-
};
|
|
446
|
-
var ENCODING = {
|
|
447
|
-
UTF8: "utf8",
|
|
448
|
-
BASE64: "base64"
|
|
449
|
-
};
|
|
450
|
-
var ControlProtocolError = class extends Error {
|
|
451
|
-
};
|
|
452
|
-
var textEncoder3 = new TextEncoder();
|
|
453
|
-
var textDecoder2 = new TextDecoder();
|
|
454
|
-
function encodeMessage(message) {
|
|
455
|
-
const json = textEncoder3.encode(JSON.stringify(message));
|
|
456
|
-
if (json.length > MAX_MESSAGE_BYTES) {
|
|
457
|
-
throw new ControlProtocolError(`control message ${json.length} exceeds ${MAX_MESSAGE_BYTES}`);
|
|
458
|
-
}
|
|
459
|
-
const framed = new Uint8Array(LENGTH_PREFIX_BYTES + json.length);
|
|
460
|
-
new DataView(framed.buffer).setUint32(0, json.length);
|
|
461
|
-
framed.set(json, LENGTH_PREFIX_BYTES);
|
|
462
|
-
return framed;
|
|
463
|
-
}
|
|
464
|
-
function isRequest(message) {
|
|
465
|
-
return typeof message.op === "string";
|
|
466
|
-
}
|
|
467
|
-
var MessageFramer = class {
|
|
468
|
-
// Annotated Uint8Array (ArrayBufferLike) so concat/subarray/slice results
|
|
469
|
-
// assign back cleanly under TS 5.7's Uint8Array<ArrayBuffer> generic.
|
|
470
|
-
buffer = new Uint8Array(0);
|
|
471
|
-
push(bytes) {
|
|
472
|
-
this.buffer = concat(this.buffer, bytes);
|
|
473
|
-
const messages = [];
|
|
474
|
-
for (; ; ) {
|
|
475
|
-
if (this.buffer.length < LENGTH_PREFIX_BYTES) break;
|
|
476
|
-
const length = new DataView(
|
|
477
|
-
this.buffer.buffer,
|
|
478
|
-
this.buffer.byteOffset,
|
|
479
|
-
LENGTH_PREFIX_BYTES
|
|
480
|
-
).getUint32(0);
|
|
481
|
-
if (length > MAX_MESSAGE_BYTES) {
|
|
482
|
-
throw new ControlProtocolError(`framed length ${length} exceeds ${MAX_MESSAGE_BYTES}`);
|
|
483
|
-
}
|
|
484
|
-
if (this.buffer.length < LENGTH_PREFIX_BYTES + length) break;
|
|
485
|
-
const jsonBytes = this.buffer.subarray(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + length);
|
|
486
|
-
messages.push(JSON.parse(textDecoder2.decode(jsonBytes)));
|
|
487
|
-
this.buffer = this.buffer.subarray(LENGTH_PREFIX_BYTES + length);
|
|
488
|
-
}
|
|
489
|
-
if (this.buffer.byteOffset > 0) this.buffer = this.buffer.slice();
|
|
490
|
-
return messages;
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
function concat(a, b) {
|
|
494
|
-
if (a.length === 0) return b;
|
|
495
|
-
if (b.length === 0) return a;
|
|
496
|
-
const out = new Uint8Array(a.length + b.length);
|
|
497
|
-
out.set(a, 0);
|
|
498
|
-
out.set(b, a.length);
|
|
499
|
-
return out;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// src/tunnel/v2/control-agent.ts
|
|
503
|
-
var DEFAULT_IGNORED = /* @__PURE__ */ new Set([
|
|
504
|
-
"node_modules",
|
|
505
|
-
".git",
|
|
506
|
-
".next",
|
|
507
|
-
"dist",
|
|
508
|
-
"build",
|
|
509
|
-
".turbo",
|
|
510
|
-
".cache",
|
|
511
|
-
".cimplify"
|
|
512
|
-
]);
|
|
513
|
-
var ControlFsError = class extends Error {
|
|
514
|
-
constructor(code, message) {
|
|
515
|
-
super(message);
|
|
516
|
-
this.code = code;
|
|
517
|
-
}
|
|
518
|
-
code;
|
|
519
|
-
};
|
|
520
|
-
var ControlKind = class {
|
|
521
|
-
constructor(deps) {
|
|
522
|
-
this.deps = deps;
|
|
523
|
-
}
|
|
524
|
-
deps;
|
|
525
|
-
kind = CONTROL_KIND;
|
|
526
|
-
handle(incoming) {
|
|
527
|
-
void new ControlSession(incoming.stream, this.deps).run();
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
var ControlSession = class {
|
|
531
|
-
constructor(stream, deps) {
|
|
532
|
-
this.stream = stream;
|
|
533
|
-
this.deps = deps;
|
|
534
|
-
}
|
|
535
|
-
stream;
|
|
536
|
-
deps;
|
|
537
|
-
framer = new MessageFramer();
|
|
538
|
-
watch = null;
|
|
539
|
-
epoch = 0;
|
|
540
|
-
seq = 0;
|
|
541
|
-
// Serialize all writes so message bytes never interleave and event order
|
|
542
|
-
// (which carries seq) is preserved.
|
|
543
|
-
sending = Promise.resolve();
|
|
544
|
-
async run() {
|
|
545
|
-
try {
|
|
546
|
-
for (; ; ) {
|
|
547
|
-
const event = await this.stream.recv();
|
|
548
|
-
if (event === null) return;
|
|
549
|
-
if (event.type === "data") {
|
|
550
|
-
for (const message of this.framer.push(event.payload)) {
|
|
551
|
-
if (isRequest(message)) void this.dispatch(message);
|
|
552
|
-
}
|
|
553
|
-
} else if (event.type === "end") {
|
|
554
|
-
continue;
|
|
555
|
-
} else {
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
} finally {
|
|
560
|
-
this.watch?.stop();
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
send(message) {
|
|
564
|
-
const bytes = encodeMessage(message);
|
|
565
|
-
this.sending = this.sending.then(() => this.stream.data(bytes).catch(() => {
|
|
566
|
-
}));
|
|
567
|
-
}
|
|
568
|
-
async dispatch(request) {
|
|
569
|
-
try {
|
|
570
|
-
const data = await this.execute(request);
|
|
571
|
-
this.send({ id: request.id, ok: true, data });
|
|
572
|
-
} catch (error) {
|
|
573
|
-
const code = error instanceof ControlFsError ? error.code : CONTROL_ERROR.IO;
|
|
574
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
575
|
-
this.send({ id: request.id, ok: false, error: { code, message } });
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
async execute(request) {
|
|
579
|
-
const { fs: backend } = this.deps;
|
|
580
|
-
switch (request.op) {
|
|
581
|
-
case CONTROL_OP.READ:
|
|
582
|
-
return backend.read(str(request, FIELD.PATH));
|
|
583
|
-
case CONTROL_OP.WRITE:
|
|
584
|
-
await backend.write(str(request, FIELD.PATH), str(request, FIELD.CONTENT), enc(request));
|
|
585
|
-
return {};
|
|
586
|
-
case CONTROL_OP.DELETE:
|
|
587
|
-
await backend.delete(str(request, FIELD.PATH), request.recursive === true);
|
|
588
|
-
return {};
|
|
589
|
-
case CONTROL_OP.LIST:
|
|
590
|
-
return { entries: await backend.list(str(request, FIELD.PATH)) };
|
|
591
|
-
case CONTROL_OP.LIST_TREE:
|
|
592
|
-
return { paths: await backend.listTree() };
|
|
593
|
-
case CONTROL_OP.STAT:
|
|
594
|
-
return await backend.stat(str(request, FIELD.PATH));
|
|
595
|
-
case CONTROL_OP.MKDIR:
|
|
596
|
-
await backend.mkdir(str(request, FIELD.PATH));
|
|
597
|
-
return {};
|
|
598
|
-
case CONTROL_OP.RENAME:
|
|
599
|
-
await backend.rename(str(request, FIELD.FROM), str(request, FIELD.TO));
|
|
600
|
-
return {};
|
|
601
|
-
case CONTROL_OP.WATCH_START:
|
|
602
|
-
return this.startWatch(request);
|
|
603
|
-
case CONTROL_OP.WATCH_STOP:
|
|
604
|
-
this.watch?.stop();
|
|
605
|
-
this.watch = null;
|
|
606
|
-
return {};
|
|
607
|
-
case CONTROL_OP.EXEC:
|
|
608
|
-
return this.deps.exec.run(execCommand(request));
|
|
609
|
-
default:
|
|
610
|
-
throw new ControlFsError(CONTROL_ERROR.UNSUPPORTED, `unknown op ${request.op}`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
startWatch(request) {
|
|
614
|
-
this.watch?.stop();
|
|
615
|
-
this.epoch += 1;
|
|
616
|
-
this.seq = 0;
|
|
617
|
-
const epoch = this.epoch;
|
|
618
|
-
const paths = Array.isArray(request.paths) ? request.paths : [WORKSPACE_ROOT];
|
|
619
|
-
this.watch = this.deps.watcher.start(
|
|
620
|
-
paths,
|
|
621
|
-
(type, changed, hashes) => {
|
|
622
|
-
if (this.epoch !== epoch) return;
|
|
623
|
-
this.seq += 1;
|
|
624
|
-
this.send({
|
|
625
|
-
event: type,
|
|
626
|
-
paths: changed,
|
|
627
|
-
...hashes ? { hashes } : {},
|
|
628
|
-
seq: this.seq,
|
|
629
|
-
epoch
|
|
630
|
-
});
|
|
631
|
-
},
|
|
632
|
-
() => {
|
|
633
|
-
if (this.epoch !== epoch) return;
|
|
634
|
-
this.send({ event: WATCH_EVENT.RESYNC, epoch });
|
|
635
|
-
}
|
|
636
|
-
);
|
|
637
|
-
return { epoch };
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
var WORKSPACE_ROOT = ".";
|
|
641
|
-
var FIELD = {
|
|
642
|
-
PATH: "path",
|
|
643
|
-
CONTENT: "content",
|
|
644
|
-
FROM: "from",
|
|
645
|
-
TO: "to"
|
|
646
|
-
};
|
|
647
|
-
function str(request, field) {
|
|
648
|
-
const value = request[field];
|
|
649
|
-
if (typeof value !== "string") {
|
|
650
|
-
throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, `missing string field '${field}'`);
|
|
651
|
-
}
|
|
652
|
-
return value;
|
|
653
|
-
}
|
|
654
|
-
function enc(request) {
|
|
655
|
-
return request.encoding === ENCODING.BASE64 ? ENCODING.BASE64 : ENCODING.UTF8;
|
|
656
|
-
}
|
|
657
|
-
function execCommand(request) {
|
|
658
|
-
const command = request.command;
|
|
659
|
-
if (typeof command === "string") return command;
|
|
660
|
-
if (Array.isArray(command) && command.every((part) => typeof part === "string")) {
|
|
661
|
-
return command;
|
|
662
|
-
}
|
|
663
|
-
throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, "exec requires a command");
|
|
664
|
-
}
|
|
665
|
-
function resolveInRoot(root, rel) {
|
|
666
|
-
const resolved = path2.resolve(root, rel);
|
|
667
|
-
const rootWithSep = root.endsWith(path2.sep) ? root : root + path2.sep;
|
|
668
|
-
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
669
|
-
throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, `path escapes workspace: ${rel}`);
|
|
670
|
-
}
|
|
671
|
-
return resolved;
|
|
672
|
-
}
|
|
673
|
-
function isIgnored(rel) {
|
|
674
|
-
return rel.split(/[\\/]/).some((segment) => DEFAULT_IGNORED.has(segment));
|
|
675
|
-
}
|
|
676
|
-
var NodeControlFs = class {
|
|
677
|
-
constructor(root) {
|
|
678
|
-
this.root = root;
|
|
679
|
-
}
|
|
680
|
-
root;
|
|
681
|
-
async read(rel) {
|
|
682
|
-
const abs = resolveInRoot(this.root, rel);
|
|
683
|
-
const bytes = await promises.readFile(abs).catch(rethrow(rel));
|
|
684
|
-
const text = tryDecodeUtf8(bytes);
|
|
685
|
-
return text !== null ? { content: text, encoding: ENCODING.UTF8 } : { content: Buffer.from(bytes).toString("base64"), encoding: ENCODING.BASE64 };
|
|
686
|
-
}
|
|
687
|
-
async write(rel, content, encoding) {
|
|
688
|
-
const abs = resolveInRoot(this.root, rel);
|
|
689
|
-
await promises.mkdir(path2.dirname(abs), { recursive: true });
|
|
690
|
-
const bytes = encoding === ENCODING.BASE64 ? Buffer.from(content, "base64") : Buffer.from(content, "utf8");
|
|
691
|
-
await promises.writeFile(abs, bytes).catch(rethrow(rel));
|
|
692
|
-
}
|
|
693
|
-
async delete(rel, recursive) {
|
|
694
|
-
const abs = resolveInRoot(this.root, rel);
|
|
695
|
-
await promises.rm(abs, { recursive, force: true });
|
|
696
|
-
}
|
|
697
|
-
async list(rel) {
|
|
698
|
-
const abs = resolveInRoot(this.root, rel);
|
|
699
|
-
const dirents = await promises.readdir(abs, { withFileTypes: true }).catch(rethrow(rel));
|
|
700
|
-
return dirents.filter((d) => !DEFAULT_IGNORED.has(d.name)).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
701
|
-
}
|
|
702
|
-
async listTree() {
|
|
703
|
-
const out = [];
|
|
704
|
-
const walk = async (dir) => {
|
|
705
|
-
const dirents = await promises.readdir(path2.join(this.root, dir), { withFileTypes: true });
|
|
706
|
-
for (const dirent of dirents) {
|
|
707
|
-
if (DEFAULT_IGNORED.has(dirent.name)) continue;
|
|
708
|
-
const rel = dir ? `${dir}/${dirent.name}` : dirent.name;
|
|
709
|
-
if (dirent.isDirectory()) await walk(rel);
|
|
710
|
-
else out.push(rel);
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
await walk("");
|
|
714
|
-
return out;
|
|
715
|
-
}
|
|
716
|
-
async stat(rel) {
|
|
717
|
-
const abs = resolveInRoot(this.root, rel);
|
|
718
|
-
const stat = await promises.stat(abs).catch(rethrow(rel));
|
|
719
|
-
return {
|
|
720
|
-
kind: stat.isDirectory() ? "dir" : "file",
|
|
721
|
-
size: stat.size,
|
|
722
|
-
mtimeMs: Math.floor(stat.mtimeMs)
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
async mkdir(rel) {
|
|
726
|
-
await promises.mkdir(resolveInRoot(this.root, rel), { recursive: true });
|
|
727
|
-
}
|
|
728
|
-
async rename(from, to) {
|
|
729
|
-
const absFrom = resolveInRoot(this.root, from);
|
|
730
|
-
const absTo = resolveInRoot(this.root, to);
|
|
731
|
-
await promises.mkdir(path2.dirname(absTo), { recursive: true });
|
|
732
|
-
await promises.rename(absFrom, absTo).catch(rethrow(from));
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
var WATCH_DEBOUNCE_MS = 60;
|
|
736
|
-
var MAX_WATCH_PENDING_PATHS = 4096;
|
|
737
|
-
var MAX_WATCH_TRACKED_PATHS = 1e5;
|
|
738
|
-
var CONTENT_HASH_ALGORITHM = "sha256";
|
|
739
|
-
var CONTENT_HASH_ENCODING = "hex";
|
|
740
|
-
var NodeControlWatcher = class {
|
|
741
|
-
constructor(root) {
|
|
742
|
-
this.root = root;
|
|
743
|
-
}
|
|
744
|
-
root;
|
|
745
|
-
start(_relPaths, onEvent, onResync) {
|
|
746
|
-
let watcher;
|
|
747
|
-
const pending = /* @__PURE__ */ new Map();
|
|
748
|
-
const versions = /* @__PURE__ */ new Map();
|
|
749
|
-
const observed = /* @__PURE__ */ new Map();
|
|
750
|
-
let stopped = false;
|
|
751
|
-
const clearTracking = () => {
|
|
752
|
-
for (const timer of pending.values()) clearTimeout(timer);
|
|
753
|
-
pending.clear();
|
|
754
|
-
versions.clear();
|
|
755
|
-
observed.clear();
|
|
756
|
-
};
|
|
757
|
-
const requestResync = () => {
|
|
758
|
-
clearTracking();
|
|
759
|
-
if (!stopped) onResync();
|
|
760
|
-
};
|
|
761
|
-
const flush = async (rel, version) => {
|
|
762
|
-
pending.delete(rel);
|
|
763
|
-
try {
|
|
764
|
-
const absolute = path2.join(this.root, rel);
|
|
765
|
-
const stat = await promises.stat(absolute);
|
|
766
|
-
if (stat.isDirectory()) return;
|
|
767
|
-
const bytes = await promises.readFile(absolute);
|
|
768
|
-
if (stopped || versions.get(rel) !== version) return;
|
|
769
|
-
const hash = createHash(CONTENT_HASH_ALGORITHM).update(bytes).digest(CONTENT_HASH_ENCODING);
|
|
770
|
-
if (observed.get(rel) === hash) return;
|
|
771
|
-
observed.set(rel, hash);
|
|
772
|
-
onEvent(WATCH_EVENT.CHANGE, [rel], { [rel]: hash });
|
|
773
|
-
} catch (error) {
|
|
774
|
-
if (stopped || versions.get(rel) !== version) return;
|
|
775
|
-
if (error.code === "ENOENT") {
|
|
776
|
-
if (observed.get(rel) === null) return;
|
|
777
|
-
observed.set(rel, null);
|
|
778
|
-
onEvent(WATCH_EVENT.REMOVE, [rel]);
|
|
779
|
-
} else {
|
|
780
|
-
requestResync();
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
if (observed.size > MAX_WATCH_TRACKED_PATHS) {
|
|
784
|
-
requestResync();
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
try {
|
|
788
|
-
watcher = watch(this.root, { recursive: true }, (_type, filename) => {
|
|
789
|
-
if (filename === null) {
|
|
790
|
-
requestResync();
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
const rel = filename.toString().split(path2.sep).join("/");
|
|
794
|
-
if (isIgnored(rel)) return;
|
|
795
|
-
const existing = pending.get(rel);
|
|
796
|
-
if (existing) clearTimeout(existing);
|
|
797
|
-
if (!existing && pending.size >= MAX_WATCH_PENDING_PATHS) {
|
|
798
|
-
requestResync();
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
const version = (versions.get(rel) ?? 0) + 1;
|
|
802
|
-
versions.set(rel, version);
|
|
803
|
-
if (versions.size > MAX_WATCH_TRACKED_PATHS) {
|
|
804
|
-
requestResync();
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
pending.set(
|
|
808
|
-
rel,
|
|
809
|
-
setTimeout(() => void flush(rel, version), WATCH_DEBOUNCE_MS)
|
|
810
|
-
);
|
|
811
|
-
});
|
|
812
|
-
} catch (error) {
|
|
813
|
-
throw new ControlFsError(
|
|
814
|
-
CONTROL_ERROR.IO,
|
|
815
|
-
`failed to start workspace watcher: ${error instanceof Error ? error.message : String(error)}`
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
watcher.on("error", () => {
|
|
819
|
-
requestResync();
|
|
820
|
-
});
|
|
821
|
-
return {
|
|
822
|
-
stop: () => {
|
|
823
|
-
stopped = true;
|
|
824
|
-
clearTracking();
|
|
825
|
-
watcher.close();
|
|
826
|
-
}
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
};
|
|
830
|
-
var EXEC_MAX_BUFFER = 8 * 1024 * 1024;
|
|
831
|
-
var NodeControlExec = class {
|
|
832
|
-
constructor(root) {
|
|
833
|
-
this.root = root;
|
|
834
|
-
}
|
|
835
|
-
root;
|
|
836
|
-
run(command) {
|
|
837
|
-
const [file, args, shell] = Array.isArray(command) ? [command[0], command.slice(1), false] : [command, [], true];
|
|
838
|
-
return new Promise((resolve) => {
|
|
839
|
-
const child = spawn(file, args, { cwd: this.root, shell, env: process.env });
|
|
840
|
-
let stdout = "";
|
|
841
|
-
let stderr = "";
|
|
842
|
-
let truncated = false;
|
|
843
|
-
const cap = (existing, chunk) => {
|
|
844
|
-
if (existing.length >= EXEC_MAX_BUFFER) {
|
|
845
|
-
truncated = true;
|
|
846
|
-
return existing;
|
|
847
|
-
}
|
|
848
|
-
return existing + chunk.toString("utf8");
|
|
849
|
-
};
|
|
850
|
-
child.stdout?.on("data", (chunk) => stdout = cap(stdout, chunk));
|
|
851
|
-
child.stderr?.on("data", (chunk) => stderr = cap(stderr, chunk));
|
|
852
|
-
child.on("error", (error) => resolve({ stdout, stderr: error.message, exitCode: 127 }));
|
|
853
|
-
child.on("close", (code) => {
|
|
854
|
-
if (truncated) stderr += "\n[output truncated]";
|
|
855
|
-
resolve({ stdout, stderr, exitCode: code ?? 0 });
|
|
856
|
-
});
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
function tryDecodeUtf8(bytes) {
|
|
861
|
-
try {
|
|
862
|
-
const text = new TextDecoder("utf8", { fatal: true }).decode(bytes);
|
|
863
|
-
return text;
|
|
864
|
-
} catch {
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
function rethrow(rel) {
|
|
869
|
-
return (error) => {
|
|
870
|
-
if (error.code === "ENOENT")
|
|
871
|
-
throw new ControlFsError(CONTROL_ERROR.NOT_FOUND, `not found: ${rel}`);
|
|
872
|
-
if (error.code === "EACCES") throw new ControlFsError(CONTROL_ERROR.PERMISSION_DENIED, rel);
|
|
873
|
-
throw new ControlFsError(CONTROL_ERROR.IO, error.message);
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// src/tunnel/protocol.ts
|
|
878
|
-
var MAX_PAYLOAD = 262144;
|
|
879
|
-
var HEADER_LEN2 = 9;
|
|
880
|
-
var SUBPROTOCOL = "cimplify-tunnel.v1";
|
|
881
|
-
var CONNECT_PATH = "/__cimplify/connect";
|
|
882
|
-
var FRAME_TYPE = {
|
|
883
|
-
OPEN_HTTP: 1,
|
|
884
|
-
OPEN_WS: 2,
|
|
885
|
-
RESP_HEAD: 3,
|
|
886
|
-
BODY: 4,
|
|
887
|
-
END: 5,
|
|
888
|
-
WS_TEXT: 6,
|
|
889
|
-
WS_BIN: 7,
|
|
890
|
-
CLOSE: 8
|
|
891
|
-
};
|
|
892
|
-
var KNOWN_FRAME_TYPES = new Set(Object.values(FRAME_TYPE));
|
|
893
|
-
var TunnelProtocolError = class extends Error {
|
|
894
|
-
};
|
|
895
|
-
function encodeFrame(frame) {
|
|
896
|
-
if (frame.payload.length > MAX_PAYLOAD) {
|
|
897
|
-
throw new TunnelProtocolError(`payload length ${frame.payload.length} exceeds ${MAX_PAYLOAD}`);
|
|
898
|
-
}
|
|
899
|
-
const bytes = new Uint8Array(HEADER_LEN2 + frame.payload.length);
|
|
900
|
-
const view = new DataView(bytes.buffer);
|
|
901
|
-
view.setUint32(0, frame.streamId);
|
|
902
|
-
view.setUint8(4, frame.frameType);
|
|
903
|
-
view.setUint32(5, frame.payload.length);
|
|
904
|
-
bytes.set(frame.payload, HEADER_LEN2);
|
|
905
|
-
return bytes;
|
|
906
|
-
}
|
|
907
|
-
function decodeFrame(bytes) {
|
|
908
|
-
if (bytes.length < HEADER_LEN2) {
|
|
909
|
-
throw new TunnelProtocolError(`frame truncated: ${bytes.length} bytes`);
|
|
910
|
-
}
|
|
911
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
912
|
-
const streamId = view.getUint32(0);
|
|
913
|
-
const rawType = view.getUint8(4);
|
|
914
|
-
if (!KNOWN_FRAME_TYPES.has(rawType)) {
|
|
915
|
-
throw new TunnelProtocolError(`unknown frame type 0x${rawType.toString(16).padStart(2, "0")}`);
|
|
916
|
-
}
|
|
917
|
-
const declared = view.getUint32(5);
|
|
918
|
-
if (declared > MAX_PAYLOAD) {
|
|
919
|
-
throw new TunnelProtocolError(`payload length ${declared} exceeds ${MAX_PAYLOAD}`);
|
|
920
|
-
}
|
|
921
|
-
const actual = bytes.length - HEADER_LEN2;
|
|
922
|
-
if (declared !== actual) {
|
|
923
|
-
throw new TunnelProtocolError(
|
|
924
|
-
`payload length field ${declared} does not match remaining ${actual} bytes`
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
return { streamId, frameType: rawType, payload: bytes.subarray(HEADER_LEN2) };
|
|
928
|
-
}
|
|
929
|
-
var textEncoder4 = new TextEncoder();
|
|
930
|
-
var textDecoder3 = new TextDecoder();
|
|
931
|
-
function encodeJsonPayload(value) {
|
|
932
|
-
return textEncoder4.encode(JSON.stringify(value));
|
|
933
|
-
}
|
|
934
|
-
function decodeJsonPayload(payload) {
|
|
935
|
-
return JSON.parse(textDecoder3.decode(payload));
|
|
936
|
-
}
|
|
937
|
-
function* chunkPayload(bytes) {
|
|
938
|
-
if (bytes.length === 0) {
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
for (let offset = 0; offset < bytes.length; offset += MAX_PAYLOAD) {
|
|
942
|
-
yield bytes.subarray(offset, Math.min(offset + MAX_PAYLOAD, bytes.length));
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// src/tunnel/client.ts
|
|
947
|
-
var LOCAL_HOST2 = "127.0.0.1";
|
|
948
|
-
var SEND_HIGH_WATER_BYTES = 4 * 1024 * 1024;
|
|
949
|
-
var BACKPRESSURE_POLL_MS = 25;
|
|
950
|
-
var EMPTY_PAYLOAD = new Uint8Array(0);
|
|
951
|
-
var HEADER_SEC_WS_PROTOCOL2 = "sec-websocket-protocol";
|
|
952
|
-
var HEADER_CONTENT_TYPE_TEXT = ["content-type", "text/plain; charset=utf-8"];
|
|
953
|
-
var WS_CLOSE_NORMAL = 1e3;
|
|
954
|
-
var HOP_BY_HOP_HEADERS2 = /* @__PURE__ */ new Set([
|
|
955
|
-
"connection",
|
|
956
|
-
"keep-alive",
|
|
957
|
-
"proxy-authenticate",
|
|
958
|
-
"proxy-authorization",
|
|
959
|
-
"te",
|
|
960
|
-
"trailer",
|
|
961
|
-
"transfer-encoding",
|
|
962
|
-
"upgrade"
|
|
963
|
-
]);
|
|
964
|
-
var textEncoder5 = new TextEncoder();
|
|
965
|
-
var textDecoder4 = new TextDecoder();
|
|
966
|
-
function defaultSocketFactory(url, protocols) {
|
|
967
|
-
const ctor = globalThis.WebSocket;
|
|
968
|
-
if (!ctor) {
|
|
969
|
-
throw new Error("global WebSocket is unavailable; the preview tunnel requires Bun or Node 22+");
|
|
970
|
-
}
|
|
971
|
-
return new ctor(url, protocols);
|
|
972
|
-
}
|
|
973
|
-
function connectUrl(baseUrl, token) {
|
|
974
|
-
return `${baseUrl.replace(/\/+$/, "")}${CONNECT_PATH}?token=${encodeURIComponent(token)}`;
|
|
975
|
-
}
|
|
976
|
-
var TunnelClient = class {
|
|
977
|
-
constructor(options) {
|
|
978
|
-
this.options = options;
|
|
979
|
-
}
|
|
980
|
-
options;
|
|
981
|
-
streams = /* @__PURE__ */ new Map();
|
|
982
|
-
socket = null;
|
|
983
|
-
closed = false;
|
|
984
|
-
start() {
|
|
985
|
-
const factory = this.options.createSocket ?? defaultSocketFactory;
|
|
986
|
-
const socket = factory(connectUrl(this.options.url, this.options.token), [SUBPROTOCOL]);
|
|
987
|
-
socket.binaryType = "arraybuffer";
|
|
988
|
-
this.socket = socket;
|
|
989
|
-
socket.addEventListener("open", () => {
|
|
990
|
-
if (!this.closed) this.options.events?.onConnected?.();
|
|
991
|
-
});
|
|
992
|
-
socket.addEventListener("message", (event) => this.handleMessage(event.data));
|
|
993
|
-
socket.addEventListener(
|
|
994
|
-
"close",
|
|
995
|
-
(event) => this.teardown(`connection closed (${event.code ?? "unknown"})`, event.code)
|
|
996
|
-
);
|
|
997
|
-
socket.addEventListener("error", () => this.teardown("connection error"));
|
|
998
|
-
if (this.options.socketAlreadyOpen && !this.closed) this.options.events?.onConnected?.();
|
|
999
|
-
}
|
|
1000
|
-
stop() {
|
|
1001
|
-
if (this.closed) return;
|
|
1002
|
-
try {
|
|
1003
|
-
this.socket?.close(WS_CLOSE_NORMAL);
|
|
1004
|
-
} catch {
|
|
1005
|
-
}
|
|
1006
|
-
this.teardown("stopped");
|
|
1007
|
-
}
|
|
1008
|
-
teardown(reason, closeCode) {
|
|
1009
|
-
if (this.closed) return;
|
|
1010
|
-
this.closed = true;
|
|
1011
|
-
for (const handler of this.streams.values()) {
|
|
1012
|
-
handler.abort();
|
|
1013
|
-
}
|
|
1014
|
-
this.streams.clear();
|
|
1015
|
-
this.socket = null;
|
|
1016
|
-
this.options.events?.onClosed?.(reason, closeCode);
|
|
1017
|
-
}
|
|
1018
|
-
handleMessage(data) {
|
|
1019
|
-
if (this.closed) return;
|
|
1020
|
-
let bytes;
|
|
1021
|
-
if (data instanceof ArrayBuffer) {
|
|
1022
|
-
bytes = new Uint8Array(data);
|
|
1023
|
-
} else if (data instanceof Uint8Array) {
|
|
1024
|
-
bytes = data;
|
|
1025
|
-
} else {
|
|
1026
|
-
this.failConnection("non-binary message violates the tunnel protocol");
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
let frame;
|
|
1030
|
-
try {
|
|
1031
|
-
frame = decodeFrame(bytes);
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
this.failConnection(`undecodable frame: ${err.message}`);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
this.dispatch(frame);
|
|
1037
|
-
}
|
|
1038
|
-
dispatch(frame) {
|
|
1039
|
-
const handler = this.streams.get(frame.streamId);
|
|
1040
|
-
if (handler) {
|
|
1041
|
-
if (frame.frameType === FRAME_TYPE.CLOSE) this.streams.delete(frame.streamId);
|
|
1042
|
-
handler.handleFrame(frame);
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
switch (frame.frameType) {
|
|
1046
|
-
case FRAME_TYPE.OPEN_HTTP:
|
|
1047
|
-
this.openHttp(frame);
|
|
1048
|
-
break;
|
|
1049
|
-
case FRAME_TYPE.OPEN_WS:
|
|
1050
|
-
this.openWs(frame);
|
|
1051
|
-
break;
|
|
1052
|
-
case FRAME_TYPE.CLOSE:
|
|
1053
|
-
break;
|
|
1054
|
-
default:
|
|
1055
|
-
this.sendClose(frame.streamId);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
openHttp(frame) {
|
|
1059
|
-
let head;
|
|
1060
|
-
try {
|
|
1061
|
-
head = decodeJsonPayload(frame.payload);
|
|
1062
|
-
} catch {
|
|
1063
|
-
this.sendClose(frame.streamId);
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
this.streams.set(
|
|
1067
|
-
frame.streamId,
|
|
1068
|
-
new HttpProxyStream(this.contextFor(frame.streamId), head, this.options.localPort)
|
|
1069
|
-
);
|
|
1070
|
-
}
|
|
1071
|
-
openWs(frame) {
|
|
1072
|
-
let head;
|
|
1073
|
-
try {
|
|
1074
|
-
head = decodeJsonPayload(frame.payload);
|
|
1075
|
-
} catch {
|
|
1076
|
-
this.sendClose(frame.streamId);
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
const factory = this.options.createLocalSocket ?? defaultSocketFactory;
|
|
1080
|
-
this.streams.set(
|
|
1081
|
-
frame.streamId,
|
|
1082
|
-
new WsProxyStream(this.contextFor(frame.streamId), head, this.options.localPort, factory)
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
contextFor(streamId) {
|
|
1086
|
-
return {
|
|
1087
|
-
send: (frameType, payload) => this.sendFrame({ streamId, frameType, payload }),
|
|
1088
|
-
sendJson: (frameType, value) => this.sendFrame({ streamId, frameType, payload: encodeJsonPayload(value) }),
|
|
1089
|
-
finish: () => {
|
|
1090
|
-
this.streams.delete(streamId);
|
|
1091
|
-
},
|
|
1092
|
-
fail: (info2) => {
|
|
1093
|
-
this.streams.delete(streamId);
|
|
1094
|
-
this.sendClose(streamId, info2);
|
|
1095
|
-
},
|
|
1096
|
-
bufferedAmount: () => this.socket?.bufferedAmount ?? 0
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
sendClose(streamId, info2) {
|
|
1100
|
-
const payload = info2 && (info2.code !== void 0 || info2.reason !== void 0) ? encodeJsonPayload(info2) : EMPTY_PAYLOAD;
|
|
1101
|
-
this.sendFrame({ streamId, frameType: FRAME_TYPE.CLOSE, payload });
|
|
1102
|
-
}
|
|
1103
|
-
sendFrame(frame) {
|
|
1104
|
-
if (this.closed || !this.socket) return false;
|
|
1105
|
-
try {
|
|
1106
|
-
this.socket.send(encodeFrame(frame));
|
|
1107
|
-
return true;
|
|
1108
|
-
} catch {
|
|
1109
|
-
return false;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
failConnection(reason) {
|
|
1113
|
-
try {
|
|
1114
|
-
this.socket?.close(WS_CLOSE_NORMAL);
|
|
1115
|
-
} catch {
|
|
1116
|
-
}
|
|
1117
|
-
this.teardown(reason);
|
|
1118
|
-
}
|
|
1119
|
-
};
|
|
1120
|
-
var HttpProxyStream = class {
|
|
1121
|
-
constructor(ctx, head, localPort) {
|
|
1122
|
-
this.ctx = ctx;
|
|
1123
|
-
this.request = http.request({
|
|
1124
|
-
host: LOCAL_HOST2,
|
|
1125
|
-
port: localPort,
|
|
1126
|
-
method: head.method,
|
|
1127
|
-
path: head.path,
|
|
1128
|
-
headers: toNodeHeaders2(head.headers)
|
|
1129
|
-
});
|
|
1130
|
-
this.request.on("response", (response) => this.pumpResponse(response));
|
|
1131
|
-
this.request.on("error", (err) => this.handleRequestError(err));
|
|
1132
|
-
}
|
|
1133
|
-
ctx;
|
|
1134
|
-
request;
|
|
1135
|
-
responded = false;
|
|
1136
|
-
done = false;
|
|
1137
|
-
handleFrame(frame) {
|
|
1138
|
-
switch (frame.frameType) {
|
|
1139
|
-
case FRAME_TYPE.BODY:
|
|
1140
|
-
this.request.write(Buffer.from(frame.payload));
|
|
1141
|
-
break;
|
|
1142
|
-
case FRAME_TYPE.END:
|
|
1143
|
-
this.request.end();
|
|
1144
|
-
break;
|
|
1145
|
-
case FRAME_TYPE.CLOSE:
|
|
1146
|
-
this.abort();
|
|
1147
|
-
break;
|
|
1148
|
-
default:
|
|
1149
|
-
this.ctx.fail();
|
|
1150
|
-
this.abort();
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
abort() {
|
|
1154
|
-
this.done = true;
|
|
1155
|
-
this.request.destroy();
|
|
1156
|
-
}
|
|
1157
|
-
pumpResponse(response) {
|
|
1158
|
-
this.responded = true;
|
|
1159
|
-
const headers = [];
|
|
1160
|
-
for (let i = 0; i + 1 < response.rawHeaders.length; i += 2) {
|
|
1161
|
-
headers.push([response.rawHeaders[i].toLowerCase(), response.rawHeaders[i + 1]]);
|
|
1162
|
-
}
|
|
1163
|
-
const head = { status: response.statusCode ?? 502, headers };
|
|
1164
|
-
if (!this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, head)) {
|
|
1165
|
-
this.abort();
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
response.on("data", (chunk) => {
|
|
1169
|
-
for (const piece of chunkPayload(chunk)) {
|
|
1170
|
-
if (!this.ctx.send(FRAME_TYPE.BODY, piece)) {
|
|
1171
|
-
this.abort();
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
this.applyBackpressure(response);
|
|
1176
|
-
});
|
|
1177
|
-
response.on("end", () => {
|
|
1178
|
-
if (this.done) return;
|
|
1179
|
-
this.done = true;
|
|
1180
|
-
this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
|
|
1181
|
-
this.ctx.finish();
|
|
1182
|
-
});
|
|
1183
|
-
response.on("error", () => {
|
|
1184
|
-
if (this.done) return;
|
|
1185
|
-
this.done = true;
|
|
1186
|
-
this.ctx.fail();
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
/** The mux socket has no stream-level flow control; pause the local read
|
|
1190
|
-
* side when the shared connection's send buffer backs up. */
|
|
1191
|
-
applyBackpressure(response) {
|
|
1192
|
-
if (this.ctx.bufferedAmount() <= SEND_HIGH_WATER_BYTES) return;
|
|
1193
|
-
response.pause();
|
|
1194
|
-
const poll = () => {
|
|
1195
|
-
if (this.done) return;
|
|
1196
|
-
if (this.ctx.bufferedAmount() <= SEND_HIGH_WATER_BYTES) {
|
|
1197
|
-
response.resume();
|
|
1198
|
-
} else {
|
|
1199
|
-
setTimeout(poll, BACKPRESSURE_POLL_MS);
|
|
1200
|
-
}
|
|
1201
|
-
};
|
|
1202
|
-
setTimeout(poll, BACKPRESSURE_POLL_MS);
|
|
1203
|
-
}
|
|
1204
|
-
handleRequestError(err) {
|
|
1205
|
-
if (this.done) return;
|
|
1206
|
-
this.done = true;
|
|
1207
|
-
if (this.responded) {
|
|
1208
|
-
this.ctx.fail();
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, {
|
|
1212
|
-
status: 502,
|
|
1213
|
-
headers: [HEADER_CONTENT_TYPE_TEXT]
|
|
1214
|
-
});
|
|
1215
|
-
this.ctx.send(FRAME_TYPE.BODY, textEncoder5.encode(`dev server unreachable: ${err.message}`));
|
|
1216
|
-
this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
|
|
1217
|
-
this.ctx.finish();
|
|
1218
|
-
}
|
|
1219
|
-
};
|
|
1220
|
-
var WsProxyStream = class {
|
|
1221
|
-
constructor(ctx, head, localPort, factory) {
|
|
1222
|
-
this.ctx = ctx;
|
|
1223
|
-
const protocols = head.headers.filter(([name]) => name === HEADER_SEC_WS_PROTOCOL2).flatMap(([, value]) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
1224
|
-
this.local = factory(`ws://${LOCAL_HOST2}:${localPort}${head.path}`, protocols);
|
|
1225
|
-
this.local.binaryType = "arraybuffer";
|
|
1226
|
-
this.local.addEventListener("open", () => {
|
|
1227
|
-
this.opened = true;
|
|
1228
|
-
const headers = this.local.protocol ? [[HEADER_SEC_WS_PROTOCOL2, this.local.protocol]] : [];
|
|
1229
|
-
if (!this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, { status: 101, headers })) {
|
|
1230
|
-
this.abort();
|
|
1231
|
-
}
|
|
1232
|
-
});
|
|
1233
|
-
this.local.addEventListener("message", (event) => this.forward(event.data));
|
|
1234
|
-
this.local.addEventListener("close", (event) => {
|
|
1235
|
-
if (this.done) return;
|
|
1236
|
-
this.done = true;
|
|
1237
|
-
this.ctx.fail({ code: sendableCloseCode2(event.code), reason: event.reason || void 0 });
|
|
1238
|
-
});
|
|
1239
|
-
this.local.addEventListener("error", () => {
|
|
1240
|
-
if (this.done || this.opened) return;
|
|
1241
|
-
this.done = true;
|
|
1242
|
-
this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, {
|
|
1243
|
-
status: 502,
|
|
1244
|
-
headers: [HEADER_CONTENT_TYPE_TEXT]
|
|
1245
|
-
});
|
|
1246
|
-
this.ctx.send(FRAME_TYPE.BODY, textEncoder5.encode("dev server refused the websocket"));
|
|
1247
|
-
this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
|
|
1248
|
-
this.ctx.finish();
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
ctx;
|
|
1252
|
-
local;
|
|
1253
|
-
opened = false;
|
|
1254
|
-
done = false;
|
|
1255
|
-
handleFrame(frame) {
|
|
1256
|
-
switch (frame.frameType) {
|
|
1257
|
-
case FRAME_TYPE.WS_TEXT:
|
|
1258
|
-
this.local.send(textDecoder4.decode(frame.payload));
|
|
1259
|
-
break;
|
|
1260
|
-
case FRAME_TYPE.WS_BIN:
|
|
1261
|
-
this.local.send(frame.payload);
|
|
1262
|
-
break;
|
|
1263
|
-
case FRAME_TYPE.CLOSE: {
|
|
1264
|
-
this.done = true;
|
|
1265
|
-
const info2 = parseCloseInfo(frame.payload);
|
|
1266
|
-
this.local.close(sanitizeCloseCode(info2.code), info2.reason);
|
|
1267
|
-
break;
|
|
1268
|
-
}
|
|
1269
|
-
default:
|
|
1270
|
-
this.ctx.fail();
|
|
1271
|
-
this.abort();
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
abort() {
|
|
1275
|
-
this.done = true;
|
|
1276
|
-
try {
|
|
1277
|
-
this.local.close();
|
|
1278
|
-
} catch {
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
forward(data) {
|
|
1282
|
-
if (this.done) return;
|
|
1283
|
-
let payload;
|
|
1284
|
-
let frameType;
|
|
1285
|
-
if (typeof data === "string") {
|
|
1286
|
-
payload = textEncoder5.encode(data);
|
|
1287
|
-
frameType = FRAME_TYPE.WS_TEXT;
|
|
1288
|
-
} else if (data instanceof ArrayBuffer) {
|
|
1289
|
-
payload = new Uint8Array(data);
|
|
1290
|
-
frameType = FRAME_TYPE.WS_BIN;
|
|
1291
|
-
} else if (data instanceof Uint8Array) {
|
|
1292
|
-
payload = data;
|
|
1293
|
-
frameType = FRAME_TYPE.WS_BIN;
|
|
1294
|
-
} else {
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
if (payload.length > MAX_PAYLOAD) {
|
|
1298
|
-
this.done = true;
|
|
1299
|
-
this.ctx.fail({ reason: "message exceeds tunnel frame cap" });
|
|
1300
|
-
this.abort();
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
if (!this.ctx.send(frameType, payload)) this.abort();
|
|
1304
|
-
}
|
|
1305
|
-
};
|
|
1306
|
-
function toNodeHeaders2(pairs) {
|
|
1307
|
-
const out = {};
|
|
1308
|
-
for (const [name, value] of pairs) {
|
|
1309
|
-
if (HOP_BY_HOP_HEADERS2.has(name)) continue;
|
|
1310
|
-
const existing = out[name];
|
|
1311
|
-
if (existing === void 0) {
|
|
1312
|
-
out[name] = value;
|
|
1313
|
-
} else if (Array.isArray(existing)) {
|
|
1314
|
-
existing.push(value);
|
|
1315
|
-
} else {
|
|
1316
|
-
out[name] = [existing, value];
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
return out;
|
|
1320
|
-
}
|
|
1321
|
-
function parseCloseInfo(payload) {
|
|
1322
|
-
if (payload.length === 0) return {};
|
|
1323
|
-
try {
|
|
1324
|
-
return decodeJsonPayload(payload);
|
|
1325
|
-
} catch {
|
|
1326
|
-
return {};
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
function sanitizeCloseCode(code) {
|
|
1330
|
-
if (code === void 0) return void 0;
|
|
1331
|
-
return code === WS_CLOSE_NORMAL || code >= 3e3 && code <= 4999 ? code : void 0;
|
|
1332
|
-
}
|
|
1333
|
-
function sendableCloseCode2(code) {
|
|
1334
|
-
if (code === void 0) return void 0;
|
|
1335
|
-
const legal = code >= 1e3 && code <= 1003 || code >= 1007 && code <= 1014 || code >= 3e3 && code <= 4999;
|
|
1336
|
-
return legal ? code : void 0;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// src/tunnel/v2/flow.ts
|
|
1340
|
-
var SendWindow = class {
|
|
1341
|
-
available;
|
|
1342
|
-
constructor(initial) {
|
|
1343
|
-
this.available = initial;
|
|
1344
|
-
}
|
|
1345
|
-
get availableCredit() {
|
|
1346
|
-
return this.available;
|
|
1347
|
-
}
|
|
1348
|
-
grantable(wanted) {
|
|
1349
|
-
return this.available <= 0 ? 0 : Math.min(this.available, wanted);
|
|
1350
|
-
}
|
|
1351
|
-
consume(n) {
|
|
1352
|
-
this.available = Math.max(0, this.available - n);
|
|
1353
|
-
}
|
|
1354
|
-
/** Returns false on overflow past Number.MAX_SAFE_INTEGER (protocol error). */
|
|
1355
|
-
increase(credit) {
|
|
1356
|
-
const next = this.available + credit;
|
|
1357
|
-
if (next > Number.MAX_SAFE_INTEGER) return false;
|
|
1358
|
-
this.available = next;
|
|
1359
|
-
return true;
|
|
1360
|
-
}
|
|
1361
|
-
};
|
|
1362
|
-
var RecvWindow = class {
|
|
1363
|
-
constructor(window) {
|
|
1364
|
-
this.window = window;
|
|
1365
|
-
}
|
|
1366
|
-
window;
|
|
1367
|
-
consumed = 0;
|
|
1368
|
-
/** Credit to emit as WINDOW_UPDATE once half the window is consumed, else 0. */
|
|
1369
|
-
recordConsumed(n) {
|
|
1370
|
-
this.consumed += n;
|
|
1371
|
-
if (this.window === 0 || this.consumed < Math.ceil(this.window / 2)) return 0;
|
|
1372
|
-
const credit = this.consumed;
|
|
1373
|
-
this.consumed = 0;
|
|
1374
|
-
return credit;
|
|
1375
|
-
}
|
|
1376
|
-
outstandingCapacity() {
|
|
1377
|
-
return Math.max(0, this.window - this.consumed);
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
1380
|
-
|
|
1381
|
-
// src/tunnel/v2/session.ts
|
|
1382
|
-
var SessionV2Error = class extends Error {
|
|
1383
|
-
};
|
|
1384
|
-
var StreamHandleV2 = class {
|
|
1385
|
-
constructor(session, streamId) {
|
|
1386
|
-
this.session = session;
|
|
1387
|
-
this.streamId = streamId;
|
|
1388
|
-
}
|
|
1389
|
-
session;
|
|
1390
|
-
streamId;
|
|
1391
|
-
accept(head) {
|
|
1392
|
-
return this.session.sendHead(this.streamId, FRAME_TYPE_V2.ACCEPT, head);
|
|
1393
|
-
}
|
|
1394
|
-
data(bytes) {
|
|
1395
|
-
return this.session.queueData(this.streamId, bytes);
|
|
1396
|
-
}
|
|
1397
|
-
end() {
|
|
1398
|
-
return this.session.sendEnd(this.streamId);
|
|
1399
|
-
}
|
|
1400
|
-
close(info2) {
|
|
1401
|
-
return this.session.closeStream(this.streamId, info2);
|
|
1402
|
-
}
|
|
1403
|
-
rst(code) {
|
|
1404
|
-
return this.session.rstStream(this.streamId, code);
|
|
1405
|
-
}
|
|
1406
|
-
/** Next event; null when the stream is gone. Reads drive window replenish. */
|
|
1407
|
-
recv() {
|
|
1408
|
-
return this.session.nextEvent(this.streamId);
|
|
1409
|
-
}
|
|
1410
|
-
};
|
|
1411
|
-
var CONNECTION_STREAM_ID = 0;
|
|
1412
|
-
var LIVENESS_PING_INTERVAL_MS = 15e3;
|
|
1413
|
-
var LIVENESS_TIMEOUT_MS = 45e3;
|
|
1414
|
-
var FROZEN_TICK_GAP_MS = 6e4;
|
|
1415
|
-
var textEncoder6 = new TextEncoder();
|
|
1416
|
-
var textDecoder5 = new TextDecoder();
|
|
1417
|
-
var SessionV2 = class {
|
|
1418
|
-
constructor(transport, options) {
|
|
1419
|
-
this.transport = transport;
|
|
1420
|
-
this.options = options;
|
|
1421
|
-
this.nextStreamId = options.role === "service" ? 1 : 2;
|
|
1422
|
-
this.connectionSend = new SendWindow(0);
|
|
1423
|
-
this.connectionRecv = new RecvWindow(options.local.connection_window);
|
|
1424
|
-
this.lastInboundAt = Date.now();
|
|
1425
|
-
this.lastTickAt = Date.now();
|
|
1426
|
-
this.livenessTimer = setInterval(() => this.onLivenessTick(), LIVENESS_PING_INTERVAL_MS);
|
|
1427
|
-
this.livenessTimer.unref?.();
|
|
1428
|
-
transport.onMessage((bytes) => this.onMessage(bytes));
|
|
1429
|
-
transport.onClose((reason) => this.teardown(reason));
|
|
1430
|
-
}
|
|
1431
|
-
transport;
|
|
1432
|
-
options;
|
|
1433
|
-
streams = /* @__PURE__ */ new Map();
|
|
1434
|
-
ready = [];
|
|
1435
|
-
connectionSend;
|
|
1436
|
-
connectionRecv;
|
|
1437
|
-
peer = null;
|
|
1438
|
-
nextStreamId;
|
|
1439
|
-
draining = false;
|
|
1440
|
-
closed = false;
|
|
1441
|
-
sending = Promise.resolve();
|
|
1442
|
-
lastInboundAt;
|
|
1443
|
-
lastTickAt;
|
|
1444
|
-
livenessTimer;
|
|
1445
|
-
onLivenessTick() {
|
|
1446
|
-
if (this.closed) return;
|
|
1447
|
-
const now = Date.now();
|
|
1448
|
-
const tickGap = now - this.lastTickAt;
|
|
1449
|
-
this.lastTickAt = now;
|
|
1450
|
-
if (tickGap < 0 || now < this.lastInboundAt) {
|
|
1451
|
-
this.lastInboundAt = now;
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
if (tickGap > FROZEN_TICK_GAP_MS) {
|
|
1455
|
-
this.abort(`process froze ${Math.round(tickGap / 1e3)}s (suspend/resume); redialing`);
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
if (now - this.lastInboundAt > LIVENESS_TIMEOUT_MS) {
|
|
1459
|
-
this.abort(`no inbound frames for ${Math.round((now - this.lastInboundAt) / 1e3)}s`);
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
void this.enqueueSend({
|
|
1463
|
-
streamId: CONNECTION_STREAM_ID,
|
|
1464
|
-
frameType: FRAME_TYPE_V2.PING,
|
|
1465
|
-
payload: new Uint8Array(0)
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
/** Presumed-dead teardown: no farewell frames — the socket is a zombie. */
|
|
1469
|
-
abort(reason) {
|
|
1470
|
-
this.teardown(reason);
|
|
1471
|
-
this.transport.close(1001, reason.slice(0, 100));
|
|
1472
|
-
}
|
|
1473
|
-
/** Send our HELLO; the peer's must arrive as its first frame. */
|
|
1474
|
-
async start() {
|
|
1475
|
-
const payload = textEncoder6.encode(JSON.stringify(this.options.local));
|
|
1476
|
-
await this.transport.send(
|
|
1477
|
-
encodeFrameV2({ streamId: CONNECTION_STREAM_ID, frameType: FRAME_TYPE_V2.HELLO, payload })
|
|
1478
|
-
);
|
|
1479
|
-
}
|
|
1480
|
-
get peerHello() {
|
|
1481
|
-
return this.peer;
|
|
1482
|
-
}
|
|
1483
|
-
async open(kind, head) {
|
|
1484
|
-
if (this.closed || this.draining) throw new SessionV2Error("session closed or draining");
|
|
1485
|
-
const peer = this.requirePeer();
|
|
1486
|
-
if (!peer.capabilities.includes(kind)) throw new SessionV2Error(`peer lacks ${kind}`);
|
|
1487
|
-
const streamId = this.nextStreamId;
|
|
1488
|
-
this.nextStreamId += 2;
|
|
1489
|
-
this.registerStream(streamId);
|
|
1490
|
-
await this.sendHead(streamId, FRAME_TYPE_V2.OPEN, { kind, head });
|
|
1491
|
-
return new StreamHandleV2(this, streamId);
|
|
1492
|
-
}
|
|
1493
|
-
async goAway(code, reason) {
|
|
1494
|
-
this.draining = true;
|
|
1495
|
-
await this.sendHead(CONNECTION_STREAM_ID, FRAME_TYPE_V2.GOAWAY, {
|
|
1496
|
-
code,
|
|
1497
|
-
last_stream_id: this.nextStreamId,
|
|
1498
|
-
...reason ? { reason } : {}
|
|
1499
|
-
});
|
|
1500
|
-
}
|
|
1501
|
-
requirePeer() {
|
|
1502
|
-
if (!this.peer) throw new SessionV2Error("handshake incomplete");
|
|
1503
|
-
return this.peer;
|
|
1504
|
-
}
|
|
1505
|
-
registerStream(streamId) {
|
|
1506
|
-
const peer = this.requirePeer();
|
|
1507
|
-
const state = {
|
|
1508
|
-
events: [],
|
|
1509
|
-
waiter: null,
|
|
1510
|
-
sendWindow: new SendWindow(peer.initial_stream_window),
|
|
1511
|
-
recvWindow: new RecvWindow(this.options.local.initial_stream_window),
|
|
1512
|
-
pending: [],
|
|
1513
|
-
localEnded: false,
|
|
1514
|
-
remoteEnded: false,
|
|
1515
|
-
finished: false
|
|
1516
|
-
};
|
|
1517
|
-
this.streams.set(streamId, state);
|
|
1518
|
-
return state;
|
|
1519
|
-
}
|
|
1520
|
-
onMessage(bytes) {
|
|
1521
|
-
if (this.closed) return;
|
|
1522
|
-
let frame;
|
|
1523
|
-
try {
|
|
1524
|
-
frame = decodeFrameV2(bytes, this.options.local.max_frame_payload);
|
|
1525
|
-
} catch (error) {
|
|
1526
|
-
this.fail(`undecodable frame: ${error.message}`);
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
this.lastInboundAt = Date.now();
|
|
1530
|
-
if (!this.peer) {
|
|
1531
|
-
this.onHandshakeFrame(frame);
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
void this.dispatch(frame);
|
|
1535
|
-
}
|
|
1536
|
-
onHandshakeFrame(frame) {
|
|
1537
|
-
if (frame.frameType !== FRAME_TYPE_V2.HELLO || frame.streamId !== CONNECTION_STREAM_ID) {
|
|
1538
|
-
this.fail("first frame was not HELLO");
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
try {
|
|
1542
|
-
const peer = parseHello(frame.payload);
|
|
1543
|
-
if (peer.version !== this.options.local.version) {
|
|
1544
|
-
this.fail(`peer version ${peer.version}`);
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
this.peer = peer;
|
|
1548
|
-
this.connectionSend = new SendWindow(peer.connection_window);
|
|
1549
|
-
} catch (error) {
|
|
1550
|
-
this.fail(error.message);
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
async dispatch(frame) {
|
|
1554
|
-
switch (frame.frameType) {
|
|
1555
|
-
case FRAME_TYPE_V2.HELLO:
|
|
1556
|
-
this.fail("HELLO after handshake");
|
|
1557
|
-
return;
|
|
1558
|
-
case FRAME_TYPE_V2.OPEN:
|
|
1559
|
-
this.onOpen(frame);
|
|
1560
|
-
return;
|
|
1561
|
-
case FRAME_TYPE_V2.ACCEPT:
|
|
1562
|
-
this.deliver(frame.streamId, { type: "accepted", head: parseJson(frame.payload) });
|
|
1563
|
-
return;
|
|
1564
|
-
case FRAME_TYPE_V2.DATA:
|
|
1565
|
-
this.onData(frame);
|
|
1566
|
-
return;
|
|
1567
|
-
case FRAME_TYPE_V2.END:
|
|
1568
|
-
this.onEnd(frame.streamId);
|
|
1569
|
-
return;
|
|
1570
|
-
case FRAME_TYPE_V2.CLOSE:
|
|
1571
|
-
this.finishStream(frame.streamId, { type: "close", info: parseJson(frame.payload) });
|
|
1572
|
-
return;
|
|
1573
|
-
case FRAME_TYPE_V2.RST:
|
|
1574
|
-
this.finishStream(frame.streamId, {
|
|
1575
|
-
type: "rst",
|
|
1576
|
-
code: payloadU32(frame) ?? ERROR_CODE.PROTOCOL_ERROR
|
|
1577
|
-
});
|
|
1578
|
-
return;
|
|
1579
|
-
case FRAME_TYPE_V2.WINDOW_UPDATE:
|
|
1580
|
-
this.onWindowUpdate(frame);
|
|
1581
|
-
return;
|
|
1582
|
-
case FRAME_TYPE_V2.PING:
|
|
1583
|
-
this.enqueueSend({ streamId: 0, frameType: FRAME_TYPE_V2.PONG, payload: frame.payload });
|
|
1584
|
-
return;
|
|
1585
|
-
case FRAME_TYPE_V2.GOAWAY:
|
|
1586
|
-
this.draining = true;
|
|
1587
|
-
return;
|
|
1588
|
-
default:
|
|
1589
|
-
return;
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
onOpen(frame) {
|
|
1593
|
-
const streamId = frame.streamId;
|
|
1594
|
-
const allocatesLocally = this.options.role === "service" ? streamId % 2 === 1 : streamId % 2 === 0;
|
|
1595
|
-
if (streamId === CONNECTION_STREAM_ID || allocatesLocally || this.streams.has(streamId)) {
|
|
1596
|
-
this.fail(`peer opened stream ${streamId}`);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
if (this.streams.size >= this.options.local.max_concurrent_streams || this.draining) {
|
|
1600
|
-
this.enqueueSend(rstFrame(streamId, ERROR_CODE.REFUSED));
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
const open = parseJson(frame.payload);
|
|
1604
|
-
if (!open || typeof open.kind !== "string") {
|
|
1605
|
-
this.enqueueSend(rstFrame(streamId, ERROR_CODE.PROTOCOL_ERROR));
|
|
1606
|
-
return;
|
|
1607
|
-
}
|
|
1608
|
-
if (!this.options.local.capabilities.includes(open.kind)) {
|
|
1609
|
-
this.enqueueSend(rstFrame(streamId, ERROR_CODE.UNSUPPORTED));
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
this.registerStream(streamId);
|
|
1613
|
-
this.options.onIncoming({
|
|
1614
|
-
kind: open.kind,
|
|
1615
|
-
head: open.head ?? null,
|
|
1616
|
-
stream: new StreamHandleV2(this, streamId)
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
onData(frame) {
|
|
1620
|
-
const len = frame.payload.length;
|
|
1621
|
-
if (len > this.connectionRecv.outstandingCapacity()) {
|
|
1622
|
-
this.fail("connection window overrun");
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
1625
|
-
const state = this.streams.get(frame.streamId);
|
|
1626
|
-
if (!state) {
|
|
1627
|
-
this.enqueueSend(rstFrame(frame.streamId, ERROR_CODE.STREAM_CLOSED));
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
if (len > state.recvWindow.outstandingCapacity()) {
|
|
1631
|
-
this.enqueueSend(rstFrame(frame.streamId, ERROR_CODE.FLOW_CONTROL_ERROR));
|
|
1632
|
-
this.dropStream(frame.streamId);
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
this.deliver(frame.streamId, { type: "data", payload: frame.payload });
|
|
1636
|
-
}
|
|
1637
|
-
onEnd(streamId) {
|
|
1638
|
-
const state = this.streams.get(streamId);
|
|
1639
|
-
if (!state) return;
|
|
1640
|
-
state.remoteEnded = true;
|
|
1641
|
-
this.deliver(streamId, { type: "end" });
|
|
1642
|
-
if (state.localEnded) this.dropStream(streamId);
|
|
1643
|
-
}
|
|
1644
|
-
onWindowUpdate(frame) {
|
|
1645
|
-
const credit = payloadU32(frame);
|
|
1646
|
-
if (!credit) {
|
|
1647
|
-
this.fail("bad WINDOW_UPDATE");
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
1650
|
-
const grew = frame.streamId === CONNECTION_STREAM_ID ? this.connectionSend.increase(credit) : this.streams.get(frame.streamId)?.sendWindow.increase(credit) ?? true;
|
|
1651
|
-
if (!grew) {
|
|
1652
|
-
this.fail("window overflow");
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
this.drainReady();
|
|
1656
|
-
}
|
|
1657
|
-
// --- outbound path -------------------------------------------------------
|
|
1658
|
-
queueData(streamId, bytes) {
|
|
1659
|
-
const state = this.streams.get(streamId);
|
|
1660
|
-
if (!state || this.closed) return Promise.reject(new SessionV2Error("stream closed"));
|
|
1661
|
-
return new Promise((resolve, reject) => {
|
|
1662
|
-
state.pending.push({ bytes, resolve, reject });
|
|
1663
|
-
if (!this.ready.includes(streamId)) this.ready.push(streamId);
|
|
1664
|
-
this.drainReady();
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
/** Round-robin: one window-and-frame-capped chunk per ready stream per turn. */
|
|
1668
|
-
drainReady() {
|
|
1669
|
-
const peer = this.peer;
|
|
1670
|
-
if (!peer) return;
|
|
1671
|
-
let turns = this.ready.length;
|
|
1672
|
-
while (turns > 0) {
|
|
1673
|
-
turns -= 1;
|
|
1674
|
-
const streamId = this.ready.shift();
|
|
1675
|
-
if (streamId === void 0) break;
|
|
1676
|
-
const state = this.streams.get(streamId);
|
|
1677
|
-
if (!state) continue;
|
|
1678
|
-
const item = state.pending[0];
|
|
1679
|
-
if (!item) continue;
|
|
1680
|
-
const wanted = Math.min(item.bytes.length, peer.max_frame_payload);
|
|
1681
|
-
const grant = this.connectionSend.grantable(state.sendWindow.grantable(wanted));
|
|
1682
|
-
if (grant === 0) {
|
|
1683
|
-
this.ready.push(streamId);
|
|
1684
|
-
if (this.connectionSend.availableCredit <= 0) break;
|
|
1685
|
-
continue;
|
|
1686
|
-
}
|
|
1687
|
-
const chunk = item.bytes.subarray(0, grant);
|
|
1688
|
-
item.bytes = item.bytes.subarray(grant);
|
|
1689
|
-
state.sendWindow.consume(grant);
|
|
1690
|
-
this.connectionSend.consume(grant);
|
|
1691
|
-
const finished = item.bytes.length === 0;
|
|
1692
|
-
if (finished) state.pending.shift();
|
|
1693
|
-
if (state.pending.length > 0) {
|
|
1694
|
-
this.ready.push(streamId);
|
|
1695
|
-
turns += 1;
|
|
1696
|
-
}
|
|
1697
|
-
this.enqueueSend({ streamId, frameType: FRAME_TYPE_V2.DATA, payload: chunk });
|
|
1698
|
-
if (finished) item.resolve();
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
sendHead(streamId, frameType, value) {
|
|
1702
|
-
const payload = textEncoder6.encode(JSON.stringify(value ?? null));
|
|
1703
|
-
if (payload.length > MAX_HEAD_BYTES) {
|
|
1704
|
-
return Promise.reject(new SessionV2Error("head exceeds MAX_HEAD_BYTES"));
|
|
1705
|
-
}
|
|
1706
|
-
return this.enqueueSend({ streamId, frameType, payload });
|
|
1707
|
-
}
|
|
1708
|
-
sendEnd(streamId) {
|
|
1709
|
-
const state = this.streams.get(streamId);
|
|
1710
|
-
const result = this.enqueueSend({
|
|
1711
|
-
streamId,
|
|
1712
|
-
frameType: FRAME_TYPE_V2.END,
|
|
1713
|
-
payload: new Uint8Array(0)
|
|
1714
|
-
});
|
|
1715
|
-
if (state) {
|
|
1716
|
-
state.localEnded = true;
|
|
1717
|
-
if (state.remoteEnded) this.dropStream(streamId);
|
|
1718
|
-
}
|
|
1719
|
-
return result;
|
|
1720
|
-
}
|
|
1721
|
-
async closeStream(streamId, info2) {
|
|
1722
|
-
await this.sendHead(streamId, FRAME_TYPE_V2.CLOSE, info2);
|
|
1723
|
-
this.dropStream(streamId);
|
|
1724
|
-
}
|
|
1725
|
-
async rstStream(streamId, code) {
|
|
1726
|
-
await this.enqueueSend(rstFrame(streamId, code));
|
|
1727
|
-
this.dropStream(streamId);
|
|
1728
|
-
}
|
|
1729
|
-
/** Serialize transport writes so frames never interleave mid-encode. */
|
|
1730
|
-
enqueueSend(frame) {
|
|
1731
|
-
const encoded = encodeFrameV2(frame);
|
|
1732
|
-
const next = this.sending.then(() => this.transport.send(encoded));
|
|
1733
|
-
this.sending = next.catch(() => {
|
|
1734
|
-
});
|
|
1735
|
-
return next;
|
|
1736
|
-
}
|
|
1737
|
-
// --- delivery ------------------------------------------------------------
|
|
1738
|
-
nextEvent(streamId) {
|
|
1739
|
-
const state = this.streams.get(streamId);
|
|
1740
|
-
if (!state) return Promise.resolve(null);
|
|
1741
|
-
const queued = state.events.shift();
|
|
1742
|
-
if (queued) {
|
|
1743
|
-
this.recordConsumption(streamId, queued);
|
|
1744
|
-
if (state.finished && state.events.length === 0) this.streams.delete(streamId);
|
|
1745
|
-
return Promise.resolve(queued);
|
|
1746
|
-
}
|
|
1747
|
-
if (state.finished) {
|
|
1748
|
-
this.streams.delete(streamId);
|
|
1749
|
-
return Promise.resolve(null);
|
|
1750
|
-
}
|
|
1751
|
-
return new Promise((resolve) => {
|
|
1752
|
-
state.waiter = (event) => {
|
|
1753
|
-
if (event) this.recordConsumption(streamId, event);
|
|
1754
|
-
resolve(event);
|
|
1755
|
-
};
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
recordConsumption(streamId, event) {
|
|
1759
|
-
if (event.type !== "data") return;
|
|
1760
|
-
const bytes = event.payload.length;
|
|
1761
|
-
const connectionCredit = this.connectionRecv.recordConsumed(bytes);
|
|
1762
|
-
if (connectionCredit > 0) {
|
|
1763
|
-
this.enqueueSend(windowUpdateFrame(CONNECTION_STREAM_ID, connectionCredit));
|
|
1764
|
-
}
|
|
1765
|
-
const state = this.streams.get(streamId);
|
|
1766
|
-
if (state) {
|
|
1767
|
-
const streamCredit = state.recvWindow.recordConsumed(bytes);
|
|
1768
|
-
if (streamCredit > 0) this.enqueueSend(windowUpdateFrame(streamId, streamCredit));
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
deliver(streamId, event) {
|
|
1772
|
-
const state = this.streams.get(streamId);
|
|
1773
|
-
if (!state) return;
|
|
1774
|
-
if (state.waiter) {
|
|
1775
|
-
const waiter = state.waiter;
|
|
1776
|
-
state.waiter = null;
|
|
1777
|
-
waiter(event);
|
|
1778
|
-
} else {
|
|
1779
|
-
state.events.push(event);
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
finishStream(streamId, event) {
|
|
1783
|
-
this.deliver(streamId, event);
|
|
1784
|
-
this.dropStream(streamId);
|
|
1785
|
-
}
|
|
1786
|
-
dropStream(streamId) {
|
|
1787
|
-
const state = this.streams.get(streamId);
|
|
1788
|
-
if (!state) return;
|
|
1789
|
-
state.finished = true;
|
|
1790
|
-
for (const pending of state.pending) {
|
|
1791
|
-
pending.reject(new SessionV2Error("stream closed"));
|
|
1792
|
-
}
|
|
1793
|
-
state.pending = [];
|
|
1794
|
-
const readyIndex = this.ready.indexOf(streamId);
|
|
1795
|
-
if (readyIndex !== -1) this.ready.splice(readyIndex, 1);
|
|
1796
|
-
if (state.waiter) {
|
|
1797
|
-
const waiter = state.waiter;
|
|
1798
|
-
state.waiter = null;
|
|
1799
|
-
this.streams.delete(streamId);
|
|
1800
|
-
waiter(null);
|
|
1801
|
-
} else if (state.events.length === 0) {
|
|
1802
|
-
this.streams.delete(streamId);
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
fail(reason) {
|
|
1806
|
-
void this.sendHead(CONNECTION_STREAM_ID, FRAME_TYPE_V2.GOAWAY, {
|
|
1807
|
-
code: ERROR_CODE.PROTOCOL_ERROR,
|
|
1808
|
-
last_stream_id: this.nextStreamId,
|
|
1809
|
-
reason
|
|
1810
|
-
});
|
|
1811
|
-
this.teardown(reason);
|
|
1812
|
-
this.transport.close(1002, "protocol error");
|
|
1813
|
-
}
|
|
1814
|
-
teardown(reason) {
|
|
1815
|
-
if (this.closed) return;
|
|
1816
|
-
this.closed = true;
|
|
1817
|
-
clearInterval(this.livenessTimer);
|
|
1818
|
-
for (const streamId of this.streams.keys()) {
|
|
1819
|
-
this.dropStream(streamId);
|
|
1820
|
-
}
|
|
1821
|
-
this.options.onClosed?.(reason);
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
function parseJson(payload) {
|
|
1825
|
-
if (payload.length === 0) return null;
|
|
1826
|
-
try {
|
|
1827
|
-
return JSON.parse(textDecoder5.decode(payload));
|
|
1828
|
-
} catch {
|
|
1829
|
-
return null;
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
// src/tunnel/v2/ws-transport.ts
|
|
1834
|
-
var WsTransportV2 = class {
|
|
1835
|
-
constructor(socket) {
|
|
1836
|
-
this.socket = socket;
|
|
1837
|
-
socket.binaryType = "arraybuffer";
|
|
1838
|
-
socket.addEventListener("message", (event) => {
|
|
1839
|
-
const data = event.data;
|
|
1840
|
-
if (data instanceof ArrayBuffer) {
|
|
1841
|
-
this.messageListener?.(new Uint8Array(data));
|
|
1842
|
-
} else if (data instanceof Uint8Array) {
|
|
1843
|
-
this.messageListener?.(data);
|
|
1844
|
-
}
|
|
1845
|
-
});
|
|
1846
|
-
socket.addEventListener(
|
|
1847
|
-
"close",
|
|
1848
|
-
(event) => this.closeListener?.(`connection closed (${event.code ?? "unknown"})`)
|
|
1849
|
-
);
|
|
1850
|
-
socket.addEventListener("error", () => this.closeListener?.("connection error"));
|
|
1851
|
-
}
|
|
1852
|
-
socket;
|
|
1853
|
-
messageListener = null;
|
|
1854
|
-
closeListener = null;
|
|
1855
|
-
send(frame) {
|
|
1856
|
-
this.socket.send(frame);
|
|
1857
|
-
return Promise.resolve();
|
|
1858
|
-
}
|
|
1859
|
-
onMessage(listener) {
|
|
1860
|
-
this.messageListener = listener;
|
|
1861
|
-
}
|
|
1862
|
-
onClose(listener) {
|
|
1863
|
-
this.closeListener = listener;
|
|
1864
|
-
}
|
|
1865
|
-
close(code, reason) {
|
|
1866
|
-
try {
|
|
1867
|
-
this.socket.close(code, reason);
|
|
1868
|
-
} catch {
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
};
|
|
1872
|
-
|
|
1873
|
-
// src/tunnel/negotiating-client.ts
|
|
1874
|
-
var NegotiatingAgentClient = class {
|
|
1875
|
-
constructor(options) {
|
|
1876
|
-
this.options = options;
|
|
1877
|
-
}
|
|
1878
|
-
options;
|
|
1879
|
-
socket = null;
|
|
1880
|
-
driver = null;
|
|
1881
|
-
closed = false;
|
|
1882
|
-
start() {
|
|
1883
|
-
const factory = this.options.createSocket ?? defaultSocketFactory;
|
|
1884
|
-
const socket = factory(connectUrl(this.options.url, this.options.token), [
|
|
1885
|
-
SUBPROTOCOL_V2,
|
|
1886
|
-
SUBPROTOCOL
|
|
1887
|
-
]);
|
|
1888
|
-
socket.binaryType = "arraybuffer";
|
|
1889
|
-
this.socket = socket;
|
|
1890
|
-
socket.addEventListener("open", () => {
|
|
1891
|
-
if (this.closed || this.driver) return;
|
|
1892
|
-
this.driver = socket.protocol === SUBPROTOCOL_V2 ? this.startV2(socket) : this.startV1(socket);
|
|
1893
|
-
});
|
|
1894
|
-
socket.addEventListener(
|
|
1895
|
-
"close",
|
|
1896
|
-
(event) => this.teardown(`connection closed (${event.code ?? "unknown"})`, event.code)
|
|
1897
|
-
);
|
|
1898
|
-
socket.addEventListener("error", () => this.teardown("connection error"));
|
|
1899
|
-
}
|
|
1900
|
-
stop() {
|
|
1901
|
-
if (this.closed) return;
|
|
1902
|
-
this.closed = true;
|
|
1903
|
-
this.driver?.stop();
|
|
1904
|
-
this.driver = null;
|
|
1905
|
-
try {
|
|
1906
|
-
this.socket?.close(1e3);
|
|
1907
|
-
} catch {
|
|
1908
|
-
}
|
|
1909
|
-
this.socket = null;
|
|
1910
|
-
}
|
|
1911
|
-
teardown(reason, closeCode) {
|
|
1912
|
-
if (this.closed) return;
|
|
1913
|
-
this.closed = true;
|
|
1914
|
-
this.driver?.stop();
|
|
1915
|
-
this.driver = null;
|
|
1916
|
-
this.socket = null;
|
|
1917
|
-
this.options.events?.onClosed?.(reason, closeCode);
|
|
1918
|
-
}
|
|
1919
|
-
startV2(socket) {
|
|
1920
|
-
const hello = defaultHello();
|
|
1921
|
-
const transport = new WsTransportV2(socket);
|
|
1922
|
-
const createLocalSocket = this.options.createLocalSocket ?? defaultSocketFactory;
|
|
1923
|
-
const dispatcher = new AgentDispatcher().register(new HttpProxyKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(new WsSpliceKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(
|
|
1924
|
-
new ControlKind({
|
|
1925
|
-
fs: new NodeControlFs(this.options.workspaceRoot),
|
|
1926
|
-
watcher: new NodeControlWatcher(this.options.workspaceRoot),
|
|
1927
|
-
exec: new NodeControlExec(this.options.workspaceRoot)
|
|
1928
|
-
})
|
|
1929
|
-
);
|
|
1930
|
-
const session = new SessionV2(transport, {
|
|
1931
|
-
role: "agent",
|
|
1932
|
-
local: hello,
|
|
1933
|
-
onIncoming: (incoming) => dispatcher.dispatch(incoming),
|
|
1934
|
-
onClosed: (reason) => this.teardown(`v2 session closed \u2014 ${reason}`)
|
|
1935
|
-
});
|
|
1936
|
-
void session.start();
|
|
1937
|
-
this.options.events?.onConnected?.();
|
|
1938
|
-
return { stop: () => transport.close(1e3, "stopped") };
|
|
1939
|
-
}
|
|
1940
|
-
startV1(socket) {
|
|
1941
|
-
const client = new TunnelClient({
|
|
1942
|
-
url: this.options.url,
|
|
1943
|
-
token: this.options.token,
|
|
1944
|
-
localPort: this.options.localPort,
|
|
1945
|
-
createSocket: () => socket,
|
|
1946
|
-
createLocalSocket: this.options.createLocalSocket,
|
|
1947
|
-
events: this.options.events,
|
|
1948
|
-
socketAlreadyOpen: true
|
|
1949
|
-
});
|
|
1950
|
-
client.start();
|
|
1951
|
-
return { stop: () => client.stop() };
|
|
1952
|
-
}
|
|
1953
|
-
};
|
|
1954
|
-
function makeAgentContext(localPort, hello, createLocalSocket) {
|
|
1955
|
-
return {
|
|
1956
|
-
localPort,
|
|
1957
|
-
maxFramePayload: hello.max_frame_payload,
|
|
1958
|
-
createLocalSocket: (url, protocols) => createLocalSocket(url, protocols)
|
|
1959
|
-
};
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// src/tunnel/manager.ts
|
|
1963
|
-
var CONFIG_POLL_MS = 2e3;
|
|
1964
|
-
var RECONNECT_MIN_MS = 1e3;
|
|
1965
|
-
var RECONNECT_MAX_MS = 3e4;
|
|
1966
|
-
var STAND_DOWN_RETRY_MS = 5 * 6e4;
|
|
1967
|
-
var CLOSE_CODE_REPLACED = 4e3;
|
|
1968
|
-
function makeDefaultCreateClient(workspaceRoot) {
|
|
1969
|
-
return (config, events) => new NegotiatingAgentClient({
|
|
1970
|
-
url: config.url,
|
|
1971
|
-
token: config.token,
|
|
1972
|
-
localPort: config.localPort,
|
|
1973
|
-
workspaceRoot,
|
|
1974
|
-
events
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
|
-
function startTunnelRuntime(cwd, options = {}) {
|
|
1978
|
-
const pollIntervalMs = options.pollIntervalMs ?? CONFIG_POLL_MS;
|
|
1979
|
-
const createClient = options.createClient ?? makeDefaultCreateClient(cwd);
|
|
1980
|
-
const onStatus = options.onStatus ?? ((message) => info(dim(message)));
|
|
1981
|
-
let stopped = false;
|
|
1982
|
-
let generation = 0;
|
|
1983
|
-
let activeConfig = null;
|
|
1984
|
-
let activeClient = null;
|
|
1985
|
-
let reconnectDelay = RECONNECT_MIN_MS;
|
|
1986
|
-
let reconnectTimer = null;
|
|
1987
|
-
let polling = false;
|
|
1988
|
-
let connected = false;
|
|
1989
|
-
let standDown = false;
|
|
1990
|
-
const clearReconnect = () => {
|
|
1991
|
-
if (reconnectTimer !== null) {
|
|
1992
|
-
clearTimeout(reconnectTimer);
|
|
1993
|
-
reconnectTimer = null;
|
|
1994
|
-
}
|
|
1995
|
-
};
|
|
1996
|
-
let backoffResetTimer = null;
|
|
1997
|
-
const startClient = (config) => {
|
|
1998
|
-
generation += 1;
|
|
1999
|
-
standDown = false;
|
|
2000
|
-
const myGeneration = generation;
|
|
2001
|
-
const client = createClient(config, {
|
|
2002
|
-
onConnected: () => {
|
|
2003
|
-
if (stopped || myGeneration !== generation) return;
|
|
2004
|
-
connected = true;
|
|
2005
|
-
if (backoffResetTimer) clearTimeout(backoffResetTimer);
|
|
2006
|
-
backoffResetTimer = setTimeout(() => {
|
|
2007
|
-
backoffResetTimer = null;
|
|
2008
|
-
if (!stopped && connected && myGeneration === generation) {
|
|
2009
|
-
reconnectDelay = RECONNECT_MIN_MS;
|
|
2010
|
-
}
|
|
2011
|
-
}, 1e4);
|
|
2012
|
-
backoffResetTimer.unref?.();
|
|
2013
|
-
onStatus("(preview tunnel: connected)");
|
|
2014
|
-
},
|
|
2015
|
-
onClosed: (reason, closeCode) => {
|
|
2016
|
-
if (stopped || myGeneration !== generation) return;
|
|
2017
|
-
connected = false;
|
|
2018
|
-
if (closeCode === CLOSE_CODE_REPLACED) {
|
|
2019
|
-
standDown = true;
|
|
2020
|
-
onStatus("(preview tunnel: replaced by a newer agent; standing down)");
|
|
2021
|
-
reconnectDelay = STAND_DOWN_RETRY_MS;
|
|
2022
|
-
scheduleReconnect();
|
|
2023
|
-
return;
|
|
2024
|
-
}
|
|
2025
|
-
onStatus(`(preview tunnel: disconnected \u2014 ${reason})`);
|
|
2026
|
-
scheduleReconnect();
|
|
2027
|
-
}
|
|
2028
|
-
});
|
|
2029
|
-
activeConfig = config;
|
|
2030
|
-
activeClient = client;
|
|
2031
|
-
client.start();
|
|
2032
|
-
};
|
|
2033
|
-
const retireClient = () => {
|
|
2034
|
-
generation += 1;
|
|
2035
|
-
activeClient?.stop();
|
|
2036
|
-
activeClient = null;
|
|
2037
|
-
};
|
|
2038
|
-
const scheduleReconnect = () => {
|
|
2039
|
-
if (reconnectTimer !== null) return;
|
|
2040
|
-
const delay = reconnectDelay + Math.floor(Math.random() * (reconnectDelay / 4));
|
|
2041
|
-
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
2042
|
-
reconnectTimer = setTimeout(() => {
|
|
2043
|
-
reconnectTimer = null;
|
|
2044
|
-
if (stopped || activeConfig === null) return;
|
|
2045
|
-
retireClient();
|
|
2046
|
-
startClient(activeConfig);
|
|
2047
|
-
}, delay);
|
|
2048
|
-
};
|
|
2049
|
-
const stopClient = () => {
|
|
2050
|
-
clearReconnect();
|
|
2051
|
-
retireClient();
|
|
2052
|
-
activeConfig = null;
|
|
2053
|
-
connected = false;
|
|
2054
|
-
};
|
|
2055
|
-
const poll = async () => {
|
|
2056
|
-
if (polling || stopped) return;
|
|
2057
|
-
polling = true;
|
|
2058
|
-
try {
|
|
2059
|
-
const config = await readTunnelConfig(cwd, options.env);
|
|
2060
|
-
if (stopped || sameTunnelConfig(config, activeConfig)) return;
|
|
2061
|
-
if (config === null) {
|
|
2062
|
-
stopClient();
|
|
2063
|
-
onStatus("(preview tunnel: credentials removed, stopped)");
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
if (activeConfig && sameTunnelEndpoint(config, activeConfig) && connected && !standDown) {
|
|
2067
|
-
activeConfig = config;
|
|
2068
|
-
return;
|
|
2069
|
-
}
|
|
2070
|
-
stopClient();
|
|
2071
|
-
onStatus("(preview tunnel: credentials found, connecting)");
|
|
2072
|
-
reconnectDelay = RECONNECT_MIN_MS;
|
|
2073
|
-
startClient(config);
|
|
2074
|
-
} finally {
|
|
2075
|
-
polling = false;
|
|
2076
|
-
}
|
|
2077
|
-
};
|
|
2078
|
-
const interval = setInterval(() => {
|
|
2079
|
-
void poll();
|
|
2080
|
-
}, pollIntervalMs);
|
|
2081
|
-
void poll();
|
|
2082
|
-
return () => {
|
|
2083
|
-
if (stopped) return;
|
|
2084
|
-
stopped = true;
|
|
2085
|
-
clearInterval(interval);
|
|
2086
|
-
stopClient();
|
|
2087
|
-
};
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
export { TUNNEL_URL_ENV, startTunnelRuntime };
|