@zgeoff/imp-client 0.7.0 → 0.9.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/index.d.ts +376 -2
- package/dist/index.js +342 -16
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { RPCLink } from "@orpc/client/fetch";
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
//#endregion
|
|
5
5
|
//#region src/check-server.ts
|
|
6
|
-
const CLIENT_VERSION = "0.
|
|
6
|
+
const CLIENT_VERSION = "0.9.0";
|
|
7
7
|
async function checkServer(rpc) {
|
|
8
8
|
const info = await rpc.system.info();
|
|
9
9
|
return {
|
|
@@ -29,6 +29,29 @@ function parseVersion(version) {
|
|
|
29
29
|
//#region ../api/src/name-schema.ts
|
|
30
30
|
const NameSchema = z.string().regex(/^[a-z][a-z0-9-]{0,30}$/, "must be a lowercase letter followed by up to 30 lowercase letters, digits or hyphens");
|
|
31
31
|
//#endregion
|
|
32
|
+
//#region ../api/src/image-schema.ts
|
|
33
|
+
const ImageSchema = z.object({
|
|
34
|
+
id: z.string(),
|
|
35
|
+
name: NameSchema,
|
|
36
|
+
ref: z.string(),
|
|
37
|
+
digest: z.string(),
|
|
38
|
+
createdAt: z.date(),
|
|
39
|
+
sizeBytes: z.int().nonnegative()
|
|
40
|
+
});
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region ../api/src/image-build-protocol.ts
|
|
43
|
+
const IMAGE_BUILD_PATH = "/images/build";
|
|
44
|
+
const DockerfilePathSchema = z.string().min(1).refine((path) => !path.startsWith("/") && !path.split(/[/\\]/u).includes(".."), { message: "the Dockerfile path must be relative and stay inside the build context" });
|
|
45
|
+
z.object({
|
|
46
|
+
name: NameSchema,
|
|
47
|
+
dockerfile: DockerfilePathSchema.optional()
|
|
48
|
+
});
|
|
49
|
+
const ImageBuildResultSchema = ImageSchema.extend({ createdAt: z.coerce.date() });
|
|
50
|
+
const ImageBuildErrorSchema = z.object({
|
|
51
|
+
code: z.string(),
|
|
52
|
+
message: z.string()
|
|
53
|
+
});
|
|
54
|
+
//#endregion
|
|
32
55
|
//#region ../api/src/session-schema.ts
|
|
33
56
|
const SessionNameSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,31}$/, "must be a lowercase letter or digit followed by up to 31 lowercase letters, digits or hyphens");
|
|
34
57
|
const SessionExitSchema = z.object({
|
|
@@ -66,10 +89,14 @@ const ExecStartMessageSchema = z.object({
|
|
|
66
89
|
cols: DimensionSchema.optional(),
|
|
67
90
|
rows: DimensionSchema.optional(),
|
|
68
91
|
session: SessionNameSchema.optional(),
|
|
69
|
-
tool: z.enum(["tar"]).optional()
|
|
92
|
+
tool: z.enum(["tar"]).optional(),
|
|
93
|
+
killGraceMs: z.int().min(1).max(6e4).optional()
|
|
70
94
|
}).refine((start) => start.session === void 0 || start.tty, {
|
|
71
95
|
message: "a session needs a tty",
|
|
72
96
|
path: ["tty"]
|
|
97
|
+
}).refine((start) => start.killGraceMs === void 0 || !start.tty, {
|
|
98
|
+
message: "a tty exec takes no kill grace",
|
|
99
|
+
path: ["killGraceMs"]
|
|
73
100
|
}).refine((start) => start.tool === void 0 || !start.tty && start.session === void 0 && start.env === void 0 && start.cwd === void 0, {
|
|
74
101
|
message: "a tool takes no tty, session, env or cwd",
|
|
75
102
|
path: ["tool"]
|
|
@@ -104,7 +131,8 @@ const ExecServerMessageSchema = z.discriminatedUnion("type", [
|
|
|
104
131
|
type: z.literal("started"),
|
|
105
132
|
pid: z.int().positive(),
|
|
106
133
|
session: SessionNameSchema.optional(),
|
|
107
|
-
created: z.boolean().optional()
|
|
134
|
+
created: z.boolean().optional(),
|
|
135
|
+
groupKill: z.boolean().optional()
|
|
108
136
|
}),
|
|
109
137
|
z.object({
|
|
110
138
|
type: z.literal("exit"),
|
|
@@ -148,6 +176,63 @@ function isExecChannel(value) {
|
|
|
148
176
|
return value === EXEC_CHANNELS.stdin || value === EXEC_CHANNELS.stdout || value === EXEC_CHANNELS.stderr;
|
|
149
177
|
}
|
|
150
178
|
//#endregion
|
|
179
|
+
//#region ../api/src/tunnel-protocol.ts
|
|
180
|
+
const TUNNEL_PATH = "/tunnel";
|
|
181
|
+
const TUNNEL_CLOSE_NORMAL = 1e3;
|
|
182
|
+
const TUNNEL_CLOSE_PROTOCOL = 1002;
|
|
183
|
+
const TUNNEL_WINDOW_BYTES = 1048576;
|
|
184
|
+
const TUNNEL_MAX_FRAME_BYTES = 65536;
|
|
185
|
+
const AckSchema = z.object({
|
|
186
|
+
type: z.literal("ack"),
|
|
187
|
+
bytes: z.int().positive()
|
|
188
|
+
});
|
|
189
|
+
const TunnelListenSchema = z.object({
|
|
190
|
+
type: z.literal("listen"),
|
|
191
|
+
name: NameSchema,
|
|
192
|
+
network: z.enum(["unix", "tcp"]),
|
|
193
|
+
path: z.string().startsWith("/").nullable().optional(),
|
|
194
|
+
port: z.int().min(0).max(65535).optional()
|
|
195
|
+
}).refine((listen) => listen.network === "tcp" ? listen.port !== void 0 && listen.path === void 0 : listen.port === void 0, {
|
|
196
|
+
message: "tcp takes a port, unix a path or null",
|
|
197
|
+
path: ["network"]
|
|
198
|
+
});
|
|
199
|
+
z.discriminatedUnion("type", [
|
|
200
|
+
z.object({
|
|
201
|
+
type: z.literal("open"),
|
|
202
|
+
name: NameSchema,
|
|
203
|
+
port: z.int().min(1).max(65535)
|
|
204
|
+
}),
|
|
205
|
+
z.object({ type: z.literal("eof") }),
|
|
206
|
+
AckSchema,
|
|
207
|
+
TunnelListenSchema,
|
|
208
|
+
z.object({
|
|
209
|
+
type: z.literal("accept"),
|
|
210
|
+
name: NameSchema,
|
|
211
|
+
listener: z.string().min(1),
|
|
212
|
+
connection: z.int().positive()
|
|
213
|
+
})
|
|
214
|
+
]);
|
|
215
|
+
const TunnelServerMessageSchema = z.discriminatedUnion("type", [
|
|
216
|
+
z.object({ type: z.literal("opened") }),
|
|
217
|
+
z.object({ type: z.literal("eof") }),
|
|
218
|
+
AckSchema,
|
|
219
|
+
z.object({
|
|
220
|
+
type: z.literal("listening"),
|
|
221
|
+
listener: z.string(),
|
|
222
|
+
path: z.string().nullable(),
|
|
223
|
+
port: z.int().nullable()
|
|
224
|
+
}),
|
|
225
|
+
z.object({
|
|
226
|
+
type: z.literal("connection"),
|
|
227
|
+
id: z.int().positive()
|
|
228
|
+
}),
|
|
229
|
+
z.object({
|
|
230
|
+
type: z.literal("error"),
|
|
231
|
+
message: z.string(),
|
|
232
|
+
code: z.string().optional()
|
|
233
|
+
})
|
|
234
|
+
]);
|
|
235
|
+
//#endregion
|
|
151
236
|
//#region ../api/src/login-shell.ts
|
|
152
237
|
const CONSOLE_SHELL = [
|
|
153
238
|
"uid=$(id -u 2>/dev/null) || uid=0",
|
|
@@ -160,6 +245,44 @@ const CONSOLE_SHELL = [
|
|
|
160
245
|
"exec \"$shell\" -l"
|
|
161
246
|
].join("\n");
|
|
162
247
|
//#endregion
|
|
248
|
+
//#region src/resolve-impd-url.ts
|
|
249
|
+
function resolveImpdUrl(base, path) {
|
|
250
|
+
const url = new URL(base);
|
|
251
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}${path}`;
|
|
252
|
+
return url;
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/build-image.ts
|
|
256
|
+
async function buildImage(deps, name, context, options = {}) {
|
|
257
|
+
const url = resolveImpdUrl(deps.baseUrl, IMAGE_BUILD_PATH);
|
|
258
|
+
url.searchParams.set("name", name);
|
|
259
|
+
if (options.dockerfile !== void 0) url.searchParams.set("dockerfile", options.dockerfile);
|
|
260
|
+
const headers = { "content-type": "application/x-tar" };
|
|
261
|
+
if (deps.token !== null) headers["authorization"] = `Bearer ${deps.token}`;
|
|
262
|
+
if (options.size !== void 0) headers["content-length"] = String(options.size);
|
|
263
|
+
const init = {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers,
|
|
266
|
+
body: context,
|
|
267
|
+
duplex: "half",
|
|
268
|
+
...options.signal !== void 0 && { signal: options.signal }
|
|
269
|
+
};
|
|
270
|
+
const request = new Request(url.href, init);
|
|
271
|
+
const response = await (deps.fetch ?? fetch)(request);
|
|
272
|
+
const body = await response.json().catch(() => null);
|
|
273
|
+
if (response.ok) return ImageBuildResultSchema.parse(body);
|
|
274
|
+
const failure = ImageBuildErrorSchema.safeParse(body);
|
|
275
|
+
if (failure.success) throw new ORPCError$1(failure.data.code, {
|
|
276
|
+
status: response.status,
|
|
277
|
+
message: failure.data.message
|
|
278
|
+
});
|
|
279
|
+
const code = response.status === 401 ? "UNAUTHORIZED" : "INTERNAL_SERVER_ERROR";
|
|
280
|
+
throw new ORPCError$1(code, {
|
|
281
|
+
status: response.status,
|
|
282
|
+
message: `impd answered ${String(response.status)} to the image build`
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
163
286
|
//#region src/exec/exec-error.ts
|
|
164
287
|
var ExecError = class extends Error {
|
|
165
288
|
code;
|
|
@@ -182,13 +305,6 @@ function toExecError(outcome) {
|
|
|
182
305
|
return new ExecError("LOCAL_ERROR", "an output handler threw", { cause: outcome.error });
|
|
183
306
|
}
|
|
184
307
|
//#endregion
|
|
185
|
-
//#region src/resolve-impd-url.ts
|
|
186
|
-
function resolveImpdUrl(base, path) {
|
|
187
|
-
const url = new URL(base);
|
|
188
|
-
url.pathname = `${url.pathname.replace(/\/+$/, "")}${path}`;
|
|
189
|
-
return url;
|
|
190
|
-
}
|
|
191
|
-
//#endregion
|
|
192
308
|
//#region src/exec/check-impd-access.ts
|
|
193
309
|
const CHECK_TIMEOUT_MS = 5e3;
|
|
194
310
|
async function checkImpdAccess(baseUrl, token, customFetch = fetch) {
|
|
@@ -272,7 +388,8 @@ function openExecSession(options) {
|
|
|
272
388
|
options.onStarted({
|
|
273
389
|
pid: message.pid,
|
|
274
390
|
session: message.session ?? null,
|
|
275
|
-
created: message.created ?? false
|
|
391
|
+
created: message.created ?? false,
|
|
392
|
+
groupKill: message.groupKill ?? false
|
|
276
393
|
});
|
|
277
394
|
} else if (message.type === "exit") resolveOutcome({
|
|
278
395
|
kind: "exit",
|
|
@@ -407,7 +524,8 @@ function openExec(deps, name, argv, options = {}) {
|
|
|
407
524
|
...options.cwd !== void 0 && { cwd: options.cwd },
|
|
408
525
|
...options.cols !== void 0 && { cols: options.cols },
|
|
409
526
|
...options.rows !== void 0 && { rows: options.rows },
|
|
410
|
-
...options.session !== void 0 && { session: options.session }
|
|
527
|
+
...options.session !== void 0 && { session: options.session },
|
|
528
|
+
...options.killGraceMs !== void 0 && { killGraceMs: options.killGraceMs }
|
|
411
529
|
});
|
|
412
530
|
}
|
|
413
531
|
function openAttach(deps, name, session, options = {}) {
|
|
@@ -459,7 +577,7 @@ async function openHandle(deps, options, tty, start) {
|
|
|
459
577
|
onOutput: (channel, data) => {
|
|
460
578
|
(channel === "stderr" ? stderr : stdout).push(data);
|
|
461
579
|
},
|
|
462
|
-
connect: (url) => new WebSocket(url),
|
|
580
|
+
connect: deps.connect ?? ((url) => new WebSocket(url)),
|
|
463
581
|
...deps.fetch !== void 0 && { fetch: deps.fetch }
|
|
464
582
|
});
|
|
465
583
|
const resolveHandle = (result) => {
|
|
@@ -701,7 +819,8 @@ function createImpClient(options) {
|
|
|
701
819
|
rpc,
|
|
702
820
|
baseUrl: options.url,
|
|
703
821
|
token: token ?? null,
|
|
704
|
-
...customFetch !== void 0 && { fetch: customFetch }
|
|
822
|
+
...customFetch !== void 0 && { fetch: customFetch },
|
|
823
|
+
...options.connect !== void 0 && { connect: options.connect }
|
|
705
824
|
};
|
|
706
825
|
return {
|
|
707
826
|
...namespaces,
|
|
@@ -710,8 +829,215 @@ function createImpClient(options) {
|
|
|
710
829
|
openConsole: (name, consoleOptions) => openConsole(execDeps, name, consoleOptions),
|
|
711
830
|
openAttach: (name, session, attachOptions) => openAttach(execDeps, name, session, attachOptions),
|
|
712
831
|
requireAwake: (name, awakeOptions) => requireAwake(rpc, name, awakeOptions),
|
|
713
|
-
checkServer: () => checkServer(rpc)
|
|
832
|
+
checkServer: () => checkServer(rpc),
|
|
833
|
+
buildImage: (name, context, buildOptions) => buildImage(execDeps, name, context, buildOptions)
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/reverse/open-reverse-forward.ts
|
|
838
|
+
function parseMessage(text) {
|
|
839
|
+
try {
|
|
840
|
+
const parsed = TunnelServerMessageSchema.safeParse(JSON.parse(text));
|
|
841
|
+
return parsed.success ? parsed.data : null;
|
|
842
|
+
} catch {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function buildListenMessage(name, guest) {
|
|
847
|
+
return guest.network === "tcp" ? {
|
|
848
|
+
type: "listen",
|
|
849
|
+
name,
|
|
850
|
+
network: "tcp",
|
|
851
|
+
port: guest.port
|
|
852
|
+
} : {
|
|
853
|
+
type: "listen",
|
|
854
|
+
name,
|
|
855
|
+
network: "unix",
|
|
856
|
+
path: guest.path
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function openRelay(target, accept, handlers) {
|
|
860
|
+
const ws = target.connect(target.url, target.headers);
|
|
861
|
+
const state = {
|
|
862
|
+
opened: false,
|
|
863
|
+
unacked: 0,
|
|
864
|
+
done: false,
|
|
865
|
+
peerEof: false,
|
|
866
|
+
guestEof: false
|
|
867
|
+
};
|
|
868
|
+
const waiters = [];
|
|
869
|
+
const pending = [];
|
|
870
|
+
ws.binaryType = "arraybuffer";
|
|
871
|
+
const wakeWaiters = () => {
|
|
872
|
+
for (const wake of waiters.splice(0)) wake();
|
|
873
|
+
};
|
|
874
|
+
const sendText = (message) => {
|
|
875
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
|
|
876
|
+
};
|
|
877
|
+
const sendFrames = (data) => {
|
|
878
|
+
for (let offset = 0; offset < data.byteLength; offset += TUNNEL_MAX_FRAME_BYTES) ws.send(Uint8Array.from(data.subarray(offset, offset + TUNNEL_MAX_FRAME_BYTES)));
|
|
879
|
+
};
|
|
880
|
+
const stopRelay = (lost) => {
|
|
881
|
+
if (state.done) return;
|
|
882
|
+
state.done = true;
|
|
883
|
+
wakeWaiters();
|
|
884
|
+
handlers.onClose(lost);
|
|
885
|
+
};
|
|
886
|
+
const sendAck = async (data) => {
|
|
887
|
+
await handlers.onData(data);
|
|
888
|
+
sendText({
|
|
889
|
+
type: "ack",
|
|
890
|
+
bytes: data.byteLength
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
const handleControl = (message) => {
|
|
894
|
+
if (message.type === "opened") {
|
|
895
|
+
state.opened = true;
|
|
896
|
+
for (const data of pending.splice(0)) sendFrames(data);
|
|
897
|
+
if (state.peerEof) sendText({ type: "eof" });
|
|
898
|
+
} else if (message.type === "eof") {
|
|
899
|
+
state.guestEof = true;
|
|
900
|
+
handlers.onEof();
|
|
901
|
+
} else if (message.type === "ack") {
|
|
902
|
+
state.unacked -= message.bytes;
|
|
903
|
+
if (state.unacked <= 1048576) wakeWaiters();
|
|
904
|
+
} else {
|
|
905
|
+
ws.close(TUNNEL_CLOSE_NORMAL, "relay failed");
|
|
906
|
+
stopRelay(true);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
ws.addEventListener("open", () => {
|
|
910
|
+
sendText(accept);
|
|
911
|
+
});
|
|
912
|
+
ws.addEventListener("message", (event) => {
|
|
913
|
+
if (event.data instanceof ArrayBuffer) {
|
|
914
|
+
sendAck(new Uint8Array(event.data));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const message = parseMessage(String(event.data));
|
|
918
|
+
if (message === null) {
|
|
919
|
+
ws.close(TUNNEL_CLOSE_PROTOCOL, "bad message");
|
|
920
|
+
stopRelay(true);
|
|
921
|
+
} else handleControl(message);
|
|
922
|
+
});
|
|
923
|
+
ws.addEventListener("close", (event) => {
|
|
924
|
+
stopRelay(event.code !== TUNNEL_CLOSE_NORMAL);
|
|
925
|
+
});
|
|
926
|
+
ws.addEventListener("error", () => {});
|
|
927
|
+
return {
|
|
928
|
+
send: (data) => {
|
|
929
|
+
if (state.done) return true;
|
|
930
|
+
if (state.opened) sendFrames(data);
|
|
931
|
+
else pending.push(Uint8Array.from(data));
|
|
932
|
+
state.unacked += data.byteLength;
|
|
933
|
+
return state.unacked <= TUNNEL_WINDOW_BYTES;
|
|
934
|
+
},
|
|
935
|
+
waitForRoom: async () => {
|
|
936
|
+
while (!state.done && state.unacked > 1048576) await new Promise((resolve) => {
|
|
937
|
+
waiters.push(resolve);
|
|
938
|
+
});
|
|
939
|
+
},
|
|
940
|
+
sendEof: () => {
|
|
941
|
+
state.peerEof = true;
|
|
942
|
+
if (state.opened) sendText({ type: "eof" });
|
|
943
|
+
},
|
|
944
|
+
close: () => {
|
|
945
|
+
if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) ws.close(TUNNEL_CLOSE_NORMAL, "local side closed");
|
|
946
|
+
stopRelay(false);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function openReverseForward(options) {
|
|
951
|
+
const url = resolveImpdUrl(options.baseUrl, TUNNEL_PATH);
|
|
952
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
953
|
+
const headers = options.token === null ? {} : { authorization: `Bearer ${options.token}` };
|
|
954
|
+
const target = {
|
|
955
|
+
url: url.href,
|
|
956
|
+
headers,
|
|
957
|
+
connect: options.connect
|
|
958
|
+
};
|
|
959
|
+
const ws = options.connect(url.href, headers);
|
|
960
|
+
const listening = Promise.withResolvers();
|
|
961
|
+
const ended = Promise.withResolvers();
|
|
962
|
+
const relays = /* @__PURE__ */ new Set();
|
|
963
|
+
const state = {
|
|
964
|
+
listener: "",
|
|
965
|
+
done: false
|
|
966
|
+
};
|
|
967
|
+
const waitForListening = async () => {
|
|
968
|
+
try {
|
|
969
|
+
await listening.promise;
|
|
970
|
+
} catch {}
|
|
971
|
+
};
|
|
972
|
+
waitForListening();
|
|
973
|
+
const stopForward = (end) => {
|
|
974
|
+
if (state.done) return;
|
|
975
|
+
state.done = true;
|
|
976
|
+
for (const relay of relays) relay.close();
|
|
977
|
+
const why = end.kind === "failed" ? end.message : `the forward ${end.kind}`;
|
|
978
|
+
listening.reject(new Error(why));
|
|
979
|
+
ended.resolve(end);
|
|
980
|
+
};
|
|
981
|
+
const openClientRelay = (id) => {
|
|
982
|
+
const message = {
|
|
983
|
+
type: "accept",
|
|
984
|
+
name: options.name,
|
|
985
|
+
listener: state.listener,
|
|
986
|
+
connection: id
|
|
987
|
+
};
|
|
988
|
+
options.onConnection((handlers) => {
|
|
989
|
+
const relay = openRelay(target, message, {
|
|
990
|
+
...handlers,
|
|
991
|
+
onClose: (lost) => {
|
|
992
|
+
relays.delete(relay);
|
|
993
|
+
handlers.onClose(lost);
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
relays.add(relay);
|
|
997
|
+
return relay;
|
|
998
|
+
});
|
|
999
|
+
};
|
|
1000
|
+
const handleControl = (message) => {
|
|
1001
|
+
if (message.type === "listening") {
|
|
1002
|
+
state.listener = message.listener;
|
|
1003
|
+
listening.resolve({
|
|
1004
|
+
path: message.path,
|
|
1005
|
+
port: message.port
|
|
1006
|
+
});
|
|
1007
|
+
} else if (message.type === "connection") openClientRelay(message.id);
|
|
1008
|
+
else if (message.type === "error") stopForward({
|
|
1009
|
+
kind: "failed",
|
|
1010
|
+
code: message.code ?? null,
|
|
1011
|
+
message: message.message
|
|
1012
|
+
});
|
|
1013
|
+
else ws.close(TUNNEL_CLOSE_PROTOCOL, "bad message");
|
|
1014
|
+
};
|
|
1015
|
+
ws.binaryType = "arraybuffer";
|
|
1016
|
+
ws.addEventListener("open", () => {
|
|
1017
|
+
ws.send(JSON.stringify(buildListenMessage(options.name, options.guest)));
|
|
1018
|
+
});
|
|
1019
|
+
ws.addEventListener("message", (event) => {
|
|
1020
|
+
const message = event.data instanceof ArrayBuffer ? null : parseMessage(String(event.data));
|
|
1021
|
+
if (message === null) ws.close(TUNNEL_CLOSE_PROTOCOL, "bad message");
|
|
1022
|
+
else handleControl(message);
|
|
1023
|
+
});
|
|
1024
|
+
ws.addEventListener("close", (event) => {
|
|
1025
|
+
if (event.code === 4e3) stopForward({ kind: "lost" });
|
|
1026
|
+
else stopForward({
|
|
1027
|
+
kind: "closed",
|
|
1028
|
+
code: event.code,
|
|
1029
|
+
reason: event.reason
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
ws.addEventListener("error", () => {});
|
|
1033
|
+
return {
|
|
1034
|
+
listening: listening.promise,
|
|
1035
|
+
ended: ended.promise,
|
|
1036
|
+
stop: () => {
|
|
1037
|
+
if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) ws.close(TUNNEL_CLOSE_NORMAL, "stopped");
|
|
1038
|
+
stopForward({ kind: "stopped" });
|
|
1039
|
+
}
|
|
714
1040
|
};
|
|
715
1041
|
}
|
|
716
1042
|
//#endregion
|
|
717
|
-
export { CLIENT_VERSION, CONSOLE_SHELL, ExecError, ORPCError, createImpClient, isDefinedError, openExecSession, safe };
|
|
1043
|
+
export { CLIENT_VERSION, CONSOLE_SHELL, ExecError, ORPCError, createImpClient, isDefinedError, openExecSession, openReverseForward, safe };
|