@promigence/sdk 0.1.0-beta.6 → 0.1.0-beta.8

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/cjs/pty.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PtySession = exports.FrameReader = exports.PTY_SECRETS_HEADER = exports.PTY_COMMAND_HEADER = exports.PTY_INPUT_CHUNK = void 0;
3
+ exports.PTY_RESTART_CLOSED_EXIT = exports.PTY_RESTART_EXIT = exports.PTY_RECONNECT_MAX_WAIT_MS = exports.PTY_RECONNECT_FIRST_WAIT_MS = exports.PTY_RECONNECT_FOR_MS = exports.PTY_RECONNECT_CODE = exports.PtySession = exports.FrameReader = exports.PTY_SECRETS_HEADER = exports.PTY_COMMAND_HEADER = exports.PTY_RESUME_HEADER = exports.PTY_INPUT_KEEP = exports.PTY_INPUT_CHUNK = void 0;
4
4
  exports.wsAccept = wsAccept;
5
5
  exports.encodeFrame = encodeFrame;
6
6
  exports.connectPty = connectPty;
@@ -8,6 +8,8 @@ const node_crypto_1 = require("node:crypto");
8
8
  const caller_js_1 = require("./caller.js");
9
9
  const proxy_js_1 = require("./proxy.js");
10
10
  exports.PTY_INPUT_CHUNK = 64 * 1024;
11
+ exports.PTY_INPUT_KEEP = 256 * 1024;
12
+ exports.PTY_RESUME_HEADER = "promigence-resume";
11
13
  const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
12
14
  exports.PTY_COMMAND_HEADER = "promigence-pty-command";
13
15
  exports.PTY_SECRETS_HEADER = "promigence-pty-secrets";
@@ -99,11 +101,17 @@ class PtySession {
99
101
  rows;
100
102
  closed = false;
101
103
  done;
104
+ pending;
105
+ resumeToken;
106
+ inputSent = 0;
107
+ inputBase = 0;
108
+ inputLog = Buffer.alloc(0);
102
109
  constructor(socket, open, done) {
103
110
  this.socket = socket;
104
111
  this.pid = open.pid;
105
112
  this.cols = open.cols;
106
113
  this.rows = open.rows;
114
+ this.resumeToken = open.resume;
107
115
  this.done = done;
108
116
  void done.then(() => { this.#ended = true; });
109
117
  }
@@ -111,10 +119,67 @@ class PtySession {
111
119
  get ended() {
112
120
  return this.#ended;
113
121
  }
114
- send(opcode, payload) {
122
+ holdInput() {
123
+ this.pending ??= [];
124
+ }
125
+ rebind(socket, open) {
126
+ this.socket = socket;
127
+ this.resumeToken = open.resume;
128
+ this.inputSent = 0;
129
+ this.inputBase = 0;
130
+ this.inputLog = Buffer.alloc(0);
131
+ this.flush(open);
132
+ }
133
+ resumed(socket, open) {
134
+ this.socket = socket;
135
+ this.resumeToken = open.resume ?? this.resumeToken;
136
+ const took = open.input ?? this.inputSent;
137
+ if (took < this.inputSent) {
138
+ const from = Math.max(took, this.inputBase) - this.inputBase;
139
+ const again = this.inputLog.subarray(from);
140
+ this.inputSent = Math.max(took, this.inputBase);
141
+ for (let at = 0; at < again.length; at += exports.PTY_INPUT_CHUNK) {
142
+ this.writeNow(0x2, again.subarray(at, at + exports.PTY_INPUT_CHUNK));
143
+ }
144
+ }
145
+ this.flush(open);
146
+ }
147
+ flush(open) {
148
+ const queued = this.pending ?? [];
149
+ this.pending = undefined;
150
+ if (open.cols !== this.cols || open.rows !== this.rows) {
151
+ this.resize({ cols: this.cols, rows: this.rows });
152
+ }
153
+ for (const f of queued)
154
+ this.writeNow(f.opcode, f.payload);
155
+ }
156
+ writeNow(opcode, payload) {
115
157
  if (this.closed || this.socket.destroyed)
116
158
  return;
117
159
  this.socket.write(encodeFrame(opcode, payload));
160
+ if (opcode === 0x2) {
161
+ this.inputSent += payload.length;
162
+ this.inputLog = Buffer.concat([this.inputLog, payload]);
163
+ if (this.inputLog.length > exports.PTY_INPUT_KEEP) {
164
+ const drop = this.inputLog.length - exports.PTY_INPUT_KEEP;
165
+ this.inputLog = this.inputLog.subarray(drop);
166
+ this.inputBase += drop;
167
+ }
168
+ }
169
+ }
170
+ get hungUp() {
171
+ return this.closed;
172
+ }
173
+ send(opcode, payload) {
174
+ if (this.closed)
175
+ return;
176
+ if (this.pending) {
177
+ if (this.pending.reduce((n, f) => n + f.payload.length, 0) + payload.length <= 1 << 20) {
178
+ this.pending.push({ opcode, payload: Buffer.from(payload) });
179
+ }
180
+ return;
181
+ }
182
+ this.writeNow(opcode, payload);
118
183
  }
119
184
  write(data) {
120
185
  const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
@@ -141,17 +206,50 @@ class PtySession {
141
206
  disconnect() {
142
207
  if (this.closed)
143
208
  return;
209
+ this.pending = undefined;
144
210
  this.send(0x8, Buffer.from([0x03, 0xe8]));
145
211
  this.closed = true;
146
212
  this.socket.end();
147
213
  }
148
214
  }
149
215
  exports.PtySession = PtySession;
150
- function connectPty(opts) {
151
- const url = new URL(opts.url);
216
+ exports.PTY_RECONNECT_CODE = 1012;
217
+ exports.PTY_RECONNECT_FOR_MS = 30_000;
218
+ exports.PTY_RECONNECT_FIRST_WAIT_MS = 250;
219
+ exports.PTY_RECONNECT_MAX_WAIT_MS = 4_000;
220
+ exports.PTY_RESTART_EXIT = {
221
+ exitCode: null,
222
+ signal: null,
223
+ error: {
224
+ code: "service_restart",
225
+ message: "the service restarted and the terminal could not reconnect within 30 s; open it again",
226
+ },
227
+ };
228
+ exports.PTY_RESTART_CLOSED_EXIT = {
229
+ exitCode: null,
230
+ signal: null,
231
+ error: { code: "service_restart", message: "the service restarted and closed the terminal; open it again" },
232
+ };
233
+ const CLOSED_EXIT = {
234
+ exitCode: null,
235
+ signal: null,
236
+ error: { code: "closed", message: "the terminal connection closed without an exit record" },
237
+ };
238
+ class LegRefused {
239
+ status;
240
+ error;
241
+ restart;
242
+ constructor(status, error, restart = false) {
243
+ this.status = status;
244
+ this.error = error;
245
+ this.restart = restart;
246
+ }
247
+ }
248
+ function openLeg(opts, url, resume) {
152
249
  const key = (0, node_crypto_1.randomBytes)(16).toString("base64");
153
250
  const req = (0, proxy_js_1.requestVia)(url, {
154
251
  method: "GET",
252
+ agent: false,
155
253
  headers: {
156
254
  connection: "Upgrade",
157
255
  upgrade: "websocket",
@@ -160,58 +258,51 @@ function connectPty(opts) {
160
258
  authorization: `Bearer ${opts.apiKey}`,
161
259
  "user-agent": opts.userAgent,
162
260
  ...(opts.caller ? { [caller_js_1.CALLER_HEADER]: opts.caller } : {}),
163
- ...(opts.command
261
+ ...(resume ? { [exports.PTY_RESUME_HEADER]: resume } : {}),
262
+ ...(opts.command && !resume
164
263
  ? { [exports.PTY_COMMAND_HEADER]: Buffer.from(opts.command, "utf8").toString("base64") }
165
264
  : {}),
166
- ...(opts.secrets?.length
265
+ ...(opts.secrets?.length && !resume
167
266
  ? {
168
267
  [exports.PTY_SECRETS_HEADER]: Buffer.from(JSON.stringify(opts.secrets), "utf8").toString("base64"),
169
268
  }
170
269
  : {}),
171
270
  },
172
271
  });
173
- return new Promise((resolve, reject) => {
272
+ let resolveEnded;
273
+ const ended = new Promise((r) => { resolveEnded = r; });
274
+ const opened = new Promise((resolve, reject) => {
174
275
  let settled = false;
175
- const fail = (e) => { if (!settled) {
276
+ const fail = (status, body, retryAfter, restart = false) => {
277
+ if (settled)
278
+ return;
176
279
  settled = true;
177
- reject(e);
178
- } };
280
+ reject(new LegRefused(status, opts.refuse(status, body, retryAfter), restart));
281
+ };
179
282
  const timer = setTimeout(() => {
180
283
  req.destroy();
181
- fail(opts.refuse(504, JSON.stringify({ error: { code: "timeout", message: `terminal handshake to ${url.host} took over ${opts.timeoutMs ?? 30_000} ms`, fix: "Retry; if it repeats, check the sandbox is ready (`promigence run report <run_id>`)." } })));
284
+ fail(504, JSON.stringify({ error: { code: "timeout", message: `terminal handshake to ${url.host} took over ${opts.timeoutMs ?? 30_000} ms`, fix: "Retry; if it repeats, check the sandbox is ready (`promigence run report <run_id>`)." } }));
182
285
  }, opts.timeoutMs ?? 30_000);
183
- req.on("error", (e) => { clearTimeout(timer); fail(opts.refuse(0, JSON.stringify({ error: { code: "unreachable", message: `cannot reach ${url.host}: ${e.code ?? e.message}` } }))); });
286
+ req.on("error", (e) => { clearTimeout(timer); fail(0, JSON.stringify({ error: { code: "unreachable", message: `cannot reach ${url.host}: ${e.code ?? e.message}` } })); });
184
287
  req.on("response", (res) => {
185
288
  clearTimeout(timer);
186
289
  const parts = [];
187
290
  res.on("data", (d) => parts.push(d));
188
291
  const retryAfter = res.headers["retry-after"];
189
- res.on("end", () => fail(opts.refuse(res.statusCode ?? 0, Buffer.concat(parts).toString("utf8"), typeof retryAfter === "string" ? retryAfter : undefined)));
292
+ res.on("end", () => fail(res.statusCode ?? 0, Buffer.concat(parts).toString("utf8"), typeof retryAfter === "string" ? retryAfter : undefined));
190
293
  });
191
294
  req.on("upgrade", (res, socket, head) => {
192
295
  clearTimeout(timer);
193
296
  if (res.headers["sec-websocket-accept"] !== wsAccept(key)) {
194
297
  socket.destroy();
195
- fail(opts.refuse(502, JSON.stringify({ error: { code: "internal", message: "the terminal server answered the WebSocket handshake with the wrong Sec-WebSocket-Accept" } })));
298
+ fail(502, JSON.stringify({ error: { code: "internal", message: "the terminal server answered the WebSocket handshake with the wrong Sec-WebSocket-Accept" } }));
196
299
  return;
197
300
  }
198
301
  socket.setNoDelay(true);
199
302
  const reader = new FrameReader();
200
303
  let exit;
201
- let session;
202
- let resolveDone;
203
- const done = new Promise((r) => { resolveDone = r; });
204
- let finished = false;
205
- const finish = () => {
206
- if (finished)
207
- return;
208
- finished = true;
209
- const e = exit ?? { exitCode: null, signal: null, error: { code: "closed", message: "the terminal connection closed without an exit record" } };
210
- opts.onExit?.(e);
211
- resolveDone(e);
212
- if (!settled)
213
- fail(opts.refuse(502, JSON.stringify({ error: { code: e.error?.code ?? "internal", message: e.error?.message ?? "the terminal ended before it opened" } })));
214
- };
304
+ let closeCode;
305
+ let wasOpen = false;
215
306
  const onFrames = (chunk) => {
216
307
  for (const f of reader.push(chunk)) {
217
308
  if (f.opcode === 0x2) {
@@ -225,14 +316,18 @@ function connectPty(opts) {
225
316
  catch {
226
317
  continue;
227
318
  }
228
- if (v["event"] === "open" && !session) {
229
- session = new PtySession(socket, {
319
+ if (v["event"] === "open" && !wasOpen) {
320
+ wasOpen = true;
321
+ settled = true;
322
+ resolve({
323
+ socket,
230
324
  pid: typeof v["req_id"] === "number" ? v["req_id"] : 0,
231
325
  cols: Number(v["cols"] ?? 80),
232
326
  rows: Number(v["rows"] ?? 24),
233
- }, done);
234
- settled = true;
235
- resolve(session);
327
+ ...(typeof v["resume"] === "string" ? { resume: v["resume"] } : {}),
328
+ ...(v["reattached"] === true ? { reattached: true } : {}),
329
+ ...(typeof v["input"] === "number" ? { input: v["input"] } : {}),
330
+ });
236
331
  }
237
332
  else if (v["event"] === "exit") {
238
333
  exit = {
@@ -252,17 +347,158 @@ function connectPty(opts) {
252
347
  socket.write(encodeFrame(0xa, f.payload));
253
348
  }
254
349
  else if (f.opcode === 0x8) {
350
+ if (closeCode === undefined)
351
+ closeCode = f.payload.length >= 2 ? f.payload.readUInt16BE(0) : 1005;
255
352
  if (!socket.destroyed)
256
353
  socket.end(encodeFrame(0x8, f.payload.subarray(0, 2)));
257
354
  }
258
355
  }
259
356
  };
260
357
  socket.on("data", onFrames);
261
- socket.on("close", finish);
358
+ socket.on("close", () => {
359
+ if (!wasOpen) {
360
+ const restart = closeCode === exports.PTY_RECONNECT_CODE && exit === undefined;
361
+ const e = restart ? exports.PTY_RESTART_CLOSED_EXIT : (exit ?? CLOSED_EXIT);
362
+ fail(restart ? 503 : 502, JSON.stringify({ error: { code: e.error?.code ?? "internal", message: e.error?.message ?? "the terminal ended before it opened" } }), undefined, restart);
363
+ return;
364
+ }
365
+ resolveEnded({ code: closeCode, exit });
366
+ });
262
367
  socket.on("error", () => { });
263
368
  if (head.length)
264
369
  onFrames(head);
265
370
  });
266
371
  req.end();
267
372
  });
373
+ return { opened, ended };
374
+ }
375
+ function sizedUrl(url, size) {
376
+ const u = new URL(url);
377
+ u.searchParams.set("cols", String(Math.floor(size.cols)));
378
+ u.searchParams.set("rows", String(Math.floor(size.rows)));
379
+ return u;
380
+ }
381
+ function resumeUrl(url, size, received) {
382
+ const u = sizedUrl(url, size);
383
+ u.searchParams.set("received", String(received));
384
+ return u;
385
+ }
386
+ function worthRetrying(status) {
387
+ return status === 0 || status === 429 || status >= 500;
388
+ }
389
+ function connectPty(opts) {
390
+ const reconnect = opts.reconnect ?? opts.command === undefined;
391
+ const resumeOn = opts.resume ?? true;
392
+ let received = 0;
393
+ const legOpts = {
394
+ ...opts,
395
+ onData: (d) => {
396
+ received += d.length;
397
+ opts.onData?.(d);
398
+ },
399
+ };
400
+ let resolveDone;
401
+ const done = new Promise((r) => { resolveDone = r; });
402
+ let finished = false;
403
+ const finish = (e) => {
404
+ if (finished)
405
+ return;
406
+ finished = true;
407
+ opts.onExit?.(e);
408
+ resolveDone(e);
409
+ };
410
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
411
+ const follow = (session, leg) => {
412
+ void leg.ended.then(async ({ code, exit }) => {
413
+ if (exit !== undefined || session.hungUp) {
414
+ finish(exit ?? CLOSED_EXIT);
415
+ return;
416
+ }
417
+ const restart = code === exports.PTY_RECONNECT_CODE;
418
+ const dropped = code === undefined || code === 1006;
419
+ let resumable = resumeOn && session.resumeToken !== undefined && (restart || dropped);
420
+ if (!restart && !resumable) {
421
+ finish(CLOSED_EXIT);
422
+ return;
423
+ }
424
+ if (!resumable && !reconnect) {
425
+ finish(exports.PTY_RESTART_CLOSED_EXIT);
426
+ return;
427
+ }
428
+ session.holdInput();
429
+ const t0 = Date.now();
430
+ for (let attempt = 0;; attempt++) {
431
+ const wait = Math.min(exports.PTY_RECONNECT_FIRST_WAIT_MS * 2 ** attempt, exports.PTY_RECONNECT_MAX_WAIT_MS) * (0.5 + Math.random() / 2);
432
+ if (Date.now() - t0 + wait > (opts.reconnectForMs ?? exports.PTY_RECONNECT_FOR_MS))
433
+ break;
434
+ await sleep(wait);
435
+ if (session.hungUp) {
436
+ finish(CLOSED_EXIT);
437
+ return;
438
+ }
439
+ const next = resumable
440
+ ? openLeg(legOpts, resumeUrl(opts.url, session, received), session.resumeToken)
441
+ : openLeg(legOpts, sizedUrl(opts.url, session));
442
+ try {
443
+ const o = await next.opened;
444
+ if (session.hungUp) {
445
+ o.socket.destroy();
446
+ finish(CLOSED_EXIT);
447
+ return;
448
+ }
449
+ if (resumable) {
450
+ session.resumed(o.socket, o);
451
+ }
452
+ else {
453
+ received = 0;
454
+ session.rebind(o.socket, o);
455
+ }
456
+ follow(session, next);
457
+ opts.onReconnected?.({ resumed: resumable });
458
+ return;
459
+ }
460
+ catch (e) {
461
+ const refused = e instanceof LegRefused ? e : undefined;
462
+ if (resumable && refused?.status === 404) {
463
+ resumable = false;
464
+ if (!restart) {
465
+ finish(CLOSED_EXIT);
466
+ return;
467
+ }
468
+ if (!reconnect) {
469
+ finish(exports.PTY_RESTART_CLOSED_EXIT);
470
+ return;
471
+ }
472
+ continue;
473
+ }
474
+ if (refused && !worthRetrying(refused.status)) {
475
+ const err = refused.error;
476
+ finish({ exitCode: null, signal: null, error: { code: typeof err.code === "string" ? err.code : "internal", message: err.message } });
477
+ return;
478
+ }
479
+ }
480
+ }
481
+ finish(restart ? exports.PTY_RESTART_EXIT : CLOSED_EXIT);
482
+ });
483
+ };
484
+ return (async () => {
485
+ const t0 = Date.now();
486
+ for (let attempt = 0;; attempt++) {
487
+ const first = openLeg(legOpts, new URL(opts.url));
488
+ try {
489
+ const o = await first.opened;
490
+ const session = new PtySession(o.socket, o, done);
491
+ follow(session, first);
492
+ return session;
493
+ }
494
+ catch (e) {
495
+ const wait = Math.min(exports.PTY_RECONNECT_FIRST_WAIT_MS * 2 ** attempt, exports.PTY_RECONNECT_MAX_WAIT_MS) * (0.5 + Math.random() / 2);
496
+ if (e instanceof LegRefused && e.restart && reconnect && Date.now() - t0 + wait <= (opts.reconnectForMs ?? exports.PTY_RECONNECT_FOR_MS)) {
497
+ await sleep(wait);
498
+ continue;
499
+ }
500
+ throw e instanceof LegRefused ? e.error : e;
501
+ }
502
+ }
503
+ })();
268
504
  }
@@ -88,6 +88,24 @@ export interface PtyCreateOpts extends Partial<PtySize> {
88
88
  cmd?: string;
89
89
  /** Handshake bound (default 30 s). */
90
90
  timeoutMs?: number;
91
+ /**
92
+ * When the connection drops, or the service is updated, the same terminal is picked up again
93
+ * over a new connection, where it left off: the same shell or command, still running.
94
+ * Default: on.
95
+ */
96
+ resume?: boolean;
97
+ /**
98
+ * When the service is updated and the terminal cannot be picked up again, the session goes on
99
+ * over a new connection in a NEW shell (what ran in the old one has ended), with its callbacks
100
+ * and its size, for up to 30 s of trying; past that it ends with `error.code`
101
+ * `"service_restart"`. Default: on for the login shell, off with `cmd` (reconnecting would start
102
+ * the command again).
103
+ */
104
+ reconnect?: boolean;
105
+ /** Called after each reconnect: `resumed` says whether it is the same terminal or a new shell. */
106
+ onReconnected?: (how: {
107
+ resumed: boolean;
108
+ }) => void;
91
109
  }
92
110
  /** The default terminal command. */
93
111
  export declare const DEFAULT_PTY_CMD = "TERM=${TERM:-xterm-256color} exec /bin/sh -l";
@@ -134,7 +152,6 @@ export interface SandboxCreateOpts extends SandboxOpts {
134
152
  alias?: string;
135
153
  /** Name of a stored secret holding the registry credentials. */
136
154
  registrySecret?: string;
137
- cpuClass?: string;
138
155
  /** Bound on the first build of an image (default 15 min). Expiry cancels nothing. */
139
156
  buildTimeoutMs?: number;
140
157
  /**
@@ -157,6 +174,20 @@ export interface SandboxCreateOpts extends SandboxOpts {
157
174
  * nobody), then it rejects with `TimeoutError` ("aborted by the caller's signal").
158
175
  */
159
176
  signal?: AbortSignal;
177
+ /**
178
+ * Stored secrets the sandbox receives, as environment variables: a name binds the secret to a
179
+ * variable of the same name, `{ name, as }` to a different one. Every command run against the
180
+ * sandbox gets them. As `Sandbox.fork` takes them.
181
+ */
182
+ secrets?: SecretBinding[];
183
+ /**
184
+ * The only destinations the sandbox may reach: `["github.com", "*.npmjs.org"]`. Everything
185
+ * else is refused, and the refusal names the host. It wins over `allowInternetAccess`. As
186
+ * `Sandbox.fork` takes it.
187
+ */
188
+ allowHosts?: string[];
189
+ /** What to favour, `start` or `compute`. As `Sandbox.fork` takes it. */
190
+ optimizeFor?: OptimizeFor;
160
191
  }
161
192
  /**
162
193
  * Create-or-reuse the snapshot for an image digest and return its `snapshot_id`.
@@ -165,7 +196,7 @@ export interface SandboxCreateOpts extends SandboxOpts {
165
196
  * inputs) was built before, or 202 with a `build_id`. The **body**, not the status, decides: an
166
197
  * idempotency replay answers 202 with a document that may already be `verified`.
167
198
  */
168
- export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "registrySecret" | "cpuClass" | "buildTimeoutMs">, hooks?: {
199
+ export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "registrySecret" | "buildTimeoutMs">, hooks?: {
169
200
  /**
170
201
  * The `POST /snapshots` answer, before any wait: `build_price` and whether it was `deduped`
171
202
  * or `joined` (nothing new charged). An implicit build costs money, so a caller
@@ -422,8 +453,7 @@ export declare class Sandbox {
422
453
  ok: boolean;
423
454
  ms: number;
424
455
  checked: string | string[];
425
- first_bad_path: string | null;
426
- probe: Record<string, unknown>;
456
+ first_bad_path?: string | null;
427
457
  }>;
428
458
  isRunning(): Promise<boolean>;
429
459
  /**
@@ -97,8 +97,6 @@ async function imageSnapshot(client, opts, hooks = {}) {
97
97
  create.alias = opts.alias;
98
98
  if (opts.registrySecret !== undefined)
99
99
  create.registrySecret = opts.registrySecret;
100
- if (opts.cpuClass !== undefined)
101
- create.cpuClass = opts.cpuClass;
102
100
  let build = (await client.snapshots.create(create)) ?? {};
103
101
  if (hooks.onBuild)
104
102
  await hooks.onBuild(build);
@@ -329,6 +327,9 @@ class Sandbox {
329
327
  ...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
330
328
  ...(opts.onData ? { onData: opts.onData } : {}),
331
329
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
330
+ ...(opts.resume === undefined ? {} : { resume: opts.resume }),
331
+ reconnect: opts.reconnect ?? opts.cmd === undefined,
332
+ ...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
332
333
  onExit: (e) => { this.terminals.delete(pid); opts.onExit?.(e); },
333
334
  });
334
335
  pid = session.pid;
package/dist/cjs/ssh.js CHANGED
@@ -10,6 +10,7 @@ function connectSshTunnel(opts) {
10
10
  const key = (0, node_crypto_1.randomBytes)(16).toString("base64");
11
11
  const req = (0, proxy_js_1.requestVia)(url, {
12
12
  method: "GET",
13
+ agent: false,
13
14
  headers: {
14
15
  connection: "Upgrade",
15
16
  upgrade: "websocket",