@zgeoff/imp-client 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { RPCLink } from "@orpc/client/fetch";
3
3
  import * as z from "zod";
4
4
  //#endregion
5
5
  //#region src/check-server.ts
6
- const CLIENT_VERSION = "0.16.0";
6
+ const CLIENT_VERSION = "0.17.0";
7
7
  async function checkServer(rpc) {
8
8
  const info = await rpc.system.info();
9
9
  return {
@@ -28,6 +28,137 @@ function parseVersion(version) {
28
28
  //#endregion
29
29
  //#region ../api/src/name-schema.ts
30
30
  const NameSchema = z.string().regex(/^[a-z][a-z0-9-]{0,30}$/, "must be a lowercase letter followed by up to 30 lowercase letters, digits or hyphens");
31
+ z.enum(["tailnet", "public"]);
32
+ const PublicAuthSchema = z.enum([
33
+ "none",
34
+ "token",
35
+ "basic"
36
+ ]);
37
+ const BasicUserSchema = z.string().regex(/^[!-9;-~]{1,64}$/, "must be 1 to 64 printable characters, without a colon");
38
+ z.object({
39
+ name: NameSchema,
40
+ auth: PublicAuthSchema.default("token"),
41
+ user: BasicUserSchema.optional()
42
+ }).refine((input) => input.auth === "basic" || input.user === void 0, {
43
+ message: "only basic auth takes a user",
44
+ path: ["user"]
45
+ });
46
+ z.object({
47
+ url: z.url(),
48
+ auth: PublicAuthSchema,
49
+ user: z.string().nullable(),
50
+ credential: z.string().nullable(),
51
+ warning: z.string().optional()
52
+ });
53
+ z.string().regex(/^[\w.:-]{1,64}$/, "must be 1–64 letters, digits, dots, underscores, colons or hyphens");
54
+ z.int().min(10).max(3600);
55
+ const LeaseOwnerSchema = z.object({
56
+ principal: z.string(),
57
+ display: z.string(),
58
+ label: z.string()
59
+ });
60
+ const LeaseSchema = z.object({
61
+ name: NameSchema,
62
+ owner: LeaseOwnerSchema,
63
+ until: z.date().nullable()
64
+ });
65
+ const LeaseSummarySchema = z.object({
66
+ leases: z.array(LeaseSchema).readonly(),
67
+ otherCount: z.int().nonnegative()
68
+ });
69
+ //#endregion
70
+ //#region ../api/src/move-schema.ts
71
+ const MoveStateSchema = z.enum([
72
+ "sending",
73
+ "moved",
74
+ "receiving"
75
+ ]);
76
+ const PeerUrlSchema = z.url({ protocol: /^https?$/ });
77
+ z.object({
78
+ bytes: z.int().nonnegative(),
79
+ checkpoints: z.int().nonnegative()
80
+ });
81
+ z.object({
82
+ ticket: z.string(),
83
+ expiresAt: z.date(),
84
+ peerUrl: PeerUrlSchema
85
+ });
86
+ z.object({
87
+ state: MoveStateSchema.nullable(),
88
+ peer: z.string().nullable(),
89
+ sentBytes: z.int().nonnegative(),
90
+ totalBytes: z.int().nonnegative(),
91
+ isDone: z.boolean(),
92
+ error: z.string().nullable()
93
+ });
94
+ //#endregion
95
+ //#region ../api/src/imp-schema.ts
96
+ const ImpStateSchema = z.enum([
97
+ "creating",
98
+ "running",
99
+ "sleeping",
100
+ "stopped",
101
+ "error"
102
+ ]);
103
+ const OutdatedPartSchema = z.enum([
104
+ "firecracker",
105
+ "kernel",
106
+ "agent",
107
+ "impd",
108
+ "ipv6"
109
+ ]);
110
+ const ImpResourcesSchema = z.object({
111
+ wakeCount: z.int().nonnegative(),
112
+ awakeMs: z.int().nonnegative(),
113
+ sample: z.object({
114
+ measuredAt: z.date(),
115
+ since: z.date(),
116
+ cpuPercent: z.number().nonnegative().optional(),
117
+ cpuThrottledMs: z.int().nonnegative(),
118
+ netRxBytes: z.int().nonnegative(),
119
+ netTxBytes: z.int().nonnegative()
120
+ }).optional()
121
+ });
122
+ z.object({
123
+ id: z.string(),
124
+ name: NameSchema,
125
+ image: NameSchema,
126
+ state: ImpStateSchema,
127
+ vcpus: z.int().positive(),
128
+ memoryMib: z.int().positive(),
129
+ diskMib: z.int().positive(),
130
+ diskUsage: z.object({
131
+ exclusiveBytes: z.int().nonnegative(),
132
+ sharedBytes: z.int().nonnegative(),
133
+ measuredAt: z.date(),
134
+ isPartial: z.boolean(),
135
+ isUpperBound: z.boolean()
136
+ }).optional(),
137
+ ip: z.ipv4(),
138
+ slot: z.int().nonnegative(),
139
+ port: z.int().positive(),
140
+ httpPort: z.int().positive(),
141
+ url: z.url(),
142
+ public: z.object({ auth: PublicAuthSchema }).optional(),
143
+ createdAt: z.date(),
144
+ lastActiveAt: z.date(),
145
+ ramMib: z.int().nonnegative().optional(),
146
+ rssMib: z.int().nonnegative().optional(),
147
+ sleptAt: z.date().optional(),
148
+ holdUntil: z.date().optional(),
149
+ leases: LeaseSummarySchema.optional(),
150
+ error: z.string().optional(),
151
+ sessions: z.int().nonnegative().optional(),
152
+ coldBootReason: z.string().optional(),
153
+ agentSilentSince: z.date().optional(),
154
+ outdated: z.array(OutdatedPartSchema).readonly().optional(),
155
+ cpu: z.object({
156
+ limit: z.number().positive().nullable(),
157
+ weight: z.int()
158
+ }).optional(),
159
+ resources: ImpResourcesSchema.optional(),
160
+ move: MoveStateSchema.optional()
161
+ });
31
162
  //#endregion
32
163
  //#region ../api/src/image-schema.ts
33
164
  const ImageSourceSchema = z.enum(["oci", "imp"]);
@@ -54,6 +185,67 @@ const ImageBuildErrorSchema = z.object({
54
185
  message: z.string()
55
186
  });
56
187
  //#endregion
188
+ //#region ../api/src/session-output-schema.ts
189
+ const ExecutionGenerationSchema = z.string().regex(/^[0-9a-f]{32}$/);
190
+ const OffsetSchema = z.int().nonnegative();
191
+ const ColdBootCauseSchema = z.enum([
192
+ "start",
193
+ "wake_fallback",
194
+ "watchdog",
195
+ "restore",
196
+ "recovery",
197
+ "unknown"
198
+ ]);
199
+ const ColdBootSchema = z.object({
200
+ bootId: z.string(),
201
+ cause: ColdBootCauseSchema,
202
+ at: z.iso.datetime()
203
+ }).readonly();
204
+ const ColdBootsSchema = z.array(ColdBootSchema).max(4).readonly();
205
+ const PreviousGenerationSchema = z.object({
206
+ executionGeneration: ExecutionGenerationSchema,
207
+ end: OffsetSchema,
208
+ exitCode: z.int().nullable()
209
+ }).readonly();
210
+ const ResumeFromSchema = z.object({
211
+ executionGeneration: ExecutionGenerationSchema,
212
+ offset: OffsetSchema
213
+ }).readonly();
214
+ const ResumeResultSchema = z.discriminatedUnion("kind", [
215
+ z.object({ kind: z.literal("exact") }),
216
+ z.object({
217
+ kind: z.literal("gap"),
218
+ from: OffsetSchema,
219
+ to: OffsetSchema
220
+ }),
221
+ z.object({
222
+ kind: z.literal("generation_changed"),
223
+ executionGeneration: ExecutionGenerationSchema,
224
+ firstOffset: OffsetSchema
225
+ })
226
+ ]).readonly();
227
+ const SessionOutputSchema = z.discriminatedUnion("continuity", [z.object({ continuity: z.literal("none") }), z.object({
228
+ continuity: z.literal("offsets"),
229
+ bootId: z.string(),
230
+ executionGeneration: ExecutionGenerationSchema,
231
+ bufferStart: OffsetSchema,
232
+ end: OffsetSchema,
233
+ offset: OffsetSchema,
234
+ prelude: OffsetSchema,
235
+ coldBoots: ColdBootsSchema,
236
+ previous: PreviousGenerationSchema.optional(),
237
+ resume: ResumeResultSchema.optional()
238
+ })]).readonly();
239
+ const NoSessionDataSchema = z.object({
240
+ bootId: z.string(),
241
+ coldBoots: ColdBootsSchema,
242
+ previous: PreviousGenerationSchema.optional()
243
+ }).readonly();
244
+ const InvalidResumeDataSchema = z.object({
245
+ end: OffsetSchema,
246
+ bufferStart: OffsetSchema
247
+ }).readonly();
248
+ //#endregion
57
249
  //#region ../api/src/session-schema.ts
58
250
  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");
59
251
  const SessionExitSchema = z.object({
@@ -69,7 +261,12 @@ z.object({
69
261
  cols: z.int().positive(),
70
262
  rows: z.int().positive(),
71
263
  startedAt: z.date(),
72
- exit: SessionExitSchema.optional()
264
+ exit: SessionExitSchema.optional(),
265
+ continuity: z.enum(["none", "offsets"]).optional(),
266
+ executionGeneration: ExecutionGenerationSchema.optional(),
267
+ bootId: z.string().optional(),
268
+ end: z.int().nonnegative().optional(),
269
+ endObservedAt: z.date().optional()
73
270
  });
74
271
  //#endregion
75
272
  //#region ../api/src/exec-protocol.ts
@@ -92,10 +289,14 @@ const ExecStartMessageSchema = z.object({
92
289
  rows: DimensionSchema.optional(),
93
290
  session: SessionNameSchema.optional(),
94
291
  tool: z.enum(["tar"]).optional(),
95
- killGraceMs: z.int().min(1).max(6e4).optional()
292
+ killGraceMs: z.int().min(1).max(6e4).optional(),
293
+ resumeFrom: ResumeFromSchema.optional()
96
294
  }).refine((start) => start.session === void 0 || start.tty, {
97
295
  message: "a session needs a tty",
98
296
  path: ["tty"]
297
+ }).refine((start) => start.resumeFrom === void 0 || start.session !== void 0, {
298
+ message: "only a session resumes",
299
+ path: ["resumeFrom"]
99
300
  }).refine((start) => start.killGraceMs === void 0 || !start.tty, {
100
301
  message: "a tty exec takes no kill grace",
101
302
  path: ["killGraceMs"]
@@ -108,7 +309,9 @@ const ExecAttachMessageSchema = z.object({
108
309
  name: NameSchema,
109
310
  session: SessionNameSchema,
110
311
  cols: DimensionSchema.optional(),
111
- rows: DimensionSchema.optional()
312
+ rows: DimensionSchema.optional(),
313
+ resumeFrom: ResumeFromSchema.optional(),
314
+ wake: z.boolean().optional()
112
315
  });
113
316
  z.discriminatedUnion("type", [
114
317
  ExecStartMessageSchema,
@@ -134,12 +337,14 @@ const ExecServerMessageSchema = z.discriminatedUnion("type", [
134
337
  pid: z.int().positive(),
135
338
  session: SessionNameSchema.optional(),
136
339
  created: z.boolean().optional(),
137
- groupKill: z.boolean().optional()
340
+ groupKill: z.boolean().optional(),
341
+ output: SessionOutputSchema.optional()
138
342
  }),
139
343
  z.object({
140
344
  type: z.literal("exit"),
141
345
  code: z.int().nullable(),
142
- signal: z.string().nullable()
346
+ signal: z.string().nullable(),
347
+ offset: z.int().nonnegative().optional()
143
348
  }),
144
349
  z.object({
145
350
  type: z.literal("detached"),
@@ -147,7 +352,8 @@ const ExecServerMessageSchema = z.discriminatedUnion("type", [
147
352
  "taken_over",
148
353
  "slow",
149
354
  "lost"
150
- ])
355
+ ]),
356
+ offset: z.int().nonnegative().optional()
151
357
  }),
152
358
  z.object({
153
359
  type: z.literal("stdin_ack"),
@@ -235,6 +441,102 @@ const TunnelServerMessageSchema = z.discriminatedUnion("type", [
235
441
  })
236
442
  ]);
237
443
  //#endregion
444
+ //#region ../api/src/define-errors.ts
445
+ function defineErrors(errors) {
446
+ return errors;
447
+ }
448
+ //#endregion
449
+ //#region ../api/src/imp-errors.ts
450
+ const ResourceKindSchema = z.enum([
451
+ "imp",
452
+ "image",
453
+ "checkpoint",
454
+ "session",
455
+ "service",
456
+ "secret",
457
+ "grant",
458
+ "backup",
459
+ "token",
460
+ "ssh-key",
461
+ "network"
462
+ ]);
463
+ const ResourceDataSchema = z.object({
464
+ kind: ResourceKindSchema,
465
+ name: z.string()
466
+ });
467
+ const ProtectedImpSchema = z.object({
468
+ name: NameSchema,
469
+ ramMib: z.int().nonnegative(),
470
+ leased: z.boolean(),
471
+ busy: z.boolean()
472
+ });
473
+ const IMP_ERRORS = defineErrors({
474
+ NOT_FOUND: {
475
+ message: "Not found",
476
+ data: ResourceDataSchema
477
+ },
478
+ CONFLICT: {
479
+ message: "Already exists",
480
+ data: ResourceDataSchema
481
+ },
482
+ FORBIDDEN: { message: "Not allowed" },
483
+ PRECONDITION_FAILED: { message: "Not possible on this host" },
484
+ RAM_BUDGET_EXCEEDED: {
485
+ message: "Not enough RAM budget, even after sleeping idle imps",
486
+ status: 503,
487
+ data: z.object({
488
+ budgetMib: z.int().nonnegative(),
489
+ usedMib: z.int().nonnegative(),
490
+ requestedMib: z.int().nonnegative(),
491
+ neededMib: z.int().nonnegative().optional(),
492
+ protected: z.array(ProtectedImpSchema).readonly().optional(),
493
+ protectedHidden: z.int().nonnegative().optional()
494
+ })
495
+ },
496
+ SERVICE_UNAVAILABLE: { message: "impd is stopping" },
497
+ INVALID_STATE: {
498
+ message: "The imp is not in a state that allows this",
499
+ status: 409,
500
+ data: z.object({
501
+ state: ImpStateSchema,
502
+ allowed: z.array(ImpStateSchema),
503
+ coldBoots: ColdBootsSchema.optional()
504
+ })
505
+ },
506
+ INVALID_RESUME: {
507
+ message: "The resume offset is past the end of the output",
508
+ status: 409,
509
+ data: InvalidResumeDataSchema
510
+ },
511
+ DISK_FULL: {
512
+ message: "Not enough free disk on the host",
513
+ status: 507,
514
+ data: z.object({
515
+ availableBytes: z.int().nonnegative(),
516
+ reserveBytes: z.int().nonnegative(),
517
+ requestedBytes: z.int().nonnegative()
518
+ })
519
+ },
520
+ LEASED: {
521
+ message: "The imp is leased",
522
+ status: 409,
523
+ data: LeaseSummarySchema
524
+ },
525
+ LEASE_NOT_HELD: {
526
+ message: "The caller holds no such lease",
527
+ status: 409
528
+ },
529
+ AGENT_OUTDATED: {
530
+ message: "The imp's agent is too old for this",
531
+ status: 409
532
+ },
533
+ MOVING: {
534
+ message: "The imp is moving between hosts",
535
+ status: 409,
536
+ data: z.object({ retryAfterS: z.int().positive() })
537
+ }
538
+ });
539
+ //#endregion
238
540
  //#region ../api/src/login-shell.ts
239
541
  const CONSOLE_SHELL = [
240
542
  "uid=$(id -u 2>/dev/null) || uid=0",
@@ -297,15 +599,6 @@ var ExecError = class extends Error {
297
599
  this.data = options.data;
298
600
  }
299
601
  };
300
- function toExecError(outcome) {
301
- if (outcome.kind === "failed") return new ExecError(outcome.code ?? "EXEC_ERROR", outcome.message, { data: outcome.data });
302
- 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");
303
- if (outcome.kind === "unreachable") return new ExecError("UNREACHABLE", `cannot reach impd (${outcome.detail})`);
304
- if (outcome.kind === "detached") return new ExecError("DETACHED", `detached from the session (${outcome.reason})`, { data: { reason: outcome.reason } });
305
- if (outcome.kind === "closed") return outcome.closeCode === 1012 ? new ExecError("RESTARTING", "impd is restarting") : new ExecError("CONNECTION_CLOSED", `exec connection closed (${outcome.reason})`);
306
- if (outcome.kind === "bad_message") return new ExecError("BAD_MESSAGE", `bad message from impd: ${outcome.detail}`);
307
- return new ExecError("LOCAL_ERROR", "an output handler threw", { cause: outcome.error });
308
- }
309
602
  //#endregion
310
603
  //#region src/exec/check-impd-access.ts
311
604
  const CHECK_TIMEOUT_MS = 5e3;
@@ -391,16 +684,19 @@ function openExecSession(options) {
391
684
  pid: message.pid,
392
685
  session: message.session ?? null,
393
686
  created: message.created ?? false,
394
- groupKill: message.groupKill ?? false
687
+ groupKill: message.groupKill ?? false,
688
+ output: message.output ?? { continuity: "none" }
395
689
  });
396
690
  } else if (message.type === "exit") resolveOutcome({
397
691
  kind: "exit",
398
692
  code: message.code,
399
- signal: message.signal
693
+ signal: message.signal,
694
+ ...message.offset !== void 0 && { offset: message.offset }
400
695
  });
401
696
  else if (message.type === "detached") resolveOutcome({
402
697
  kind: "detached",
403
- reason: message.reason
698
+ reason: message.reason,
699
+ ...message.offset !== void 0 && { offset: message.offset }
404
700
  });
405
701
  else if (message.type === "stdin_ack") {} else resolveOutcome({
406
702
  kind: "failed",
@@ -510,6 +806,59 @@ function buildOpenMessage(start) {
510
806
  };
511
807
  }
512
808
  //#endregion
809
+ //#region src/exec/invalid-resume-error.ts
810
+ var InvalidResumeError = class extends ExecError {
811
+ constructor(message, data) {
812
+ super("INVALID_RESUME", message, { data });
813
+ this.name = "InvalidResumeError";
814
+ }
815
+ };
816
+ //#endregion
817
+ //#region src/exec/invalid-state-error.ts
818
+ var InvalidStateError = class extends ExecError {
819
+ constructor(message, data) {
820
+ super("INVALID_STATE", message, { data });
821
+ this.name = "InvalidStateError";
822
+ }
823
+ };
824
+ //#endregion
825
+ //#region src/exec/no-session-error.ts
826
+ var NoSessionError = class extends ExecError {
827
+ constructor(message, data) {
828
+ super("NO_SESSION", message, { data });
829
+ this.name = "NoSessionError";
830
+ }
831
+ };
832
+ //#endregion
833
+ //#region src/exec/to-exec-error.ts
834
+ function toExecError(outcome) {
835
+ if (outcome.kind === "failed") return toFailedError(outcome.code, outcome.message, outcome.data);
836
+ 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");
837
+ if (outcome.kind === "unreachable") return new ExecError("UNREACHABLE", `cannot reach impd (${outcome.detail})`);
838
+ if (outcome.kind === "detached") return new ExecError("DETACHED", `detached from the session (${outcome.reason})`, { data: {
839
+ reason: outcome.reason,
840
+ ...outcome.offset !== void 0 && { offset: outcome.offset }
841
+ } });
842
+ if (outcome.kind === "closed") return outcome.closeCode === 1012 ? new ExecError("RESTARTING", "impd is restarting") : new ExecError("CONNECTION_CLOSED", `exec connection closed (${outcome.reason})`);
843
+ if (outcome.kind === "bad_message") return new ExecError("BAD_MESSAGE", `bad message from impd: ${outcome.detail}`);
844
+ return new ExecError("LOCAL_ERROR", "an output handler threw", { cause: outcome.error });
845
+ }
846
+ function toFailedError(code, message, data) {
847
+ if (code === "NO_SESSION") {
848
+ const parsed = NoSessionDataSchema.safeParse(data);
849
+ return new NoSessionError(message, parsed.success ? parsed.data : void 0);
850
+ }
851
+ if (code === "INVALID_STATE") {
852
+ const parsed = IMP_ERRORS.INVALID_STATE.data.safeParse(data);
853
+ if (parsed.success) return new InvalidStateError(message, parsed.data);
854
+ }
855
+ if (code === "INVALID_RESUME") {
856
+ const parsed = InvalidResumeDataSchema.safeParse(data);
857
+ if (parsed.success) return new InvalidResumeError(message, parsed.data);
858
+ }
859
+ return new ExecError(code ?? "EXEC_ERROR", message, { data });
860
+ }
861
+ //#endregion
513
862
  //#region src/exec/open-exec.ts
514
863
  const DEFAULT_MAX_UNREAD_BYTES = 8388608;
515
864
  const TTY_SIGNAL_KEYS = {
@@ -527,7 +876,8 @@ function openExec(deps, name, argv, options = {}) {
527
876
  ...options.cols !== void 0 && { cols: options.cols },
528
877
  ...options.rows !== void 0 && { rows: options.rows },
529
878
  ...options.session !== void 0 && { session: options.session },
530
- ...options.killGraceMs !== void 0 && { killGraceMs: options.killGraceMs }
879
+ ...options.killGraceMs !== void 0 && { killGraceMs: options.killGraceMs },
880
+ ...options.resumeFrom !== void 0 && { resumeFrom: options.resumeFrom }
531
881
  });
532
882
  }
533
883
  function openAttach(deps, name, session, options = {}) {
@@ -535,7 +885,9 @@ function openAttach(deps, name, session, options = {}) {
535
885
  name,
536
886
  session,
537
887
  ...options.cols !== void 0 && { cols: options.cols },
538
- ...options.rows !== void 0 && { rows: options.rows }
888
+ ...options.rows !== void 0 && { rows: options.rows },
889
+ ...options.resumeFrom !== void 0 && { resumeFrom: options.resumeFrom },
890
+ ...options.wake !== void 0 && { wake: options.wake }
539
891
  });
540
892
  }
541
893
  async function openHandle(deps, options, tty, start) {
@@ -606,7 +958,8 @@ async function openHandle(deps, options, tty, start) {
606
958
  const outcome = await session.outcome;
607
959
  const result = outcome.kind === "exit" ? { value: {
608
960
  code: outcome.code,
609
- signal: outcome.signal
961
+ signal: outcome.signal,
962
+ ...outcome.offset !== void 0 && { offset: outcome.offset }
610
963
  } } : { error: toExecError(outcome) };
611
964
  resolveHandle(result);
612
965
  };
@@ -1046,4 +1399,4 @@ function openReverseForward(options) {
1046
1399
  };
1047
1400
  }
1048
1401
  //#endregion
1049
- export { CLIENT_VERSION, CONSOLE_SHELL, ExecError, ORPCError, createImpClient, isDefinedError, openExecSession, openReverseForward, safe };
1402
+ export { CLIENT_VERSION, CONSOLE_SHELL, ExecError, InvalidResumeError, InvalidStateError, NoSessionError, ORPCError, createImpClient, isDefinedError, openExecSession, openReverseForward, safe };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/imp-client",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Typed client for impd, the imp host daemon: imps, checkpoints, images and exec",
5
5
  "license": "MIT",
6
6
  "repository": {