skydive-cli 0.5.0 → 0.6.0-beta.100

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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { n as PortalClient, r as RATE_LIMIT_STAND_DOWN_MS, t as AUTH_STAND_DOWN_MS } from "./client-HdxGBXPK.mjs";
3
+ import "./api-BFQ4PQDA.mjs";
4
+
5
+ export { PortalClient };
@@ -91,7 +91,7 @@ function parseFrame(expectedType, frameBytes) {
91
91
  const parsed = jsonLen ? JSON.parse(textDecoder.decode(frameBytes.subarray(jsonStart, jsonEnd))) : {};
92
92
  if (typeof parsed !== "object" || parsed === null) return null;
93
93
  json = parsed;
94
- } catch {
94
+ } catch (_error) {
95
95
  return null;
96
96
  }
97
97
  return {
@@ -113,6 +113,18 @@ const FRAME = {
113
113
  RESIZE: 17
114
114
  };
115
115
  const MAX_INPUT_BYTES = 1 * 1024 * 1024;
116
+ /** Upper bound for an exec's `timeoutMs`; larger asks are clamped to this. */
117
+ const MAX_EXEC_TIMEOUT_MS = 1440 * 60 * 1e3;
118
+ /**
119
+ * The exec budget applied when the client sends no `timeoutMs` of its own.
120
+ * Liberal by design — one-shot execs legitimately run installs, builds, and
121
+ * long data pulls — but not absent: without it, the e2b SDK's own default
122
+ * (60s) silently kills anything longer mid-run with an opaque
123
+ * `deadline_exceeded`, which is far below what the CLI's `--timeout` help
124
+ * promises ("the server default (~10 min) applies"). Callers with a bigger
125
+ * budget send an explicit `timeoutMs` (clamped to {@link MAX_EXEC_TIMEOUT_MS}).
126
+ */
127
+ const DEFAULT_EXEC_TIMEOUT_MS = 600 * 1e3;
116
128
  /** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
117
129
  function streamSpecToQuery(spec) {
118
130
  if (spec.mode === "pty") return {
@@ -124,7 +136,8 @@ function streamSpecToQuery(spec) {
124
136
  if (spec.mode === "exec") return {
125
137
  agentId: spec.agentId,
126
138
  mode: "exec",
127
- command: spec.command
139
+ command: spec.command,
140
+ ...spec.timeoutMs !== void 0 ? { timeoutMs: String(spec.timeoutMs) } : {}
128
141
  };
129
142
  return {
130
143
  agentId: spec.agentId,
@@ -223,8 +236,23 @@ var SandboxStream = class SandboxStream {
223
236
  }
224
237
  });
225
238
  let failure = null;
239
+ ws.on("unexpected-response", (_request, response) => {
240
+ const chunks = [];
241
+ let bytes = 0;
242
+ response.on("data", (chunk) => {
243
+ if (bytes >= MAX_HANDSHAKE_BODY_BYTES) return;
244
+ const remaining = MAX_HANDSHAKE_BODY_BYTES - bytes;
245
+ const kept = Buffer.from(chunk).subarray(0, remaining);
246
+ chunks.push(kept);
247
+ bytes += kept.length;
248
+ });
249
+ response.on("end", () => {
250
+ failure = formatHandshakeRejection(response.statusCode, response.statusMessage, Buffer.concat(chunks).toString("utf8"));
251
+ ws.terminate();
252
+ });
253
+ });
226
254
  ws.on("error", (err) => {
227
- failure = err.message;
255
+ failure ??= err.message;
228
256
  });
229
257
  ws.on("close", () => {
230
258
  this.closed = true;
@@ -258,13 +286,20 @@ var SandboxStream = class SandboxStream {
258
286
  } : {
259
287
  mode: "exec",
260
288
  agentId: opts.agentId,
261
- command: opts.command
289
+ command: opts.command,
290
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
262
291
  };
263
292
  const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
264
293
  for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
265
294
  return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
266
295
  }
267
296
  };
297
+ const MAX_HANDSHAKE_BODY_BYTES = 512;
298
+ function formatHandshakeRejection(statusCode, statusMessage, body) {
299
+ const status = [statusCode, statusMessage].filter(Boolean).join(" ");
300
+ const reason = Array.from(body, (char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 ? " " : char).join("").replace(/\s+/g, " ").trim();
301
+ return reason ? `${status}: ${reason}` : status || "WebSocket upgrade rejected";
302
+ }
268
303
  function toBuffer(data) {
269
304
  if (Buffer.isBuffer(data)) return data;
270
305
  if (Array.isArray(data)) return Buffer.concat(data);
@@ -272,4 +307,4 @@ function toBuffer(data) {
272
307
  }
273
308
 
274
309
  //#endregion
275
- export { encodeFsRequest as a, decodeFsResponse as i, SANDBOX_STREAM_PATH as n, streamSpecToQuery as r, SandboxStream as t };
310
+ export { FS_MAX_BLOB_BYTES as a, streamSpecToQuery as i, DEFAULT_EXEC_TIMEOUT_MS as n, decodeFsResponse as o, SANDBOX_STREAM_PATH as r, encodeFsRequest as s, SandboxStream as t };
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
2
3
  import { a as mintPortalDeviceToken, c as unifyLegacyCliGrants, i as grantPortalAccess, l as startTlsForward, n as fetchPortalDevices, r as findThisDevice } from "./api-BFQ4PQDA.mjs";
3
4
  import os from "node:os";
4
5
  import { z } from "zod";
@@ -160,7 +161,7 @@ function errorMessage(err) {
160
161
  * the commit time of the built tree is one monotonic clock they share.
161
162
  */
162
163
  function portalDaemonBuild() {
163
- return "1787785513";
164
+ return "1792017288";
164
165
  }
165
166
  /**
166
167
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -186,6 +187,8 @@ function parseStamp(stamp) {
186
187
 
187
188
  //#endregion
188
189
  //#region ../portal-protocol/src/index.ts
190
+ const CLOSE_SUPERSEDED = 4001;
191
+ const CLOSE_FLAP_COOLDOWN = 4002;
189
192
  const MAX_WS_FRAME_BYTES = 16 * 1024 * 1024;
190
193
  const T_DATA = 1;
191
194
  const T_CTRL = 2;
@@ -577,14 +580,41 @@ var JobManager = class {
577
580
 
578
581
  //#endregion
579
582
  //#region ../portal-daemon/src/client.ts
583
+ const INITIAL_BACKOFF_MS = 500;
584
+ const MAX_BACKOFF_MS = 1e4;
585
+ const AUTH_STAND_DOWN_MS = 5 * 6e4;
586
+ const AUTH_STAND_DOWN_MESSAGE = "portal sign-in expired — reopen Skydive Desktop (or run `skydive portal open`) to reconnect";
587
+ /**
588
+ * True when the error means the server could not authenticate us: a token-mint
589
+ * failure (HttpError 401) or a WS upgrade rejected at the auth layer. `ws`
590
+ * reports a rejected upgrade as `Unexpected server response: <status>` with no
591
+ * structured status property, so the status is read from the message. A 429 is
592
+ * deliberately NOT an auth failure — see isRateLimited.
593
+ */
594
+ function isAuthFailure(err) {
595
+ if (err instanceof HttpError) return err.status === 401;
596
+ return /Unexpected server response: 401\b/.test(errorMessage(err));
597
+ }
598
+ const RATE_LIMIT_STAND_DOWN_MS = 12e4;
599
+ const RATE_LIMIT_STAND_DOWN_MESSAGE = "portal connection rate limited — retrying in 2 minutes";
600
+ function isRateLimited(err) {
601
+ if (err instanceof HttpError) return err.status === 429;
602
+ return /Unexpected server response: 429\b/.test(errorMessage(err));
603
+ }
580
604
  const DEFAULT_HEARTBEAT = {
581
605
  intervalMs: 15e3,
582
606
  maxMissed: 2,
583
607
  wakeTickMs: 2e3,
584
608
  wakeGapThresholdMs: 1e4,
585
609
  netWatchTickMs: 2e3,
586
- initialBackoffMs: 500,
587
- maxBackoffMs: 1e4
610
+ initialBackoffMs: INITIAL_BACKOFF_MS,
611
+ maxBackoffMs: MAX_BACKOFF_MS,
612
+ supersededBackoffFloorMs: 5e3,
613
+ flapWindowMs: 6e4,
614
+ flapMaxStandDowns: 3,
615
+ flapBackoffMs: 6e4,
616
+ authStandDownMs: AUTH_STAND_DOWN_MS,
617
+ rateLimitStandDownMs: RATE_LIMIT_STAND_DOWN_MS
588
618
  };
589
619
  /**
590
620
  * A cheap fingerprint of the machine's network identity: every non-internal
@@ -664,11 +694,14 @@ var PortalClient = class {
664
694
  machineName = "";
665
695
  friendlyName = "";
666
696
  legacyGrantsChecked = false;
697
+ authRejected = false;
698
+ rateLimited = false;
667
699
  wakeWatch = null;
668
700
  lastTick = Date.now();
669
701
  netWatch = null;
670
702
  netFingerprint = networkFingerprint();
671
703
  backoffKick = null;
704
+ inStandDownSleep = false;
672
705
  dialReason = null;
673
706
  disconnectedAt = null;
674
707
  handingOff = false;
@@ -739,6 +772,7 @@ var PortalClient = class {
739
772
  this.ws.terminate();
740
773
  return;
741
774
  }
775
+ if (this.inStandDownSleep) return;
742
776
  this.kickBackoff();
743
777
  }
744
778
  kickBackoff() {
@@ -777,6 +811,8 @@ var PortalClient = class {
777
811
  if (this.enabled || this.disposed) return;
778
812
  this.enabled = true;
779
813
  this.error = null;
814
+ this.authRejected = false;
815
+ this.rateLimited = false;
780
816
  this.connectLoop();
781
817
  }
782
818
  disable() {
@@ -914,29 +950,68 @@ var PortalClient = class {
914
950
  this.opts.onMachineName(identity.machineName, identity.machineNameSource);
915
951
  }
916
952
  let backoff = this.heartbeatConfig.initialBackoffMs;
953
+ let standDowns = [];
917
954
  while (this.enabled && !this.disposed && !this.handingOff) {
918
955
  this.setStatus("connecting");
956
+ let sleepFloor = 0;
919
957
  try {
920
958
  const token = await this.wsToken();
921
- if (await this.runConnection(token)) backoff = this.heartbeatConfig.initialBackoffMs;
922
- else backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
959
+ const { healthy, standDown } = await this.runConnection(token);
960
+ if (standDown) {
961
+ this.setStatus("stand-down");
962
+ const now = Date.now();
963
+ standDowns.push(now);
964
+ standDowns = standDowns.filter((at) => now - at <= this.heartbeatConfig.flapWindowMs);
965
+ if (standDowns.length >= this.heartbeatConfig.flapMaxStandDowns) {
966
+ backoff = this.heartbeatConfig.flapBackoffMs;
967
+ this.setStatus("error", "another portal connection for this machine keeps taking over — if Skydive Desktop or `skydive portal open` is running twice, quit the duplicate; an unstable network can also cause this briefly");
968
+ } else backoff = Math.max(Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs), this.heartbeatConfig.supersededBackoffFloorMs);
969
+ sleepFloor = backoff / 2;
970
+ } else if (healthy) backoff = this.heartbeatConfig.initialBackoffMs;
971
+ else if (this.authRejected) {
972
+ this.setStatus("error", AUTH_STAND_DOWN_MESSAGE);
973
+ this.authRejected = false;
974
+ backoff = this.heartbeatConfig.authStandDownMs;
975
+ sleepFloor = backoff;
976
+ } else if (this.rateLimited) {
977
+ this.setStatus("error", RATE_LIMIT_STAND_DOWN_MESSAGE);
978
+ this.rateLimited = false;
979
+ backoff = this.heartbeatConfig.rateLimitStandDownMs;
980
+ sleepFloor = backoff;
981
+ } else backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
923
982
  } catch (err) {
924
983
  if (!this.enabled || this.disposed || this.handingOff) break;
925
- this.setStatus("error", errorMessage(err));
926
- backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
984
+ if (isAuthFailure(err)) {
985
+ this.setStatus("error", AUTH_STAND_DOWN_MESSAGE);
986
+ backoff = this.heartbeatConfig.authStandDownMs;
987
+ sleepFloor = backoff;
988
+ } else if (isRateLimited(err)) {
989
+ this.setStatus("error", RATE_LIMIT_STAND_DOWN_MESSAGE);
990
+ backoff = this.heartbeatConfig.rateLimitStandDownMs;
991
+ sleepFloor = backoff;
992
+ } else {
993
+ this.setStatus("error", errorMessage(err));
994
+ backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
995
+ }
927
996
  }
928
997
  if (!this.enabled || this.disposed || this.handingOff) break;
929
- await this.backoffSleep(Math.random() * backoff);
998
+ this.inStandDownSleep = sleepFloor > 0;
999
+ await this.backoffSleep(sleepFloor + Math.random() * Math.max(backoff - sleepFloor, 0));
1000
+ this.inStandDownSleep = false;
930
1001
  }
931
1002
  }
932
1003
  runConnection(token) {
933
1004
  return new Promise((resolve) => {
934
1005
  let settled = false;
935
1006
  let healthy = false;
1007
+ let standDown = false;
936
1008
  const settle = () => {
937
1009
  if (settled) return;
938
1010
  settled = true;
939
- resolve(healthy);
1011
+ resolve({
1012
+ healthy,
1013
+ standDown
1014
+ });
940
1015
  };
941
1016
  const ws = new WebSocket(portalWsUrl(this.opts.appUrl, this.machineName, this.friendlyName, {
942
1017
  build: portalDaemonBuild(),
@@ -974,6 +1049,8 @@ var PortalClient = class {
974
1049
  };
975
1050
  ws.on("open", () => {
976
1051
  missed = 0;
1052
+ this.authRejected = false;
1053
+ this.rateLimited = false;
977
1054
  this.dialReason = null;
978
1055
  this.disconnectedAt = null;
979
1056
  this.setStatus("connected");
@@ -987,8 +1064,18 @@ var PortalClient = class {
987
1064
  });
988
1065
  ws.on("error", (err) => {
989
1066
  this.error = errorMessage(err);
1067
+ if (isAuthFailure(err)) {
1068
+ this.authRejected = true;
1069
+ this.dialReason ??= "portal credentials rejected; standing down";
1070
+ }
1071
+ if (isRateLimited(err)) {
1072
+ this.rateLimited = true;
1073
+ this.dialReason ??= "portal connection rate limited; standing down";
1074
+ }
990
1075
  });
991
- ws.on("close", (code) => {
1076
+ ws.on("close", (code, reason) => {
1077
+ standDown = code === CLOSE_SUPERSEDED || code === CLOSE_FLAP_COOLDOWN || code === 1e3 && reason.toString() === "superseded";
1078
+ if (standDown) this.dialReason ??= `superseded (code ${code})`;
992
1079
  this.dialReason ??= `connection closed (code ${code})`;
993
1080
  this.disconnectedAt ??= Date.now();
994
1081
  clearInterval(heartbeat);
@@ -1048,4 +1135,4 @@ function sleep(ms) {
1048
1135
  }
1049
1136
 
1050
1137
  //#endregion
1051
- export { resolveMachineIdentity as a, isRecord as i, isNewerBuild as n, portalDaemonBuild as r, PortalClient as t };
1138
+ export { portalDaemonBuild as a, isNewerBuild as i, PortalClient as n, isRecord as o, RATE_LIMIT_STAND_DOWN_MS as r, resolveMachineIdentity as s, AUTH_STAND_DOWN_MS as t };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./client--k9cjfkX.mjs";
2
+ import "./client-HdxGBXPK.mjs";
3
3
  import "./tls-cert-Rua2oV7n.mjs";
4
4
  import "./api-BFQ4PQDA.mjs";
5
- import { a as runPortalDaemon, c as stopDaemonForHandoff, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-Do1jU2UF.mjs";
5
+ import { a as runPortalDaemon, c as stopDaemonForHandoff, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-B8c3Z1Te.mjs";
6
6
 
7
7
  export { runPortalDaemon };