@impz/client 0.0.0-stage → 2.0.6
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/LICENSE +21 -0
- package/README.md +234 -2
- package/dist/index.d.ts +20839 -0
- package/dist/index.js +1860 -0
- package/package.json +36 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
import { ORPCError, ORPCError as ORPCError$1, createORPCClient, isDefinedError, safe } from "@orpc/client";
|
|
2
|
+
import { RPCLink } from "@orpc/client/fetch";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/check-server.ts
|
|
6
|
+
const CLIENT_VERSION = "2.0.6";
|
|
7
|
+
async function checkServer(rpc) {
|
|
8
|
+
const info = await rpc.system.info();
|
|
9
|
+
return {
|
|
10
|
+
clientVersion: CLIENT_VERSION,
|
|
11
|
+
serverVersion: info.version,
|
|
12
|
+
compatible: isCompatibleVersion(CLIENT_VERSION, info.version)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function isCompatibleVersion(client, server) {
|
|
16
|
+
const ours = parseVersion(client);
|
|
17
|
+
const theirs = parseVersion(server);
|
|
18
|
+
if (ours === null || theirs === null || ours.major !== theirs.major) return false;
|
|
19
|
+
return ours.major > 0 || ours.minor === theirs.minor;
|
|
20
|
+
}
|
|
21
|
+
function parseVersion(version) {
|
|
22
|
+
const groups = /^(?<major>\d+)\.(?<minor>\d+)\.\d+/.exec(version)?.groups;
|
|
23
|
+
return groups === void 0 ? null : {
|
|
24
|
+
major: Number(groups["major"]),
|
|
25
|
+
minor: Number(groups["minor"])
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region ../api/src/schemas/name-schema.ts
|
|
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
|
+
z.string().regex(/^[\w.:-]{1,64}$/, "must be 1–64 letters, digits, dots, underscores, colons or hyphens");
|
|
32
|
+
z.int().min(10).max(3600);
|
|
33
|
+
const LeaseOwnerSchema = z.object({
|
|
34
|
+
principal: z.string(),
|
|
35
|
+
display: z.string(),
|
|
36
|
+
label: z.string()
|
|
37
|
+
});
|
|
38
|
+
const LeaseSchema = z.object({
|
|
39
|
+
name: NameSchema,
|
|
40
|
+
owner: LeaseOwnerSchema,
|
|
41
|
+
until: z.date().nullable()
|
|
42
|
+
});
|
|
43
|
+
const LeaseSummarySchema = z.object({
|
|
44
|
+
leases: z.array(LeaseSchema).readonly(),
|
|
45
|
+
otherCount: z.int().nonnegative()
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region ../api/src/schemas/image-schema.ts
|
|
49
|
+
const ImageSourceSchema = z.enum(["oci", "imp"]);
|
|
50
|
+
const ImageSchema = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
name: NameSchema,
|
|
53
|
+
ref: z.string(),
|
|
54
|
+
digest: z.string(),
|
|
55
|
+
source: ImageSourceSchema,
|
|
56
|
+
createdAt: z.date(),
|
|
57
|
+
sizeBytes: z.int().nonnegative()
|
|
58
|
+
});
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region ../api/src/protocols/image-build-protocol.ts
|
|
61
|
+
const IMAGE_BUILD_PATH = "/images/build";
|
|
62
|
+
const IMAGE_BUILD_STREAM_TYPE = "application/x-ndjson";
|
|
63
|
+
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" });
|
|
64
|
+
z.object({
|
|
65
|
+
name: NameSchema,
|
|
66
|
+
dockerfile: DockerfilePathSchema.optional()
|
|
67
|
+
});
|
|
68
|
+
const ImageBuildResultSchema = ImageSchema.extend({ createdAt: z.coerce.date() });
|
|
69
|
+
const ImageBuildErrorSchema = z.object({
|
|
70
|
+
code: z.string(),
|
|
71
|
+
message: z.string()
|
|
72
|
+
});
|
|
73
|
+
const ImageBuildPhaseSchema = z.enum([
|
|
74
|
+
"upload",
|
|
75
|
+
"pack",
|
|
76
|
+
"build",
|
|
77
|
+
"pull",
|
|
78
|
+
"unpack",
|
|
79
|
+
"copy"
|
|
80
|
+
]);
|
|
81
|
+
const IMAGE_BUILD_LOG_MAX_CHARS = 8192;
|
|
82
|
+
const ImageBuildProgressSchema = z.object({
|
|
83
|
+
type: z.literal("progress"),
|
|
84
|
+
phase: ImageBuildPhaseSchema,
|
|
85
|
+
elapsedMs: z.number().nonnegative(),
|
|
86
|
+
step: z.string().optional()
|
|
87
|
+
});
|
|
88
|
+
const ImageBuildLogSchema = z.object({
|
|
89
|
+
type: z.literal("log"),
|
|
90
|
+
text: z.string().max(IMAGE_BUILD_LOG_MAX_CHARS)
|
|
91
|
+
});
|
|
92
|
+
const ImageBuildEventSchema = z.discriminatedUnion("type", [
|
|
93
|
+
ImageBuildProgressSchema,
|
|
94
|
+
ImageBuildLogSchema,
|
|
95
|
+
z.object({
|
|
96
|
+
type: z.literal("image"),
|
|
97
|
+
image: ImageBuildResultSchema
|
|
98
|
+
}),
|
|
99
|
+
ImageBuildErrorSchema.extend({ type: z.literal("error") })
|
|
100
|
+
]);
|
|
101
|
+
z.discriminatedUnion("type", [
|
|
102
|
+
ImageBuildProgressSchema,
|
|
103
|
+
ImageBuildLogSchema,
|
|
104
|
+
z.object({
|
|
105
|
+
type: z.literal("image"),
|
|
106
|
+
image: ImageSchema
|
|
107
|
+
})
|
|
108
|
+
]);
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region ../api/src/schemas/offset-schema.ts
|
|
111
|
+
const OffsetSchema = z.int().nonnegative();
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region ../api/src/schemas/session-output-schema.ts
|
|
114
|
+
const ExecutionGenerationSchema = z.string().regex(/^[0-9a-f]{32}$/);
|
|
115
|
+
const ColdBootCauseSchema = z.enum([
|
|
116
|
+
"start",
|
|
117
|
+
"wake_fallback",
|
|
118
|
+
"watchdog",
|
|
119
|
+
"restore",
|
|
120
|
+
"recovery",
|
|
121
|
+
"unknown"
|
|
122
|
+
]);
|
|
123
|
+
const ColdBootSchema = z.object({
|
|
124
|
+
bootId: z.string(),
|
|
125
|
+
cause: ColdBootCauseSchema,
|
|
126
|
+
at: z.iso.datetime()
|
|
127
|
+
}).readonly();
|
|
128
|
+
const ColdBootsSchema = z.array(ColdBootSchema).max(4).readonly();
|
|
129
|
+
const PreviousGenerationSchema = z.object({
|
|
130
|
+
executionGeneration: ExecutionGenerationSchema,
|
|
131
|
+
end: OffsetSchema,
|
|
132
|
+
exitCode: z.int().nullable()
|
|
133
|
+
}).readonly();
|
|
134
|
+
const ResumeFromSchema = z.object({
|
|
135
|
+
executionGeneration: ExecutionGenerationSchema,
|
|
136
|
+
offset: OffsetSchema
|
|
137
|
+
}).readonly();
|
|
138
|
+
const ResumeResultSchema = z.discriminatedUnion("kind", [
|
|
139
|
+
z.object({ kind: z.literal("exact") }),
|
|
140
|
+
z.object({
|
|
141
|
+
kind: z.literal("gap"),
|
|
142
|
+
from: OffsetSchema,
|
|
143
|
+
to: OffsetSchema
|
|
144
|
+
}),
|
|
145
|
+
z.object({
|
|
146
|
+
kind: z.literal("generation_changed"),
|
|
147
|
+
executionGeneration: ExecutionGenerationSchema,
|
|
148
|
+
firstOffset: OffsetSchema
|
|
149
|
+
})
|
|
150
|
+
]).readonly();
|
|
151
|
+
const SessionLogStateSchema = z.object({ enabled: z.boolean() }).readonly();
|
|
152
|
+
const SessionOutputSchema = z.discriminatedUnion("continuity", [z.object({ continuity: z.literal("none") }), z.object({
|
|
153
|
+
continuity: z.literal("offsets"),
|
|
154
|
+
bootId: z.string(),
|
|
155
|
+
executionGeneration: ExecutionGenerationSchema,
|
|
156
|
+
bufferStart: OffsetSchema,
|
|
157
|
+
end: OffsetSchema,
|
|
158
|
+
offset: OffsetSchema,
|
|
159
|
+
prelude: OffsetSchema,
|
|
160
|
+
coldBoots: ColdBootsSchema,
|
|
161
|
+
previous: PreviousGenerationSchema.optional(),
|
|
162
|
+
resume: ResumeResultSchema.optional(),
|
|
163
|
+
log: SessionLogStateSchema.optional()
|
|
164
|
+
})]).readonly();
|
|
165
|
+
const NoSessionDataSchema = z.object({
|
|
166
|
+
bootId: z.string(),
|
|
167
|
+
coldBoots: ColdBootsSchema,
|
|
168
|
+
previous: PreviousGenerationSchema.optional()
|
|
169
|
+
}).readonly();
|
|
170
|
+
const InvalidResumeDataSchema = z.object({
|
|
171
|
+
end: OffsetSchema,
|
|
172
|
+
bufferStart: OffsetSchema
|
|
173
|
+
}).readonly();
|
|
174
|
+
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");
|
|
175
|
+
const SessionExitSchema = z.object({
|
|
176
|
+
code: z.int().nullable(),
|
|
177
|
+
signal: z.string().nullable()
|
|
178
|
+
}).readonly();
|
|
179
|
+
z.object({
|
|
180
|
+
name: SessionNameSchema,
|
|
181
|
+
pid: z.int().positive(),
|
|
182
|
+
argv: z.array(z.string()).readonly(),
|
|
183
|
+
state: z.enum(["running", "exited"]),
|
|
184
|
+
attached: z.boolean(),
|
|
185
|
+
cols: z.int().positive(),
|
|
186
|
+
rows: z.int().positive(),
|
|
187
|
+
startedAt: z.date(),
|
|
188
|
+
exit: SessionExitSchema.optional(),
|
|
189
|
+
continuity: z.enum(["none", "offsets"]).optional(),
|
|
190
|
+
executionGeneration: ExecutionGenerationSchema.optional(),
|
|
191
|
+
bootId: z.string().optional(),
|
|
192
|
+
end: z.int().nonnegative().optional(),
|
|
193
|
+
endObservedAt: z.date().optional(),
|
|
194
|
+
log: z.boolean().optional()
|
|
195
|
+
});
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region ../api/src/protocols/exec-protocol.ts
|
|
198
|
+
const EXEC_PATH = "/exec";
|
|
199
|
+
const EXEC_TICKET_PARAM = "ticket";
|
|
200
|
+
const EXEC_CHANNELS = {
|
|
201
|
+
stdin: 0,
|
|
202
|
+
stdout: 1,
|
|
203
|
+
stderr: 2
|
|
204
|
+
};
|
|
205
|
+
const DimensionSchema = z.int().min(1).max(65535);
|
|
206
|
+
const EXEC_TOOLS = ["tar"];
|
|
207
|
+
const EXEC_REQUIREMENTS = ["broker"];
|
|
208
|
+
const EXEC_HIGH_WATER_BYTES = 1048576;
|
|
209
|
+
const ExecStartMessageSchema = z.object({
|
|
210
|
+
type: z.literal("start"),
|
|
211
|
+
name: NameSchema,
|
|
212
|
+
argv: z.array(z.string()).min(1),
|
|
213
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
214
|
+
cwd: z.string().optional(),
|
|
215
|
+
tty: z.boolean(),
|
|
216
|
+
cols: DimensionSchema.optional(),
|
|
217
|
+
rows: DimensionSchema.optional(),
|
|
218
|
+
session: SessionNameSchema.optional(),
|
|
219
|
+
tool: z.enum(EXEC_TOOLS).optional(),
|
|
220
|
+
outer: z.boolean().optional(),
|
|
221
|
+
killGraceMs: z.int().min(1).max(6e4).optional(),
|
|
222
|
+
resumeFrom: ResumeFromSchema.optional(),
|
|
223
|
+
require: z.array(z.enum(EXEC_REQUIREMENTS)).optional(),
|
|
224
|
+
log: z.boolean().optional()
|
|
225
|
+
}).refine((start) => start.session === void 0 || start.tty, {
|
|
226
|
+
message: "a session needs a tty",
|
|
227
|
+
path: ["tty"]
|
|
228
|
+
}).refine((start) => start.resumeFrom === void 0 || start.session !== void 0, {
|
|
229
|
+
message: "only a session resumes",
|
|
230
|
+
path: ["resumeFrom"]
|
|
231
|
+
}).refine((start) => start.log !== true || start.session !== void 0, {
|
|
232
|
+
message: "only a session keeps a log",
|
|
233
|
+
path: ["log"]
|
|
234
|
+
}).refine((start) => start.killGraceMs === void 0 || !start.tty, {
|
|
235
|
+
message: "a tty exec takes no kill grace",
|
|
236
|
+
path: ["killGraceMs"]
|
|
237
|
+
}).refine((start) => start.tool === void 0 || !start.tty && start.session === void 0 && start.env === void 0 && start.cwd === void 0, {
|
|
238
|
+
message: "a tool takes no tty, session, env or cwd",
|
|
239
|
+
path: ["tool"]
|
|
240
|
+
}).refine((start) => start.outer !== true || start.tool === void 0 && start.session === void 0, {
|
|
241
|
+
message: "an outer exec takes no tool or session",
|
|
242
|
+
path: ["outer"]
|
|
243
|
+
}).refine((start) => start.require === void 0 || start.require.length === 0 || start.tool === void 0 && start.outer !== true, {
|
|
244
|
+
message: "a tool or an outer exec takes no require",
|
|
245
|
+
path: ["require"]
|
|
246
|
+
});
|
|
247
|
+
const ExecAttachMessageSchema = z.object({
|
|
248
|
+
type: z.literal("attach"),
|
|
249
|
+
name: NameSchema,
|
|
250
|
+
session: SessionNameSchema,
|
|
251
|
+
cols: DimensionSchema.optional(),
|
|
252
|
+
rows: DimensionSchema.optional(),
|
|
253
|
+
resumeFrom: ResumeFromSchema.optional(),
|
|
254
|
+
wake: z.boolean().optional()
|
|
255
|
+
});
|
|
256
|
+
z.discriminatedUnion("type", [
|
|
257
|
+
ExecStartMessageSchema,
|
|
258
|
+
ExecAttachMessageSchema,
|
|
259
|
+
z.object({ type: z.literal("stdin_eof") }),
|
|
260
|
+
z.object({
|
|
261
|
+
type: z.literal("resize"),
|
|
262
|
+
cols: DimensionSchema,
|
|
263
|
+
rows: DimensionSchema
|
|
264
|
+
}),
|
|
265
|
+
z.object({
|
|
266
|
+
type: z.literal("signal"),
|
|
267
|
+
signal: z.string().regex(/^SIG[A-Z0-9]+$/)
|
|
268
|
+
}),
|
|
269
|
+
z.object({
|
|
270
|
+
type: z.literal("stdout_ack"),
|
|
271
|
+
bytes: z.int().positive()
|
|
272
|
+
})
|
|
273
|
+
]);
|
|
274
|
+
const ExecServerMessageSchema = z.discriminatedUnion("type", [
|
|
275
|
+
z.object({
|
|
276
|
+
type: z.literal("started"),
|
|
277
|
+
pid: z.int().positive(),
|
|
278
|
+
session: SessionNameSchema.optional(),
|
|
279
|
+
created: z.boolean().optional(),
|
|
280
|
+
groupKill: z.boolean().optional(),
|
|
281
|
+
output: SessionOutputSchema.optional()
|
|
282
|
+
}),
|
|
283
|
+
z.object({
|
|
284
|
+
type: z.literal("exit"),
|
|
285
|
+
code: z.int().nullable(),
|
|
286
|
+
signal: z.string().nullable(),
|
|
287
|
+
offset: z.int().nonnegative().optional()
|
|
288
|
+
}),
|
|
289
|
+
z.object({
|
|
290
|
+
type: z.literal("detached"),
|
|
291
|
+
reason: z.enum([
|
|
292
|
+
"taken_over",
|
|
293
|
+
"slow",
|
|
294
|
+
"lost"
|
|
295
|
+
]),
|
|
296
|
+
offset: z.int().nonnegative().optional()
|
|
297
|
+
}),
|
|
298
|
+
z.object({
|
|
299
|
+
type: z.literal("stdin_ack"),
|
|
300
|
+
bytes: z.int().positive()
|
|
301
|
+
}),
|
|
302
|
+
z.object({
|
|
303
|
+
type: z.literal("error"),
|
|
304
|
+
message: z.string(),
|
|
305
|
+
code: z.string().optional(),
|
|
306
|
+
data: z.unknown().optional()
|
|
307
|
+
})
|
|
308
|
+
]);
|
|
309
|
+
function encodeExecFrame(channel, data) {
|
|
310
|
+
const frame = new Uint8Array(data.byteLength + 1);
|
|
311
|
+
frame[0] = channel;
|
|
312
|
+
frame.set(data, 1);
|
|
313
|
+
return frame;
|
|
314
|
+
}
|
|
315
|
+
function decodeExecFrame(frame) {
|
|
316
|
+
const [channel] = frame;
|
|
317
|
+
if (!isExecChannel(channel)) throw new Error(`exec frame has an unknown channel byte: ${String(channel)}`);
|
|
318
|
+
return {
|
|
319
|
+
channel,
|
|
320
|
+
data: frame.subarray(1)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function isExecChannel(value) {
|
|
324
|
+
return value === EXEC_CHANNELS.stdin || value === EXEC_CHANNELS.stdout || value === EXEC_CHANNELS.stderr;
|
|
325
|
+
}
|
|
326
|
+
z.enum(["tailnet", "public"]);
|
|
327
|
+
const PublicAuthSchema = z.enum([
|
|
328
|
+
"none",
|
|
329
|
+
"token",
|
|
330
|
+
"basic"
|
|
331
|
+
]);
|
|
332
|
+
const BasicUserSchema = z.string().regex(/^[!-9;-~]{1,64}$/, "must be 1 to 64 printable characters, without a colon");
|
|
333
|
+
z.object({
|
|
334
|
+
name: NameSchema,
|
|
335
|
+
auth: PublicAuthSchema.default("token"),
|
|
336
|
+
user: BasicUserSchema.optional()
|
|
337
|
+
}).refine((input) => input.auth === "basic" || input.user === void 0, {
|
|
338
|
+
message: "only basic auth takes a user",
|
|
339
|
+
path: ["user"]
|
|
340
|
+
});
|
|
341
|
+
z.object({
|
|
342
|
+
url: z.url(),
|
|
343
|
+
auth: PublicAuthSchema,
|
|
344
|
+
user: z.string().nullable(),
|
|
345
|
+
credential: z.string().nullable(),
|
|
346
|
+
warning: z.string().optional()
|
|
347
|
+
});
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region ../api/src/schemas/move-schema.ts
|
|
350
|
+
const MoveStateSchema = z.enum([
|
|
351
|
+
"sending",
|
|
352
|
+
"moved",
|
|
353
|
+
"receiving"
|
|
354
|
+
]);
|
|
355
|
+
const PeerUrlSchema = z.url({ protocol: /^https?$/ });
|
|
356
|
+
const WarmHostSchema = z.object({
|
|
357
|
+
firecrackerVersion: z.string(),
|
|
358
|
+
snapshotVersion: z.string(),
|
|
359
|
+
hostKernel: z.string(),
|
|
360
|
+
cpuModel: z.string(),
|
|
361
|
+
cpuFlags: z.string(),
|
|
362
|
+
dataDir: z.string(),
|
|
363
|
+
storage: z.enum(["xfs", "zfs"]),
|
|
364
|
+
subnet: z.string(),
|
|
365
|
+
slotCount: z.int().positive(),
|
|
366
|
+
brokerPort: z.int().positive(),
|
|
367
|
+
dns: z.array(z.string()).readonly()
|
|
368
|
+
});
|
|
369
|
+
const WarmMoveSchema = z.object({
|
|
370
|
+
slot: z.int().nonnegative(),
|
|
371
|
+
egressMode: z.string(),
|
|
372
|
+
snapshot: z.object({
|
|
373
|
+
firecrackerVersion: z.string(),
|
|
374
|
+
snapshotVersion: z.string(),
|
|
375
|
+
hostKernel: z.string(),
|
|
376
|
+
cpuModel: z.string().nullable(),
|
|
377
|
+
cpuFlags: z.string().nullable(),
|
|
378
|
+
ipv6Prefix: z.string().nullable()
|
|
379
|
+
}),
|
|
380
|
+
host: WarmHostSchema.pick({
|
|
381
|
+
dataDir: true,
|
|
382
|
+
storage: true,
|
|
383
|
+
subnet: true,
|
|
384
|
+
brokerPort: true,
|
|
385
|
+
dns: true
|
|
386
|
+
})
|
|
387
|
+
});
|
|
388
|
+
z.object({
|
|
389
|
+
bytes: z.int().nonnegative(),
|
|
390
|
+
checkpoints: z.int().nonnegative(),
|
|
391
|
+
warm: WarmMoveSchema.nullable()
|
|
392
|
+
});
|
|
393
|
+
z.object({
|
|
394
|
+
ticket: z.string(),
|
|
395
|
+
expiresAt: z.date(),
|
|
396
|
+
peerUrl: PeerUrlSchema
|
|
397
|
+
});
|
|
398
|
+
z.object({
|
|
399
|
+
state: MoveStateSchema.nullable(),
|
|
400
|
+
peer: z.string().nullable(),
|
|
401
|
+
sentBytes: z.int().nonnegative(),
|
|
402
|
+
totalBytes: z.int().nonnegative(),
|
|
403
|
+
isDone: z.boolean(),
|
|
404
|
+
error: z.string().nullable()
|
|
405
|
+
});
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region ../api/src/schemas/secret-schema.ts
|
|
408
|
+
const SecretNameSchema = NameSchema;
|
|
409
|
+
const PrintableSchema = z.string().regex(/^[!-~]+$/, "must be printable ASCII without spaces");
|
|
410
|
+
PrintableSchema.min(1).max(8192);
|
|
411
|
+
const BrokerHostSchema = z.string().max(253).regex(/^(?:[a-z0-9][a-z0-9-]{0,62}\.)+[a-z][a-z0-9-]{0,62}$/, "must be a lowercase hostname such as api.example.com");
|
|
412
|
+
const BrokerUpstreamSchema = z.string().max(2048).transform((value, context) => {
|
|
413
|
+
const url = URL.parse(value);
|
|
414
|
+
if (url === null || url.protocol !== "http:" && url.protocol !== "https:") {
|
|
415
|
+
context.addIssue({
|
|
416
|
+
code: "custom",
|
|
417
|
+
message: "must be an http or https URL"
|
|
418
|
+
});
|
|
419
|
+
return z.NEVER;
|
|
420
|
+
}
|
|
421
|
+
if (url.username !== "" || url.password !== "") {
|
|
422
|
+
context.addIssue({
|
|
423
|
+
code: "custom",
|
|
424
|
+
message: "must not hold a user name or password"
|
|
425
|
+
});
|
|
426
|
+
return z.NEVER;
|
|
427
|
+
}
|
|
428
|
+
if (url.search !== "" || url.hash !== "" || value.includes("?") || value.includes("#")) {
|
|
429
|
+
context.addIssue({
|
|
430
|
+
code: "custom",
|
|
431
|
+
message: "must not have a query or a fragment"
|
|
432
|
+
});
|
|
433
|
+
return z.NEVER;
|
|
434
|
+
}
|
|
435
|
+
if (url.pathname !== "/") {
|
|
436
|
+
context.addIssue({
|
|
437
|
+
code: "custom",
|
|
438
|
+
message: "must be an origin with no path"
|
|
439
|
+
});
|
|
440
|
+
return z.NEVER;
|
|
441
|
+
}
|
|
442
|
+
return url.origin;
|
|
443
|
+
});
|
|
444
|
+
const BrokerRuleSchema = z.object({
|
|
445
|
+
host: BrokerHostSchema,
|
|
446
|
+
header: z.string().regex(/^[a-z0-9-]{1,64}$/, "must be a lowercase header name such as authorization"),
|
|
447
|
+
scheme: z.enum([
|
|
448
|
+
"bearer",
|
|
449
|
+
"basic",
|
|
450
|
+
"raw"
|
|
451
|
+
]),
|
|
452
|
+
user: BasicUserSchema.optional(),
|
|
453
|
+
upstream: BrokerUpstreamSchema.optional()
|
|
454
|
+
});
|
|
455
|
+
const SecretKindSchema = z.enum([
|
|
456
|
+
"github",
|
|
457
|
+
"anthropic",
|
|
458
|
+
"npm",
|
|
459
|
+
"custom",
|
|
460
|
+
"oauth"
|
|
461
|
+
]);
|
|
462
|
+
const OAuthStateSchema = z.object({
|
|
463
|
+
tokenUrl: z.url({ protocol: /^https$/ }).max(2048),
|
|
464
|
+
clientId: PrintableSchema.min(1).max(256),
|
|
465
|
+
tokenFormat: z.enum(["json", "form"]).default("form")
|
|
466
|
+
}).extend({
|
|
467
|
+
status: z.enum([
|
|
468
|
+
"pending",
|
|
469
|
+
"ready",
|
|
470
|
+
"needs_login"
|
|
471
|
+
]),
|
|
472
|
+
expiresAt: z.date().nullable(),
|
|
473
|
+
refreshedAt: z.date().nullable(),
|
|
474
|
+
error: z.string().nullable(),
|
|
475
|
+
idClaims: z.record(z.string(), z.unknown()).readonly().nullable()
|
|
476
|
+
});
|
|
477
|
+
z.object({
|
|
478
|
+
name: SecretNameSchema,
|
|
479
|
+
kind: SecretKindSchema,
|
|
480
|
+
rules: z.array(BrokerRuleSchema).readonly(),
|
|
481
|
+
imps: z.array(NameSchema).readonly(),
|
|
482
|
+
createdAt: z.date(),
|
|
483
|
+
oauth: OAuthStateSchema.optional()
|
|
484
|
+
}).extend({ droppedGrants: z.int().nonnegative().default(0) });
|
|
485
|
+
const GrantMethodSchema = z.string().regex(/^[A-Z]{1,16}$/, "must be an uppercase method such as GET");
|
|
486
|
+
const GrantPathSchema = z.string().max(512).regex(/^(?:\/(?:\*|[A-Za-z0-9._~!$&'()+,;=:@-]*))*(?:\/\*\*)?$/, "must be a path such as /v1/messages, with * for one segment and a last /** for the rest").refine((path) => path.startsWith("/"), "must start with /");
|
|
487
|
+
const GrantFilterSchema = z.object({
|
|
488
|
+
methods: z.array(GrantMethodSchema).min(1).max(16).readonly().optional(),
|
|
489
|
+
paths: z.array(GrantPathSchema).min(1).max(64).readonly().optional()
|
|
490
|
+
}).refine((filter) => filter.methods !== void 0 || filter.paths !== void 0, "must name methods, paths or both");
|
|
491
|
+
z.object({
|
|
492
|
+
secret: SecretNameSchema,
|
|
493
|
+
filter: GrantFilterSchema.nullable()
|
|
494
|
+
});
|
|
495
|
+
const AuditRefusalSchema = z.enum([
|
|
496
|
+
"not-http",
|
|
497
|
+
"not-connect",
|
|
498
|
+
"bad-target",
|
|
499
|
+
"connection-limit",
|
|
500
|
+
"egress-policy",
|
|
501
|
+
"address-refused",
|
|
502
|
+
"wrong-host",
|
|
503
|
+
"websocket",
|
|
504
|
+
"no-grant",
|
|
505
|
+
"outside-filter"
|
|
506
|
+
]);
|
|
507
|
+
z.object({
|
|
508
|
+
at: z.date(),
|
|
509
|
+
imp: NameSchema,
|
|
510
|
+
secret: SecretNameSchema,
|
|
511
|
+
method: z.string(),
|
|
512
|
+
host: BrokerHostSchema,
|
|
513
|
+
path: z.string(),
|
|
514
|
+
status: z.int(),
|
|
515
|
+
requestBytes: z.int().nonnegative(),
|
|
516
|
+
responseBytes: z.int().nonnegative(),
|
|
517
|
+
durationMs: z.int().nonnegative()
|
|
518
|
+
});
|
|
519
|
+
z.object({
|
|
520
|
+
at: z.date(),
|
|
521
|
+
imp: NameSchema,
|
|
522
|
+
method: z.string().nullable(),
|
|
523
|
+
host: z.string().max(253).nullable(),
|
|
524
|
+
path: z.string().nullable(),
|
|
525
|
+
status: z.int(),
|
|
526
|
+
durationMs: z.int().nonnegative(),
|
|
527
|
+
reason: AuditRefusalSchema
|
|
528
|
+
});
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region ../api/src/schemas/imp-schema.ts
|
|
531
|
+
const ImpStateSchema = z.enum([
|
|
532
|
+
"creating",
|
|
533
|
+
"running",
|
|
534
|
+
"sleeping",
|
|
535
|
+
"stopped",
|
|
536
|
+
"error"
|
|
537
|
+
]);
|
|
538
|
+
const ImpKindSchema = z.enum(["user", "builder"]);
|
|
539
|
+
const OutdatedPartSchema = z.enum([
|
|
540
|
+
"firecracker",
|
|
541
|
+
"kernel",
|
|
542
|
+
"agent",
|
|
543
|
+
"impd",
|
|
544
|
+
"ipv6"
|
|
545
|
+
]);
|
|
546
|
+
const ImpResourcesSchema = z.object({
|
|
547
|
+
wakeCount: z.int().nonnegative(),
|
|
548
|
+
awakeMs: z.int().nonnegative(),
|
|
549
|
+
sample: z.object({
|
|
550
|
+
measuredAt: z.date(),
|
|
551
|
+
since: z.date(),
|
|
552
|
+
cpuPercent: z.number().nonnegative().optional(),
|
|
553
|
+
cpuThrottledMs: z.int().nonnegative(),
|
|
554
|
+
netRxBytes: z.int().nonnegative(),
|
|
555
|
+
netTxBytes: z.int().nonnegative()
|
|
556
|
+
}).optional()
|
|
557
|
+
});
|
|
558
|
+
const ImpSchema = z.object({
|
|
559
|
+
id: z.string(),
|
|
560
|
+
name: NameSchema,
|
|
561
|
+
image: NameSchema,
|
|
562
|
+
state: ImpStateSchema,
|
|
563
|
+
kind: ImpKindSchema.optional(),
|
|
564
|
+
vcpus: z.int().positive(),
|
|
565
|
+
memoryMib: z.int().positive(),
|
|
566
|
+
maxMemoryMib: z.int().positive().optional(),
|
|
567
|
+
pluggedMib: z.int().nonnegative().optional(),
|
|
568
|
+
backup: z.boolean().optional(),
|
|
569
|
+
diskMib: z.int().positive(),
|
|
570
|
+
diskUsage: z.object({
|
|
571
|
+
exclusiveBytes: z.int().nonnegative(),
|
|
572
|
+
sharedBytes: z.int().nonnegative(),
|
|
573
|
+
measuredAt: z.date(),
|
|
574
|
+
isPartial: z.boolean(),
|
|
575
|
+
isUpperBound: z.boolean()
|
|
576
|
+
}).optional(),
|
|
577
|
+
ip: z.ipv4(),
|
|
578
|
+
slot: z.int().nonnegative(),
|
|
579
|
+
port: z.int().positive(),
|
|
580
|
+
httpPort: z.int().positive(),
|
|
581
|
+
url: z.url(),
|
|
582
|
+
public: z.object({ auth: PublicAuthSchema }).optional(),
|
|
583
|
+
createdAt: z.date(),
|
|
584
|
+
lastActiveAt: z.date(),
|
|
585
|
+
ramMib: z.int().nonnegative().optional(),
|
|
586
|
+
rssMib: z.int().nonnegative().optional(),
|
|
587
|
+
sleptAt: z.date().optional(),
|
|
588
|
+
holdUntil: z.date().optional(),
|
|
589
|
+
leases: LeaseSummarySchema.optional(),
|
|
590
|
+
error: z.string().optional(),
|
|
591
|
+
sessions: z.int().nonnegative().optional(),
|
|
592
|
+
coldBootReason: z.string().optional(),
|
|
593
|
+
agentSilentSince: z.date().optional(),
|
|
594
|
+
inner: z.object({
|
|
595
|
+
up: z.boolean(),
|
|
596
|
+
restarts: z.int().nonnegative(),
|
|
597
|
+
lastError: z.string().optional()
|
|
598
|
+
}).optional(),
|
|
599
|
+
outdated: z.array(OutdatedPartSchema).readonly().optional(),
|
|
600
|
+
cpu: z.object({
|
|
601
|
+
limit: z.number().positive().nullable(),
|
|
602
|
+
weight: z.int()
|
|
603
|
+
}).optional(),
|
|
604
|
+
resources: ImpResourcesSchema.optional(),
|
|
605
|
+
move: MoveStateSchema.optional()
|
|
606
|
+
});
|
|
607
|
+
const GrantNotCopiedReasonSchema = z.enum([
|
|
608
|
+
"not-grantable",
|
|
609
|
+
"clash",
|
|
610
|
+
"no-secret"
|
|
611
|
+
]);
|
|
612
|
+
const GrantNotCopiedSchema = z.object({
|
|
613
|
+
secret: SecretNameSchema,
|
|
614
|
+
reason: GrantNotCopiedReasonSchema
|
|
615
|
+
});
|
|
616
|
+
ImpSchema.extend({
|
|
617
|
+
grantsNotCopied: z.array(GrantNotCopiedSchema).readonly().optional(),
|
|
618
|
+
grantsError: z.string().optional()
|
|
619
|
+
});
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region ../api/src/protocols/tunnel-protocol.ts
|
|
622
|
+
const TUNNEL_PATH = "/tunnel";
|
|
623
|
+
const TUNNEL_CLOSE_NORMAL = 1e3;
|
|
624
|
+
const TUNNEL_CLOSE_PROTOCOL = 1002;
|
|
625
|
+
const TUNNEL_WINDOW_BYTES = 1048576;
|
|
626
|
+
const TUNNEL_MAX_FRAME_BYTES = 65536;
|
|
627
|
+
const AckSchema = z.object({
|
|
628
|
+
type: z.literal("ack"),
|
|
629
|
+
bytes: z.int().positive()
|
|
630
|
+
});
|
|
631
|
+
const TunnelListenSchema = z.object({
|
|
632
|
+
type: z.literal("listen"),
|
|
633
|
+
name: NameSchema,
|
|
634
|
+
network: z.enum(["unix", "tcp"]),
|
|
635
|
+
path: z.string().startsWith("/").nullable().optional(),
|
|
636
|
+
port: z.int().min(0).max(65535).optional()
|
|
637
|
+
}).refine((listen) => listen.network === "tcp" ? listen.port !== void 0 && listen.path === void 0 : listen.port === void 0, {
|
|
638
|
+
message: "tcp takes a port, unix a path or null",
|
|
639
|
+
path: ["network"]
|
|
640
|
+
});
|
|
641
|
+
z.discriminatedUnion("type", [
|
|
642
|
+
z.object({
|
|
643
|
+
type: z.literal("open"),
|
|
644
|
+
name: NameSchema,
|
|
645
|
+
port: z.int().min(1).max(65535)
|
|
646
|
+
}),
|
|
647
|
+
z.object({ type: z.literal("eof") }),
|
|
648
|
+
AckSchema,
|
|
649
|
+
TunnelListenSchema,
|
|
650
|
+
z.object({
|
|
651
|
+
type: z.literal("accept"),
|
|
652
|
+
name: NameSchema,
|
|
653
|
+
listener: z.string().min(1),
|
|
654
|
+
connection: z.int().positive()
|
|
655
|
+
})
|
|
656
|
+
]);
|
|
657
|
+
const TunnelServerMessageSchema = z.discriminatedUnion("type", [
|
|
658
|
+
z.object({ type: z.literal("opened") }),
|
|
659
|
+
z.object({ type: z.literal("eof") }),
|
|
660
|
+
AckSchema,
|
|
661
|
+
z.object({
|
|
662
|
+
type: z.literal("listening"),
|
|
663
|
+
listener: z.string(),
|
|
664
|
+
path: z.string().nullable(),
|
|
665
|
+
port: z.int().nullable()
|
|
666
|
+
}),
|
|
667
|
+
z.object({
|
|
668
|
+
type: z.literal("connection"),
|
|
669
|
+
id: z.int().positive()
|
|
670
|
+
}),
|
|
671
|
+
z.object({
|
|
672
|
+
type: z.literal("error"),
|
|
673
|
+
message: z.string(),
|
|
674
|
+
code: z.string().optional()
|
|
675
|
+
})
|
|
676
|
+
]);
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region ../api/src/contract/define-errors.ts
|
|
679
|
+
function defineErrors(errors) {
|
|
680
|
+
return errors;
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region ../api/src/contract/imp-errors.ts
|
|
684
|
+
const ResourceKindSchema = z.enum([
|
|
685
|
+
"imp",
|
|
686
|
+
"image",
|
|
687
|
+
"checkpoint",
|
|
688
|
+
"session",
|
|
689
|
+
"service",
|
|
690
|
+
"secret",
|
|
691
|
+
"grant",
|
|
692
|
+
"backup",
|
|
693
|
+
"token",
|
|
694
|
+
"ssh-key",
|
|
695
|
+
"network",
|
|
696
|
+
"oauth-client",
|
|
697
|
+
"oauth-grant",
|
|
698
|
+
"oauth-approval",
|
|
699
|
+
"database-copy"
|
|
700
|
+
]);
|
|
701
|
+
const ResourceDataSchema = z.object({
|
|
702
|
+
kind: ResourceKindSchema,
|
|
703
|
+
name: z.string(),
|
|
704
|
+
reason: z.enum(["binding_changed"]).optional()
|
|
705
|
+
});
|
|
706
|
+
const ForbiddenReasonSchema = z.enum([
|
|
707
|
+
"imp_out_of_scope",
|
|
708
|
+
"not_grantable",
|
|
709
|
+
"scope"
|
|
710
|
+
]);
|
|
711
|
+
const ForbiddenDataSchema = z.object({ reason: ForbiddenReasonSchema });
|
|
712
|
+
const PreconditionDataSchema = z.object({
|
|
713
|
+
reason: z.enum(["broker_not_ready", "impd_outdated"]),
|
|
714
|
+
detail: z.string()
|
|
715
|
+
});
|
|
716
|
+
const ProtectedImpSchema = z.object({
|
|
717
|
+
name: NameSchema,
|
|
718
|
+
ramMib: z.int().nonnegative(),
|
|
719
|
+
leased: z.boolean(),
|
|
720
|
+
busy: z.boolean()
|
|
721
|
+
});
|
|
722
|
+
const IMP_ERRORS = defineErrors({
|
|
723
|
+
NOT_FOUND: {
|
|
724
|
+
message: "Not found",
|
|
725
|
+
data: ResourceDataSchema
|
|
726
|
+
},
|
|
727
|
+
CONFLICT: {
|
|
728
|
+
message: "Already exists",
|
|
729
|
+
data: ResourceDataSchema
|
|
730
|
+
},
|
|
731
|
+
FORBIDDEN: {
|
|
732
|
+
message: "Not allowed",
|
|
733
|
+
data: ForbiddenDataSchema.optional()
|
|
734
|
+
},
|
|
735
|
+
PRECONDITION_FAILED: {
|
|
736
|
+
message: "Not possible on this host",
|
|
737
|
+
data: PreconditionDataSchema.optional()
|
|
738
|
+
},
|
|
739
|
+
RAM_BUDGET_EXCEEDED: {
|
|
740
|
+
message: "Not enough RAM budget, even after sleeping idle imps",
|
|
741
|
+
status: 503,
|
|
742
|
+
data: z.object({
|
|
743
|
+
budgetMib: z.int().nonnegative(),
|
|
744
|
+
usedMib: z.int().nonnegative(),
|
|
745
|
+
requestedMib: z.int().nonnegative(),
|
|
746
|
+
neededMib: z.int().nonnegative().optional(),
|
|
747
|
+
protected: z.array(ProtectedImpSchema).readonly().optional(),
|
|
748
|
+
protectedHidden: z.int().nonnegative().optional()
|
|
749
|
+
})
|
|
750
|
+
},
|
|
751
|
+
SERVICE_UNAVAILABLE: { message: "impd is stopping" },
|
|
752
|
+
INVALID_STATE: {
|
|
753
|
+
message: "The imp is not in a state that allows this",
|
|
754
|
+
status: 409,
|
|
755
|
+
data: z.object({
|
|
756
|
+
state: ImpStateSchema,
|
|
757
|
+
allowed: z.array(ImpStateSchema),
|
|
758
|
+
coldBoots: ColdBootsSchema.optional()
|
|
759
|
+
})
|
|
760
|
+
},
|
|
761
|
+
INVALID_RESUME: {
|
|
762
|
+
message: "The resume offset is past the end of the output",
|
|
763
|
+
status: 409,
|
|
764
|
+
data: InvalidResumeDataSchema
|
|
765
|
+
},
|
|
766
|
+
DISK_FULL: {
|
|
767
|
+
message: "Not enough free disk on the host",
|
|
768
|
+
status: 507,
|
|
769
|
+
data: z.object({
|
|
770
|
+
availableBytes: z.int().nonnegative(),
|
|
771
|
+
reserveBytes: z.int().nonnegative(),
|
|
772
|
+
requestedBytes: z.int().nonnegative()
|
|
773
|
+
})
|
|
774
|
+
},
|
|
775
|
+
LEASED: {
|
|
776
|
+
message: "The imp is leased",
|
|
777
|
+
status: 409,
|
|
778
|
+
data: LeaseSummarySchema
|
|
779
|
+
},
|
|
780
|
+
LEASE_NOT_HELD: {
|
|
781
|
+
message: "The caller holds no such lease",
|
|
782
|
+
status: 409
|
|
783
|
+
},
|
|
784
|
+
AGENT_OUTDATED: {
|
|
785
|
+
message: "The imp's agent is too old for this",
|
|
786
|
+
status: 409
|
|
787
|
+
},
|
|
788
|
+
MOVING: {
|
|
789
|
+
message: "The imp is moving between hosts",
|
|
790
|
+
status: 409,
|
|
791
|
+
data: z.object({ retryAfterS: z.int().positive() })
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region ../api/src/protocols/login-shell.ts
|
|
796
|
+
const CONSOLE_SHELL = buildConsoleShell("/etc/passwd", ["/bin/bash", "/bin/sh"]);
|
|
797
|
+
function buildConsoleShell(passwdPath, fallbacks) {
|
|
798
|
+
return [
|
|
799
|
+
"uid=$(id -u 2>/dev/null) || uid=0",
|
|
800
|
+
"shell=",
|
|
801
|
+
"while IFS=: read -r _ _ id _ _ _ login; do",
|
|
802
|
+
" if [ \"$id\" = \"$uid\" ]; then shell=$login; break; fi",
|
|
803
|
+
`done < ${passwdPath}`,
|
|
804
|
+
...fallbacks.map((fallback) => `[ -x "$shell" ] || shell=${fallback}`),
|
|
805
|
+
"exec \"$shell\" -l"
|
|
806
|
+
].join("\n");
|
|
807
|
+
}
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region ../shared/src/split-lines.ts
|
|
810
|
+
function splitLines() {
|
|
811
|
+
const state = { pending: "" };
|
|
812
|
+
return new TransformStream({
|
|
813
|
+
transform: (chunk, controller) => {
|
|
814
|
+
const lines = `${state.pending}${chunk}`.split("\n");
|
|
815
|
+
state.pending = lines.pop() ?? "";
|
|
816
|
+
for (const line of lines) controller.enqueue(line);
|
|
817
|
+
},
|
|
818
|
+
flush: (controller) => {
|
|
819
|
+
if (state.pending !== "") controller.enqueue(state.pending);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/read-build-answer.ts
|
|
825
|
+
async function readBuildAnswer(response, onProgress, onLog) {
|
|
826
|
+
if (response.ok && isStream(response)) return readBuildEvents(response, onProgress, onLog);
|
|
827
|
+
const body = await response.json().catch(() => null);
|
|
828
|
+
if (response.ok) return ImageBuildResultSchema.parse(body);
|
|
829
|
+
const failure = ImageBuildErrorSchema.safeParse(body);
|
|
830
|
+
if (failure.success) throw new ORPCError$1(failure.data.code, {
|
|
831
|
+
status: response.status,
|
|
832
|
+
message: failure.data.message
|
|
833
|
+
});
|
|
834
|
+
const code = response.status === 401 ? "UNAUTHORIZED" : "INTERNAL_SERVER_ERROR";
|
|
835
|
+
throw new ORPCError$1(code, {
|
|
836
|
+
status: response.status,
|
|
837
|
+
message: `impd answered ${String(response.status)} to the image build`
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
function isStream(response) {
|
|
841
|
+
return response.headers.get("content-type")?.startsWith(IMAGE_BUILD_STREAM_TYPE) === true;
|
|
842
|
+
}
|
|
843
|
+
async function readBuildEvents(response, onProgress, onLog) {
|
|
844
|
+
for await (const line of readLines(response)) {
|
|
845
|
+
const parsed = ImageBuildEventSchema.safeParse(parseJson(line));
|
|
846
|
+
if (!parsed.success) continue;
|
|
847
|
+
const event = parsed.data;
|
|
848
|
+
if (event.type === "image") return event.image;
|
|
849
|
+
if (event.type === "error") throw new ORPCError$1(event.code, { message: event.message });
|
|
850
|
+
if (event.type === "log") onLog?.(event.text);
|
|
851
|
+
else onProgress?.(event);
|
|
852
|
+
}
|
|
853
|
+
throw new ORPCError$1("INTERNAL_SERVER_ERROR", { message: "the image build stream ended before impd answered the image" });
|
|
854
|
+
}
|
|
855
|
+
async function* readLines(response) {
|
|
856
|
+
if (response.body === null) return;
|
|
857
|
+
yield* response.body.pipeThrough(new TextDecoderStream()).pipeThrough(splitLines());
|
|
858
|
+
}
|
|
859
|
+
function parseJson(line) {
|
|
860
|
+
try {
|
|
861
|
+
return JSON.parse(line);
|
|
862
|
+
} catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/resolve-impd-url.ts
|
|
868
|
+
function resolveImpdUrl(base, path) {
|
|
869
|
+
const url = new URL(base);
|
|
870
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}${path}`;
|
|
871
|
+
return url;
|
|
872
|
+
}
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/build-image.ts
|
|
875
|
+
async function buildImage(deps, name, context, options = {}) {
|
|
876
|
+
const url = resolveImpdUrl(deps.baseUrl, IMAGE_BUILD_PATH);
|
|
877
|
+
url.searchParams.set("name", name);
|
|
878
|
+
if (options.dockerfile !== void 0) url.searchParams.set("dockerfile", options.dockerfile);
|
|
879
|
+
const headers = {
|
|
880
|
+
"content-type": "application/x-tar",
|
|
881
|
+
accept: `${IMAGE_BUILD_STREAM_TYPE}, application/json`
|
|
882
|
+
};
|
|
883
|
+
if (deps.token !== null) headers["authorization"] = `Bearer ${deps.token}`;
|
|
884
|
+
if (options.size !== void 0) headers["content-length"] = String(options.size);
|
|
885
|
+
const init = {
|
|
886
|
+
method: "POST",
|
|
887
|
+
headers,
|
|
888
|
+
body: context,
|
|
889
|
+
duplex: "half",
|
|
890
|
+
...options.signal !== void 0 && { signal: options.signal }
|
|
891
|
+
};
|
|
892
|
+
const request = new Request(url.href, init);
|
|
893
|
+
return readBuildAnswer(await (deps.fetch ?? fetch)(request), options.onProgress, options.onLog);
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/exec/exec-error.ts
|
|
897
|
+
var ExecError = class extends Error {
|
|
898
|
+
code;
|
|
899
|
+
data;
|
|
900
|
+
constructor(code, message, options = {}) {
|
|
901
|
+
const errorOptions = options.cause === void 0 ? {} : { cause: options.cause };
|
|
902
|
+
super(message, errorOptions);
|
|
903
|
+
this.name = "ExecError";
|
|
904
|
+
this.code = code;
|
|
905
|
+
this.data = options.data;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/exec/check-exec-require.ts
|
|
910
|
+
const CHECK_TIMEOUT_MS$1 = 5e3;
|
|
911
|
+
const FeaturesSchema = z.object({ execRequire: z.boolean().optional() });
|
|
912
|
+
const InfoAnswerSchema = z.object({ json: z.object({ features: FeaturesSchema.optional() }) });
|
|
913
|
+
async function checkExecRequire(baseUrl, token, customFetch = fetch) {
|
|
914
|
+
let response;
|
|
915
|
+
try {
|
|
916
|
+
response = await customFetch(new Request(resolveImpdUrl(baseUrl, "/rpc/system/info").href, {
|
|
917
|
+
method: "POST",
|
|
918
|
+
headers: token === null ? {} : { authorization: `Bearer ${token}` },
|
|
919
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS$1)
|
|
920
|
+
}));
|
|
921
|
+
} catch (error) {
|
|
922
|
+
return {
|
|
923
|
+
kind: "unreachable",
|
|
924
|
+
detail: `system.info: ${error instanceof Error ? error.message : String(error)}`
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
if (response.status === 401) {
|
|
928
|
+
await response.body?.cancel();
|
|
929
|
+
return { kind: "unauthorized" };
|
|
930
|
+
}
|
|
931
|
+
const body = await response.json().catch(() => null);
|
|
932
|
+
const parsed = InfoAnswerSchema.safeParse(body);
|
|
933
|
+
if (!response.ok || !parsed.success) return {
|
|
934
|
+
kind: "failed",
|
|
935
|
+
code: null,
|
|
936
|
+
message: `impd answered system.info with ${String(response.status)}; nothing was started`
|
|
937
|
+
};
|
|
938
|
+
if (parsed.data.json.features?.execRequire === true) return null;
|
|
939
|
+
const detail = "this impd is older than 0.30.0 and does not check exec requirements";
|
|
940
|
+
return {
|
|
941
|
+
kind: "failed",
|
|
942
|
+
code: "PRECONDITION_FAILED",
|
|
943
|
+
message: `nothing was started: ${detail}`,
|
|
944
|
+
data: {
|
|
945
|
+
reason: "impd_outdated",
|
|
946
|
+
detail
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region src/exec/check-impd-access.ts
|
|
952
|
+
const CHECK_TIMEOUT_MS = 5e3;
|
|
953
|
+
async function checkImpdAccess(baseUrl, token, customFetch = fetch) {
|
|
954
|
+
try {
|
|
955
|
+
const response = await customFetch(new Request(resolveImpdUrl(baseUrl, "/rpc/system/info").href, {
|
|
956
|
+
method: "POST",
|
|
957
|
+
headers: token === null ? {} : { authorization: `Bearer ${token}` },
|
|
958
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS)
|
|
959
|
+
}));
|
|
960
|
+
await response.body?.cancel();
|
|
961
|
+
return response.status === 401 ? "unauthorized" : "reachable";
|
|
962
|
+
} catch {
|
|
963
|
+
return "unreachable";
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/exec/open-exec-session.ts
|
|
968
|
+
const OPEN_TIMEOUT_MS = 1e4;
|
|
969
|
+
const DRAIN_POLL_MS = 50;
|
|
970
|
+
var BadMessageError = class extends Error {
|
|
971
|
+
name = "BadMessageError";
|
|
972
|
+
};
|
|
973
|
+
function openExecSession(options) {
|
|
974
|
+
const url = resolveImpdUrl(options.baseUrl, EXEC_PATH);
|
|
975
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
976
|
+
if (options.ticket !== void 0) url.searchParams.set(EXEC_TICKET_PARAM, options.ticket);
|
|
977
|
+
const headers = options.token === null ? {} : { authorization: `Bearer ${options.token}` };
|
|
978
|
+
const ws = options.connect(url.href, headers);
|
|
979
|
+
const outcome = Promise.withResolvers();
|
|
980
|
+
const state = {
|
|
981
|
+
opened: false,
|
|
982
|
+
sentOpen: false,
|
|
983
|
+
started: false,
|
|
984
|
+
finished: false,
|
|
985
|
+
refused: false
|
|
986
|
+
};
|
|
987
|
+
const held = [];
|
|
988
|
+
let heldBytes = 0;
|
|
989
|
+
const countQueuedBytes = () => ws.bufferedAmount + heldBytes;
|
|
990
|
+
ws.binaryType = "arraybuffer";
|
|
991
|
+
const stopSocket = () => {
|
|
992
|
+
state.finished = true;
|
|
993
|
+
clearTimeout(openTimer);
|
|
994
|
+
ws.close();
|
|
995
|
+
};
|
|
996
|
+
const resolveOutcome = (result) => {
|
|
997
|
+
if (state.finished) return;
|
|
998
|
+
stopSocket();
|
|
999
|
+
outcome.resolve(result);
|
|
1000
|
+
};
|
|
1001
|
+
const openTimer = setTimeout(() => {
|
|
1002
|
+
resolveOutcome({
|
|
1003
|
+
kind: "unreachable",
|
|
1004
|
+
detail: `no answer in ${String(OPEN_TIMEOUT_MS / 1e3)} s`
|
|
1005
|
+
});
|
|
1006
|
+
}, OPEN_TIMEOUT_MS);
|
|
1007
|
+
const sendControl = (data) => {
|
|
1008
|
+
if (state.finished || ws.readyState !== WebSocket.OPEN) return;
|
|
1009
|
+
if (state.sentOpen) ws.send(data);
|
|
1010
|
+
else {
|
|
1011
|
+
held.push(data);
|
|
1012
|
+
heldBytes += typeof data === "string" ? data.length : data.byteLength;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
const handleFrame = (data) => {
|
|
1016
|
+
let frame;
|
|
1017
|
+
try {
|
|
1018
|
+
frame = decodeExecFrame(data);
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
throw new BadMessageError(error instanceof Error ? error.message : String(error));
|
|
1021
|
+
}
|
|
1022
|
+
if (frame.channel === EXEC_CHANNELS.stdin) throw new BadMessageError("impd sent data on the stdin channel");
|
|
1023
|
+
const channel = frame.channel === EXEC_CHANNELS.stderr ? "stderr" : "stdout";
|
|
1024
|
+
options.onOutput(channel, frame.data);
|
|
1025
|
+
};
|
|
1026
|
+
const handleControl = (text) => {
|
|
1027
|
+
let json;
|
|
1028
|
+
try {
|
|
1029
|
+
json = JSON.parse(text);
|
|
1030
|
+
} catch {
|
|
1031
|
+
throw new BadMessageError(`not JSON: ${text.slice(0, 80)}`);
|
|
1032
|
+
}
|
|
1033
|
+
const parsed = ExecServerMessageSchema.safeParse(json);
|
|
1034
|
+
if (!parsed.success) throw new BadMessageError(`unknown message: ${text.slice(0, 80)}`);
|
|
1035
|
+
const message = parsed.data;
|
|
1036
|
+
if (message.type === "started") {
|
|
1037
|
+
if (state.started) return;
|
|
1038
|
+
state.started = true;
|
|
1039
|
+
options.onStarted({
|
|
1040
|
+
pid: message.pid,
|
|
1041
|
+
session: message.session ?? null,
|
|
1042
|
+
created: message.created ?? false,
|
|
1043
|
+
groupKill: message.groupKill ?? false,
|
|
1044
|
+
output: message.output ?? { continuity: "none" }
|
|
1045
|
+
});
|
|
1046
|
+
} else if (message.type === "exit") resolveOutcome({
|
|
1047
|
+
kind: "exit",
|
|
1048
|
+
code: message.code,
|
|
1049
|
+
signal: message.signal,
|
|
1050
|
+
...message.offset !== void 0 && { offset: message.offset }
|
|
1051
|
+
});
|
|
1052
|
+
else if (message.type === "detached") resolveOutcome({
|
|
1053
|
+
kind: "detached",
|
|
1054
|
+
reason: message.reason,
|
|
1055
|
+
...message.offset !== void 0 && { offset: message.offset }
|
|
1056
|
+
});
|
|
1057
|
+
else if (message.type === "stdin_ack") {} else resolveOutcome({
|
|
1058
|
+
kind: "failed",
|
|
1059
|
+
code: message.code ?? null,
|
|
1060
|
+
message: message.message,
|
|
1061
|
+
...message.data !== void 0 && { data: message.data }
|
|
1062
|
+
});
|
|
1063
|
+
};
|
|
1064
|
+
const sendOpen = async () => {
|
|
1065
|
+
const start = options.start;
|
|
1066
|
+
if ("argv" in start && start.require !== void 0 && start.require.length > 0) {
|
|
1067
|
+
const refusal = await checkExecRequire(options.baseUrl, options.token, options.fetch);
|
|
1068
|
+
if (refusal !== null) {
|
|
1069
|
+
resolveOutcome(refusal);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (state.finished || ws.readyState !== WebSocket.OPEN) return;
|
|
1074
|
+
ws.send(JSON.stringify(buildOpenMessage(start)));
|
|
1075
|
+
state.sentOpen = true;
|
|
1076
|
+
heldBytes = 0;
|
|
1077
|
+
for (const data of held.splice(0)) ws.send(data);
|
|
1078
|
+
};
|
|
1079
|
+
ws.addEventListener("open", () => {
|
|
1080
|
+
state.opened = true;
|
|
1081
|
+
clearTimeout(openTimer);
|
|
1082
|
+
sendOpen();
|
|
1083
|
+
});
|
|
1084
|
+
ws.addEventListener("message", (event) => {
|
|
1085
|
+
if (state.finished) return;
|
|
1086
|
+
try {
|
|
1087
|
+
if (event.data instanceof ArrayBuffer) handleFrame(new Uint8Array(event.data));
|
|
1088
|
+
else handleControl(String(event.data));
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
const failure = error instanceof BadMessageError ? {
|
|
1091
|
+
kind: "bad_message",
|
|
1092
|
+
detail: error.message
|
|
1093
|
+
} : {
|
|
1094
|
+
kind: "local_error",
|
|
1095
|
+
error
|
|
1096
|
+
};
|
|
1097
|
+
resolveOutcome(failure);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
const resolveRefusal = async (reason) => {
|
|
1101
|
+
if (state.refused) return;
|
|
1102
|
+
state.refused = true;
|
|
1103
|
+
const access = await checkImpdAccess(options.baseUrl, options.token, options.fetch);
|
|
1104
|
+
if (access === "unauthorized") resolveOutcome({ kind: "unauthorized" });
|
|
1105
|
+
else if (access === "reachable" && options.ticket !== void 0) resolveOutcome({
|
|
1106
|
+
kind: "unauthorized",
|
|
1107
|
+
ticketRefused: true
|
|
1108
|
+
});
|
|
1109
|
+
else if (access === "unreachable") resolveOutcome({
|
|
1110
|
+
kind: "unreachable",
|
|
1111
|
+
detail: reason
|
|
1112
|
+
});
|
|
1113
|
+
else resolveOutcome({
|
|
1114
|
+
kind: "closed",
|
|
1115
|
+
reason
|
|
1116
|
+
});
|
|
1117
|
+
};
|
|
1118
|
+
ws.addEventListener("error", () => {
|
|
1119
|
+
if (!state.opened && !state.finished) resolveRefusal("the connection failed");
|
|
1120
|
+
});
|
|
1121
|
+
ws.addEventListener("close", (event) => {
|
|
1122
|
+
if (state.finished) return;
|
|
1123
|
+
const reason = event.reason === "" ? `code ${String(event.code)}` : event.reason;
|
|
1124
|
+
if (state.opened) {
|
|
1125
|
+
resolveOutcome({
|
|
1126
|
+
kind: "closed",
|
|
1127
|
+
reason,
|
|
1128
|
+
closeCode: event.code
|
|
1129
|
+
});
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
resolveRefusal(reason);
|
|
1133
|
+
});
|
|
1134
|
+
const waitForDrain = async () => {
|
|
1135
|
+
while (!state.finished && countQueuedBytes() > 1048576) await new Promise((resolve) => {
|
|
1136
|
+
setTimeout(resolve, DRAIN_POLL_MS);
|
|
1137
|
+
});
|
|
1138
|
+
};
|
|
1139
|
+
return {
|
|
1140
|
+
outcome: outcome.promise,
|
|
1141
|
+
isStarted: () => state.started,
|
|
1142
|
+
sendStdin: (data) => {
|
|
1143
|
+
sendControl(encodeExecFrame(EXEC_CHANNELS.stdin, data));
|
|
1144
|
+
return countQueuedBytes() <= EXEC_HIGH_WATER_BYTES;
|
|
1145
|
+
},
|
|
1146
|
+
waitForDrain,
|
|
1147
|
+
closeStdin: () => {
|
|
1148
|
+
sendControl(JSON.stringify({ type: "stdin_eof" }));
|
|
1149
|
+
},
|
|
1150
|
+
resize: (cols, rows) => {
|
|
1151
|
+
sendControl(JSON.stringify({
|
|
1152
|
+
type: "resize",
|
|
1153
|
+
cols,
|
|
1154
|
+
rows
|
|
1155
|
+
}));
|
|
1156
|
+
},
|
|
1157
|
+
sendSignal: (signal) => {
|
|
1158
|
+
sendControl(JSON.stringify({
|
|
1159
|
+
type: "signal",
|
|
1160
|
+
signal
|
|
1161
|
+
}));
|
|
1162
|
+
},
|
|
1163
|
+
stop: () => {
|
|
1164
|
+
stopSocket();
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function buildOpenMessage(start) {
|
|
1169
|
+
if ("argv" in start) {
|
|
1170
|
+
const { require: requirements, ...rest } = start;
|
|
1171
|
+
return {
|
|
1172
|
+
type: "start",
|
|
1173
|
+
...rest,
|
|
1174
|
+
argv: [...start.argv],
|
|
1175
|
+
...requirements !== void 0 && { require: [...requirements] }
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
type: "attach",
|
|
1180
|
+
...start
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
//#endregion
|
|
1184
|
+
//#region src/exec/invalid-resume-error.ts
|
|
1185
|
+
var InvalidResumeError = class extends ExecError {
|
|
1186
|
+
constructor(message, data) {
|
|
1187
|
+
super("INVALID_RESUME", message, { data });
|
|
1188
|
+
this.name = "InvalidResumeError";
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/exec/invalid-state-error.ts
|
|
1193
|
+
var InvalidStateError = class extends ExecError {
|
|
1194
|
+
constructor(message, data) {
|
|
1195
|
+
super("INVALID_STATE", message, { data });
|
|
1196
|
+
this.name = "InvalidStateError";
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
//#endregion
|
|
1200
|
+
//#region src/exec/no-session-error.ts
|
|
1201
|
+
var NoSessionError = class extends ExecError {
|
|
1202
|
+
constructor(message, data) {
|
|
1203
|
+
super("NO_SESSION", message, { data });
|
|
1204
|
+
this.name = "NoSessionError";
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
//#endregion
|
|
1208
|
+
//#region src/exec/to-exec-error.ts
|
|
1209
|
+
function toExecError(outcome) {
|
|
1210
|
+
if (outcome.kind === "failed") return toFailedError(outcome.code, outcome.message, outcome.data);
|
|
1211
|
+
if (outcome.kind === "unauthorized") return new ExecError("UNAUTHORIZED", outcome.ticketRefused === true ? "impd refused the exec ticket: it expired (30 s) or was used already" : "impd rejected the token");
|
|
1212
|
+
if (outcome.kind === "unreachable") return new ExecError("UNREACHABLE", `cannot reach impd (${outcome.detail})`);
|
|
1213
|
+
if (outcome.kind === "detached") return new ExecError("DETACHED", `detached from the session (${outcome.reason})`, { data: {
|
|
1214
|
+
reason: outcome.reason,
|
|
1215
|
+
...outcome.offset !== void 0 && { offset: outcome.offset }
|
|
1216
|
+
} });
|
|
1217
|
+
if (outcome.kind === "closed") return outcome.closeCode === 1012 ? new ExecError("RESTARTING", "impd is restarting") : new ExecError("CONNECTION_CLOSED", `exec connection closed (${outcome.reason})`);
|
|
1218
|
+
if (outcome.kind === "bad_message") return new ExecError("BAD_MESSAGE", `bad message from impd: ${outcome.detail}`);
|
|
1219
|
+
return new ExecError("LOCAL_ERROR", "an output handler threw", { cause: outcome.error });
|
|
1220
|
+
}
|
|
1221
|
+
function toFailedError(code, message, data) {
|
|
1222
|
+
if (code === "NO_SESSION") {
|
|
1223
|
+
const parsed = NoSessionDataSchema.safeParse(data);
|
|
1224
|
+
return new NoSessionError(message, parsed.success ? parsed.data : void 0);
|
|
1225
|
+
}
|
|
1226
|
+
if (code === "INVALID_STATE") {
|
|
1227
|
+
const parsed = IMP_ERRORS.INVALID_STATE.data.safeParse(data);
|
|
1228
|
+
if (parsed.success) return new InvalidStateError(message, parsed.data);
|
|
1229
|
+
}
|
|
1230
|
+
if (code === "INVALID_RESUME") {
|
|
1231
|
+
const parsed = InvalidResumeDataSchema.safeParse(data);
|
|
1232
|
+
if (parsed.success) return new InvalidResumeError(message, parsed.data);
|
|
1233
|
+
}
|
|
1234
|
+
return new ExecError(code ?? "EXEC_ERROR", message, { data });
|
|
1235
|
+
}
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region src/exec/open-exec.ts
|
|
1238
|
+
const DEFAULT_MAX_UNREAD_BYTES = 8388608;
|
|
1239
|
+
const TTY_SIGNAL_KEYS = {
|
|
1240
|
+
SIGINT: "",
|
|
1241
|
+
SIGQUIT: ""
|
|
1242
|
+
};
|
|
1243
|
+
function openExec(deps, name, argv, options = {}) {
|
|
1244
|
+
const tty = options.tty ?? false;
|
|
1245
|
+
return openHandle(deps, options, tty, {
|
|
1246
|
+
name,
|
|
1247
|
+
argv,
|
|
1248
|
+
tty,
|
|
1249
|
+
...options.env !== void 0 && { env: options.env },
|
|
1250
|
+
...options.cwd !== void 0 && { cwd: options.cwd },
|
|
1251
|
+
...options.cols !== void 0 && { cols: options.cols },
|
|
1252
|
+
...options.rows !== void 0 && { rows: options.rows },
|
|
1253
|
+
...options.session !== void 0 && { session: options.session },
|
|
1254
|
+
...options.killGraceMs !== void 0 && { killGraceMs: options.killGraceMs },
|
|
1255
|
+
...options.resumeFrom !== void 0 && { resumeFrom: options.resumeFrom },
|
|
1256
|
+
...options.require !== void 0 && { require: options.require },
|
|
1257
|
+
...options.log === true && { log: true }
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
function openAttach(deps, name, session, options = {}) {
|
|
1261
|
+
return openHandle(deps, options, true, {
|
|
1262
|
+
name,
|
|
1263
|
+
session,
|
|
1264
|
+
...options.cols !== void 0 && { cols: options.cols },
|
|
1265
|
+
...options.rows !== void 0 && { rows: options.rows },
|
|
1266
|
+
...options.resumeFrom !== void 0 && { resumeFrom: options.resumeFrom },
|
|
1267
|
+
...options.wake !== void 0 && { wake: options.wake }
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
async function openHandle(deps, options, tty, start) {
|
|
1271
|
+
const name = start.name;
|
|
1272
|
+
const abort = options.signal;
|
|
1273
|
+
const callOptions = abort === void 0 ? {} : { signal: abort };
|
|
1274
|
+
abort?.throwIfAborted();
|
|
1275
|
+
const issued = await deps.rpc.exec.ticket({ name }, callOptions);
|
|
1276
|
+
abort?.throwIfAborted();
|
|
1277
|
+
const maxUnreadBytes = options.maxUnreadBytes ?? DEFAULT_MAX_UNREAD_BYTES;
|
|
1278
|
+
const started = Promise.withResolvers();
|
|
1279
|
+
const ended = Promise.withResolvers();
|
|
1280
|
+
const state = {
|
|
1281
|
+
ended: false,
|
|
1282
|
+
cancelled: 0
|
|
1283
|
+
};
|
|
1284
|
+
const stopWith = (code, message) => {
|
|
1285
|
+
session.stop();
|
|
1286
|
+
resolveHandle({ error: new ExecError(code, message) });
|
|
1287
|
+
};
|
|
1288
|
+
const outputHooks = {
|
|
1289
|
+
maxUnreadBytes,
|
|
1290
|
+
onOverflow: (channel) => {
|
|
1291
|
+
stopWith("OUTPUT_OVERFLOW", `${channel} holds more than ${String(maxUnreadBytes)} unread bytes; read or cancel it`);
|
|
1292
|
+
},
|
|
1293
|
+
onCancel: () => {
|
|
1294
|
+
state.cancelled += 1;
|
|
1295
|
+
if (state.cancelled === 2) stopWith("CLOSED", "both output streams were cancelled");
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
const stdout = buildOutputStream("stdout", outputHooks);
|
|
1299
|
+
const stderr = buildOutputStream("stderr", outputHooks);
|
|
1300
|
+
const session = openExecSession({
|
|
1301
|
+
baseUrl: deps.baseUrl,
|
|
1302
|
+
token: deps.token,
|
|
1303
|
+
ticket: issued.ticket,
|
|
1304
|
+
start,
|
|
1305
|
+
onStarted: (info) => {
|
|
1306
|
+
started.resolve({ value: info });
|
|
1307
|
+
},
|
|
1308
|
+
onOutput: (channel, data) => {
|
|
1309
|
+
(channel === "stderr" ? stderr : stdout).push(data);
|
|
1310
|
+
},
|
|
1311
|
+
connect: deps.connect ?? ((url) => new WebSocket(url)),
|
|
1312
|
+
...deps.fetch !== void 0 && { fetch: deps.fetch }
|
|
1313
|
+
});
|
|
1314
|
+
const resolveHandle = (result) => {
|
|
1315
|
+
if (state.ended) return;
|
|
1316
|
+
state.ended = true;
|
|
1317
|
+
stdout.end();
|
|
1318
|
+
stderr.end();
|
|
1319
|
+
abort?.removeEventListener("abort", stopForAbort);
|
|
1320
|
+
if ("error" in result) started.resolve({ error: result.error });
|
|
1321
|
+
ended.resolve(result);
|
|
1322
|
+
};
|
|
1323
|
+
const stopSession = () => {
|
|
1324
|
+
stopWith("CLOSED", "the exec session was closed");
|
|
1325
|
+
};
|
|
1326
|
+
const stopForAbort = () => {
|
|
1327
|
+
if (session.isStarted()) {
|
|
1328
|
+
stopSession();
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
session.stop();
|
|
1332
|
+
resolveHandle({ error: readAbortReason(abort) });
|
|
1333
|
+
};
|
|
1334
|
+
const waitForOutcome = async () => {
|
|
1335
|
+
const outcome = await session.outcome;
|
|
1336
|
+
const result = outcome.kind === "exit" ? { value: {
|
|
1337
|
+
code: outcome.code,
|
|
1338
|
+
signal: outcome.signal,
|
|
1339
|
+
...outcome.offset !== void 0 && { offset: outcome.offset }
|
|
1340
|
+
} } : { error: toExecError(outcome) };
|
|
1341
|
+
resolveHandle(result);
|
|
1342
|
+
};
|
|
1343
|
+
if (abort?.aborted === true) stopForAbort();
|
|
1344
|
+
else abort?.addEventListener("abort", stopForAbort, { once: true });
|
|
1345
|
+
waitForOutcome();
|
|
1346
|
+
return buildHandle(session, {
|
|
1347
|
+
started: started.promise,
|
|
1348
|
+
ended: ended.promise,
|
|
1349
|
+
isEnded: () => state.ended,
|
|
1350
|
+
tty,
|
|
1351
|
+
stopSession,
|
|
1352
|
+
streams: [stdout.stream, stderr.stream]
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
function readAbortReason(abort) {
|
|
1356
|
+
const reason = abort?.reason;
|
|
1357
|
+
return reason instanceof Error ? reason : new DOMException("the exec was aborted", "AbortError");
|
|
1358
|
+
}
|
|
1359
|
+
function openConsole(deps, name, options = {}) {
|
|
1360
|
+
return openExec(deps, name, [
|
|
1361
|
+
"/bin/sh",
|
|
1362
|
+
"-c",
|
|
1363
|
+
CONSOLE_SHELL
|
|
1364
|
+
], {
|
|
1365
|
+
...options,
|
|
1366
|
+
tty: true,
|
|
1367
|
+
env: options.env ?? { TERM: "xterm-256color" }
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
async function runCommand(deps, name, argv, options = {}) {
|
|
1371
|
+
const handle = await openExec(deps, name, argv, options);
|
|
1372
|
+
const output = Promise.all([readAll(handle.stdout), readAll(handle.stderr)]);
|
|
1373
|
+
if (options.stdin !== void 0) await handle.write(options.stdin).catch(checkWriteFailure);
|
|
1374
|
+
await handle.closeStdin();
|
|
1375
|
+
const [exit, [stdout, stderr]] = await Promise.all([handle.exit, output]);
|
|
1376
|
+
return {
|
|
1377
|
+
...exit,
|
|
1378
|
+
stdout,
|
|
1379
|
+
stderr
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
const STDIN_FRAME_BYTES = 1024 ** 2;
|
|
1383
|
+
function checkWriteFailure(error) {
|
|
1384
|
+
if (!(error instanceof ExecError && error.code === "CLOSED")) throw error;
|
|
1385
|
+
}
|
|
1386
|
+
function buildHandle(session, parts) {
|
|
1387
|
+
const encoder = new TextEncoder();
|
|
1388
|
+
const waitForStart = async () => {
|
|
1389
|
+
const result = await parts.started;
|
|
1390
|
+
if ("error" in result) throw result.error;
|
|
1391
|
+
return result.value;
|
|
1392
|
+
};
|
|
1393
|
+
const waitForExit = async () => {
|
|
1394
|
+
const result = await parts.ended;
|
|
1395
|
+
if ("error" in result) throw result.error;
|
|
1396
|
+
return result.value;
|
|
1397
|
+
};
|
|
1398
|
+
const startedPromise = waitForStart();
|
|
1399
|
+
const exitPromise = waitForExit();
|
|
1400
|
+
waitIgnoringRejection(startedPromise);
|
|
1401
|
+
waitIgnoringRejection(exitPromise);
|
|
1402
|
+
const end = { error: null };
|
|
1403
|
+
(async () => {
|
|
1404
|
+
const result = await parts.ended;
|
|
1405
|
+
if ("error" in result) end.error = result.error;
|
|
1406
|
+
})();
|
|
1407
|
+
const requireOpen = () => {
|
|
1408
|
+
if (!parts.isEnded()) return;
|
|
1409
|
+
throw end.error ?? new ExecError("CLOSED", "the exec session has ended");
|
|
1410
|
+
};
|
|
1411
|
+
const write = async (data) => {
|
|
1412
|
+
requireOpen();
|
|
1413
|
+
await startedPromise;
|
|
1414
|
+
requireOpen();
|
|
1415
|
+
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
1416
|
+
for (let sent = 0; sent < bytes.byteLength; sent += STDIN_FRAME_BYTES) {
|
|
1417
|
+
requireOpen();
|
|
1418
|
+
if (!session.sendStdin(bytes.subarray(sent, sent + STDIN_FRAME_BYTES))) await session.waitForDrain();
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
const sendKey = async (key) => {
|
|
1422
|
+
try {
|
|
1423
|
+
await write(key);
|
|
1424
|
+
} catch {}
|
|
1425
|
+
};
|
|
1426
|
+
return {
|
|
1427
|
+
started: startedPromise,
|
|
1428
|
+
stdout: parts.streams[0],
|
|
1429
|
+
stderr: parts.streams[1],
|
|
1430
|
+
exit: exitPromise,
|
|
1431
|
+
write,
|
|
1432
|
+
closeStdin: async () => {
|
|
1433
|
+
await startedPromise;
|
|
1434
|
+
if (!parts.isEnded()) session.closeStdin();
|
|
1435
|
+
},
|
|
1436
|
+
resize: (cols, rows) => {
|
|
1437
|
+
session.resize(cols, rows);
|
|
1438
|
+
},
|
|
1439
|
+
sendSignal: (signal) => {
|
|
1440
|
+
const key = parts.tty ? TTY_SIGNAL_KEYS[signal] : void 0;
|
|
1441
|
+
if (key === void 0) session.sendSignal(signal);
|
|
1442
|
+
else sendKey(key);
|
|
1443
|
+
},
|
|
1444
|
+
close: parts.stopSession
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
async function waitIgnoringRejection(promise) {
|
|
1448
|
+
try {
|
|
1449
|
+
await promise;
|
|
1450
|
+
} catch {}
|
|
1451
|
+
}
|
|
1452
|
+
function buildOutputStream(channel, hooks) {
|
|
1453
|
+
const state = {
|
|
1454
|
+
controller: null,
|
|
1455
|
+
open: true
|
|
1456
|
+
};
|
|
1457
|
+
return {
|
|
1458
|
+
stream: new ReadableStream({
|
|
1459
|
+
start: (controller) => {
|
|
1460
|
+
state.controller = controller;
|
|
1461
|
+
},
|
|
1462
|
+
cancel: () => {
|
|
1463
|
+
state.open = false;
|
|
1464
|
+
hooks.onCancel();
|
|
1465
|
+
}
|
|
1466
|
+
}, {
|
|
1467
|
+
highWaterMark: hooks.maxUnreadBytes,
|
|
1468
|
+
size: (chunk) => chunk?.byteLength ?? 0
|
|
1469
|
+
}),
|
|
1470
|
+
push: (data) => {
|
|
1471
|
+
if (!state.open || state.controller === null) return;
|
|
1472
|
+
state.controller.enqueue(data);
|
|
1473
|
+
if ((state.controller.desiredSize ?? 0) < 0) hooks.onOverflow(channel);
|
|
1474
|
+
},
|
|
1475
|
+
end: () => {
|
|
1476
|
+
if (state.open) {
|
|
1477
|
+
state.open = false;
|
|
1478
|
+
state.controller?.close();
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
async function readAll(stream) {
|
|
1484
|
+
const buffer = await new Response(stream).arrayBuffer();
|
|
1485
|
+
return new Uint8Array(buffer);
|
|
1486
|
+
}
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/require-awake.ts
|
|
1489
|
+
async function requireAwake(rpc, name, options = {}) {
|
|
1490
|
+
const signal = options.signal;
|
|
1491
|
+
const callOptions = signal === void 0 ? {} : { signal };
|
|
1492
|
+
const retry = options.retryUnavailable ?? {
|
|
1493
|
+
attempts: 0,
|
|
1494
|
+
delayMs: 0
|
|
1495
|
+
};
|
|
1496
|
+
const input = {
|
|
1497
|
+
name,
|
|
1498
|
+
restartError: options.restartError === true
|
|
1499
|
+
};
|
|
1500
|
+
for (let attempt = 0;; attempt++) try {
|
|
1501
|
+
return await rpc.imps.wake(input, callOptions);
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
if (!isUnavailable(error) || attempt >= retry.attempts) throw error;
|
|
1504
|
+
await waitFor(retry.delayMs, signal);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
function isUnavailable(error) {
|
|
1508
|
+
if (error instanceof ORPCError$1) return error.code === "SERVICE_UNAVAILABLE";
|
|
1509
|
+
return error instanceof TypeError;
|
|
1510
|
+
}
|
|
1511
|
+
function waitFor(ms, signal) {
|
|
1512
|
+
signal?.throwIfAborted();
|
|
1513
|
+
const done = Promise.withResolvers();
|
|
1514
|
+
const onAbort = () => {
|
|
1515
|
+
clearTimeout(timer);
|
|
1516
|
+
done.reject(signal?.reason);
|
|
1517
|
+
};
|
|
1518
|
+
const timer = setTimeout(() => {
|
|
1519
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1520
|
+
done.resolve();
|
|
1521
|
+
}, ms);
|
|
1522
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1523
|
+
return done.promise;
|
|
1524
|
+
}
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/send-long-call.ts
|
|
1527
|
+
const HOUR_MS = 36e5;
|
|
1528
|
+
const LONG_CALL_DEADLINES_MS = /* @__PURE__ */ new Map([
|
|
1529
|
+
["backups.run", 24 * HOUR_MS],
|
|
1530
|
+
["backups.restore", 24 * HOUR_MS],
|
|
1531
|
+
["backups.check", 24 * HOUR_MS],
|
|
1532
|
+
["images.add", 6 * HOUR_MS],
|
|
1533
|
+
["images.build", 6 * HOUR_MS],
|
|
1534
|
+
["system.gc", HOUR_MS],
|
|
1535
|
+
["moves.abort", HOUR_MS],
|
|
1536
|
+
["moves.resume", HOUR_MS]
|
|
1537
|
+
]);
|
|
1538
|
+
async function sendLongCall(request, init, call) {
|
|
1539
|
+
const limit = new AbortController();
|
|
1540
|
+
const expired = Promise.withResolvers();
|
|
1541
|
+
const onCallerAbort = () => {
|
|
1542
|
+
limit.abort(request.signal.reason);
|
|
1543
|
+
};
|
|
1544
|
+
request.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
1545
|
+
if (request.signal.aborted) onCallerAbort();
|
|
1546
|
+
const cancel = call.startTimer(() => {
|
|
1547
|
+
const error = new DOMException(`impd sent no answer to ${call.procedure} in ${formatHours(call.deadlineMs)}`, "TimeoutError");
|
|
1548
|
+
limit.abort(error);
|
|
1549
|
+
expired.reject(error);
|
|
1550
|
+
}, call.deadlineMs);
|
|
1551
|
+
const signal = limit.signal;
|
|
1552
|
+
try {
|
|
1553
|
+
const limited = new Request(request, { signal });
|
|
1554
|
+
const sent = call.send === null ? fetch(limited, {
|
|
1555
|
+
...init,
|
|
1556
|
+
signal,
|
|
1557
|
+
timeout: false
|
|
1558
|
+
}) : call.send(limited);
|
|
1559
|
+
return await Promise.race([sent, expired.promise]);
|
|
1560
|
+
} finally {
|
|
1561
|
+
cancel();
|
|
1562
|
+
request.signal.removeEventListener("abort", onCallerAbort);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
function formatHours(ms) {
|
|
1566
|
+
const hours = ms / HOUR_MS;
|
|
1567
|
+
return hours === 1 ? "an hour" : `${String(hours)} hours`;
|
|
1568
|
+
}
|
|
1569
|
+
//#endregion
|
|
1570
|
+
//#region src/unknown-procedure-error.ts
|
|
1571
|
+
var UnknownProcedureError = class extends Error {
|
|
1572
|
+
procedure;
|
|
1573
|
+
constructor(procedure) {
|
|
1574
|
+
super(`impd has no procedure ${procedure}`);
|
|
1575
|
+
this.name = "UnknownProcedureError";
|
|
1576
|
+
this.procedure = procedure;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
//#endregion
|
|
1580
|
+
//#region src/create-imp-client.ts
|
|
1581
|
+
function createImpClient(options) {
|
|
1582
|
+
const token = options.token;
|
|
1583
|
+
const customFetch = options.fetch;
|
|
1584
|
+
const startTimer = options.startTimer ?? startRealTimer;
|
|
1585
|
+
const sendCall = (request, init) => customFetch === void 0 ? fetch(request, init) : customFetch(request);
|
|
1586
|
+
const link = new RPCLink({
|
|
1587
|
+
url: resolveImpdUrl(options.url, "/rpc").href,
|
|
1588
|
+
headers: token === void 0 ? {} : { authorization: `Bearer ${token}` },
|
|
1589
|
+
fetch: async (request, init, _options, path) => {
|
|
1590
|
+
const procedure = path.join(".");
|
|
1591
|
+
const deadlineMs = LONG_CALL_DEADLINES_MS.get(procedure);
|
|
1592
|
+
const response = await (deadlineMs === void 0 ? sendCall(request, init) : sendLongCall(request, init, {
|
|
1593
|
+
procedure,
|
|
1594
|
+
deadlineMs,
|
|
1595
|
+
startTimer,
|
|
1596
|
+
send: customFetch ?? null
|
|
1597
|
+
}));
|
|
1598
|
+
if (isUnknownProcedure(response.status, response.headers.get("content-type"))) {
|
|
1599
|
+
await response.body?.cancel();
|
|
1600
|
+
throw new UnknownProcedureError(procedure);
|
|
1601
|
+
}
|
|
1602
|
+
return response;
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
const rpc = createORPCClient(link);
|
|
1606
|
+
const namespaces = {
|
|
1607
|
+
imps: rpc.imps,
|
|
1608
|
+
leases: rpc.leases,
|
|
1609
|
+
checkpoints: rpc.checkpoints,
|
|
1610
|
+
backups: rpc.backups,
|
|
1611
|
+
images: rpc.images,
|
|
1612
|
+
exec: rpc.exec,
|
|
1613
|
+
sessions: rpc.sessions,
|
|
1614
|
+
services: rpc.services,
|
|
1615
|
+
files: rpc.files,
|
|
1616
|
+
moves: rpc.moves,
|
|
1617
|
+
secrets: rpc.secrets,
|
|
1618
|
+
networks: rpc.networks,
|
|
1619
|
+
grants: rpc.grants,
|
|
1620
|
+
audit: rpc.audit,
|
|
1621
|
+
events: rpc.events,
|
|
1622
|
+
system: rpc.system,
|
|
1623
|
+
tokens: rpc.tokens,
|
|
1624
|
+
oauth: rpc.oauth
|
|
1625
|
+
};
|
|
1626
|
+
const execDeps = {
|
|
1627
|
+
rpc,
|
|
1628
|
+
baseUrl: options.url,
|
|
1629
|
+
token: token ?? null,
|
|
1630
|
+
...customFetch !== void 0 && { fetch: customFetch },
|
|
1631
|
+
...options.connect !== void 0 && { connect: options.connect }
|
|
1632
|
+
};
|
|
1633
|
+
return {
|
|
1634
|
+
...namespaces,
|
|
1635
|
+
openExec: (name, argv, execOptions) => openExec(execDeps, name, argv, execOptions),
|
|
1636
|
+
run: (name, argv, runOptions) => runCommand(execDeps, name, argv, runOptions),
|
|
1637
|
+
openConsole: (name, consoleOptions) => openConsole(execDeps, name, consoleOptions),
|
|
1638
|
+
openAttach: (name, session, attachOptions) => openAttach(execDeps, name, session, attachOptions),
|
|
1639
|
+
requireAwake: (name, awakeOptions) => requireAwake(rpc, name, awakeOptions),
|
|
1640
|
+
checkServer: () => checkServer(rpc),
|
|
1641
|
+
buildImage: (name, context, buildOptions) => buildImage(execDeps, name, context, buildOptions)
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
function startRealTimer(fire, ms) {
|
|
1645
|
+
const timer = setTimeout(fire, ms);
|
|
1646
|
+
return () => {
|
|
1647
|
+
clearTimeout(timer);
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function isUnknownProcedure(status, contentType) {
|
|
1651
|
+
return status === 404 && !(contentType ?? "").startsWith("application/json");
|
|
1652
|
+
}
|
|
1653
|
+
//#endregion
|
|
1654
|
+
//#region src/reverse/open-reverse-forward.ts
|
|
1655
|
+
function parseMessage(text) {
|
|
1656
|
+
try {
|
|
1657
|
+
const parsed = TunnelServerMessageSchema.safeParse(JSON.parse(text));
|
|
1658
|
+
return parsed.success ? parsed.data : null;
|
|
1659
|
+
} catch {
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function buildListenMessage(name, guest) {
|
|
1664
|
+
return guest.network === "tcp" ? {
|
|
1665
|
+
type: "listen",
|
|
1666
|
+
name,
|
|
1667
|
+
network: "tcp",
|
|
1668
|
+
port: guest.port
|
|
1669
|
+
} : {
|
|
1670
|
+
type: "listen",
|
|
1671
|
+
name,
|
|
1672
|
+
network: "unix",
|
|
1673
|
+
path: guest.path
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function openRelay(target, accept, handlers) {
|
|
1677
|
+
const ws = target.connect(target.url, target.headers);
|
|
1678
|
+
const state = {
|
|
1679
|
+
opened: false,
|
|
1680
|
+
unacked: 0,
|
|
1681
|
+
done: false,
|
|
1682
|
+
peerEof: false,
|
|
1683
|
+
guestEof: false
|
|
1684
|
+
};
|
|
1685
|
+
const waiters = [];
|
|
1686
|
+
const pending = [];
|
|
1687
|
+
ws.binaryType = "arraybuffer";
|
|
1688
|
+
const wakeWaiters = () => {
|
|
1689
|
+
for (const wake of waiters.splice(0)) wake();
|
|
1690
|
+
};
|
|
1691
|
+
const sendText = (message) => {
|
|
1692
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
|
|
1693
|
+
};
|
|
1694
|
+
const sendFrames = (data) => {
|
|
1695
|
+
for (let offset = 0; offset < data.byteLength; offset += TUNNEL_MAX_FRAME_BYTES) ws.send(Uint8Array.from(data.subarray(offset, offset + TUNNEL_MAX_FRAME_BYTES)));
|
|
1696
|
+
};
|
|
1697
|
+
const stopRelay = (lost) => {
|
|
1698
|
+
if (state.done) return;
|
|
1699
|
+
state.done = true;
|
|
1700
|
+
wakeWaiters();
|
|
1701
|
+
handlers.onClose(lost);
|
|
1702
|
+
};
|
|
1703
|
+
const sendAck = async (data) => {
|
|
1704
|
+
await handlers.onData(data);
|
|
1705
|
+
sendText({
|
|
1706
|
+
type: "ack",
|
|
1707
|
+
bytes: data.byteLength
|
|
1708
|
+
});
|
|
1709
|
+
};
|
|
1710
|
+
const handleControl = (message) => {
|
|
1711
|
+
if (message.type === "opened") {
|
|
1712
|
+
state.opened = true;
|
|
1713
|
+
for (const data of pending.splice(0)) sendFrames(data);
|
|
1714
|
+
if (state.peerEof) sendText({ type: "eof" });
|
|
1715
|
+
} else if (message.type === "eof") {
|
|
1716
|
+
state.guestEof = true;
|
|
1717
|
+
handlers.onEof();
|
|
1718
|
+
} else if (message.type === "ack") {
|
|
1719
|
+
state.unacked -= message.bytes;
|
|
1720
|
+
if (state.unacked <= 1048576) wakeWaiters();
|
|
1721
|
+
} else {
|
|
1722
|
+
ws.close(TUNNEL_CLOSE_NORMAL, "relay failed");
|
|
1723
|
+
stopRelay(true);
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
ws.addEventListener("open", () => {
|
|
1727
|
+
sendText(accept);
|
|
1728
|
+
});
|
|
1729
|
+
ws.addEventListener("message", (event) => {
|
|
1730
|
+
if (event.data instanceof ArrayBuffer) {
|
|
1731
|
+
sendAck(new Uint8Array(event.data));
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const message = parseMessage(String(event.data));
|
|
1735
|
+
if (message === null) {
|
|
1736
|
+
ws.close(TUNNEL_CLOSE_PROTOCOL, "bad message");
|
|
1737
|
+
stopRelay(true);
|
|
1738
|
+
} else handleControl(message);
|
|
1739
|
+
});
|
|
1740
|
+
ws.addEventListener("close", (event) => {
|
|
1741
|
+
stopRelay(event.code !== TUNNEL_CLOSE_NORMAL);
|
|
1742
|
+
});
|
|
1743
|
+
ws.addEventListener("error", () => {});
|
|
1744
|
+
return {
|
|
1745
|
+
send: (data) => {
|
|
1746
|
+
if (state.done) return true;
|
|
1747
|
+
if (state.opened) sendFrames(data);
|
|
1748
|
+
else pending.push(Uint8Array.from(data));
|
|
1749
|
+
state.unacked += data.byteLength;
|
|
1750
|
+
return state.unacked <= TUNNEL_WINDOW_BYTES;
|
|
1751
|
+
},
|
|
1752
|
+
waitForRoom: async () => {
|
|
1753
|
+
while (!state.done && state.unacked > 1048576) await new Promise((resolve) => {
|
|
1754
|
+
waiters.push(resolve);
|
|
1755
|
+
});
|
|
1756
|
+
},
|
|
1757
|
+
sendEof: () => {
|
|
1758
|
+
state.peerEof = true;
|
|
1759
|
+
if (state.opened) sendText({ type: "eof" });
|
|
1760
|
+
},
|
|
1761
|
+
close: () => {
|
|
1762
|
+
if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) ws.close(TUNNEL_CLOSE_NORMAL, "local side closed");
|
|
1763
|
+
stopRelay(false);
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function openReverseForward(options) {
|
|
1768
|
+
const url = resolveImpdUrl(options.baseUrl, TUNNEL_PATH);
|
|
1769
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1770
|
+
const headers = options.token === null ? {} : { authorization: `Bearer ${options.token}` };
|
|
1771
|
+
const target = {
|
|
1772
|
+
url: url.href,
|
|
1773
|
+
headers,
|
|
1774
|
+
connect: options.connect
|
|
1775
|
+
};
|
|
1776
|
+
const ws = options.connect(url.href, headers);
|
|
1777
|
+
const listening = Promise.withResolvers();
|
|
1778
|
+
const ended = Promise.withResolvers();
|
|
1779
|
+
const relays = /* @__PURE__ */ new Set();
|
|
1780
|
+
const state = {
|
|
1781
|
+
listener: "",
|
|
1782
|
+
done: false
|
|
1783
|
+
};
|
|
1784
|
+
const waitForListening = async () => {
|
|
1785
|
+
try {
|
|
1786
|
+
await listening.promise;
|
|
1787
|
+
} catch {}
|
|
1788
|
+
};
|
|
1789
|
+
waitForListening();
|
|
1790
|
+
const stopForward = (end) => {
|
|
1791
|
+
if (state.done) return;
|
|
1792
|
+
state.done = true;
|
|
1793
|
+
for (const relay of relays) relay.close();
|
|
1794
|
+
const why = end.kind === "failed" ? end.message : `the forward ${end.kind}`;
|
|
1795
|
+
listening.reject(new Error(why));
|
|
1796
|
+
ended.resolve(end);
|
|
1797
|
+
};
|
|
1798
|
+
const openClientRelay = (id) => {
|
|
1799
|
+
const message = {
|
|
1800
|
+
type: "accept",
|
|
1801
|
+
name: options.name,
|
|
1802
|
+
listener: state.listener,
|
|
1803
|
+
connection: id
|
|
1804
|
+
};
|
|
1805
|
+
options.onConnection((handlers) => {
|
|
1806
|
+
const relay = openRelay(target, message, {
|
|
1807
|
+
...handlers,
|
|
1808
|
+
onClose: (lost) => {
|
|
1809
|
+
relays.delete(relay);
|
|
1810
|
+
handlers.onClose(lost);
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
relays.add(relay);
|
|
1814
|
+
return relay;
|
|
1815
|
+
});
|
|
1816
|
+
};
|
|
1817
|
+
const handleControl = (message) => {
|
|
1818
|
+
if (message.type === "listening") {
|
|
1819
|
+
state.listener = message.listener;
|
|
1820
|
+
listening.resolve({
|
|
1821
|
+
path: message.path,
|
|
1822
|
+
port: message.port
|
|
1823
|
+
});
|
|
1824
|
+
} else if (message.type === "connection") openClientRelay(message.id);
|
|
1825
|
+
else if (message.type === "error") stopForward({
|
|
1826
|
+
kind: "failed",
|
|
1827
|
+
code: message.code ?? null,
|
|
1828
|
+
message: message.message
|
|
1829
|
+
});
|
|
1830
|
+
else ws.close(TUNNEL_CLOSE_PROTOCOL, "bad message");
|
|
1831
|
+
};
|
|
1832
|
+
ws.binaryType = "arraybuffer";
|
|
1833
|
+
ws.addEventListener("open", () => {
|
|
1834
|
+
ws.send(JSON.stringify(buildListenMessage(options.name, options.guest)));
|
|
1835
|
+
});
|
|
1836
|
+
ws.addEventListener("message", (event) => {
|
|
1837
|
+
const message = event.data instanceof ArrayBuffer ? null : parseMessage(String(event.data));
|
|
1838
|
+
if (message === null) ws.close(TUNNEL_CLOSE_PROTOCOL, "bad message");
|
|
1839
|
+
else handleControl(message);
|
|
1840
|
+
});
|
|
1841
|
+
ws.addEventListener("close", (event) => {
|
|
1842
|
+
if (event.code === 4e3) stopForward({ kind: "lost" });
|
|
1843
|
+
else stopForward({
|
|
1844
|
+
kind: "closed",
|
|
1845
|
+
code: event.code,
|
|
1846
|
+
reason: event.reason
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
ws.addEventListener("error", () => {});
|
|
1850
|
+
return {
|
|
1851
|
+
listening: listening.promise,
|
|
1852
|
+
ended: ended.promise,
|
|
1853
|
+
stop: () => {
|
|
1854
|
+
if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) ws.close(TUNNEL_CLOSE_NORMAL, "stopped");
|
|
1855
|
+
stopForward({ kind: "stopped" });
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
//#endregion
|
|
1860
|
+
export { CLIENT_VERSION, CONSOLE_SHELL, EXEC_REQUIREMENTS, ExecError, InvalidResumeError, InvalidStateError, NoSessionError, ORPCError, UnknownProcedureError, createImpClient, isDefinedError, openExecSession, openReverseForward, safe };
|