@impz/client 0.0.0-stage → 2.0.7

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.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.7";
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 };